내가 만든 요소도 UXML 에 태그로 들어간다
Esc

    내가 만든 요소도 UXML 에 태그로 들어간다

    –UnityUIUI Toolkit

    움직임은 클립이 아니라 두 값 사이에서 만들어진다까지가 유니티가 준 것으로 조립하는 이야기였습니다. Button, Label, TextField 를 놓고 스타일을 걸었습니다.

    화면이 늘어나면 같은 묶음이 반복됩니다. 아이콘과 이름과 수치가 한 줄로 붙은 것이 목록마다 나오고, 제목과 버튼 두 개가 붙은 팝업 틀이 화면마다 나옵니다. uGUI 에서는 그걸 프리팹으로 묶었습니다. 여기서는 방법이 둘입니다.

    구조만 같으면 uxml 을 다시 씁니다

    uxml 파일 하나를 다른 uxml 안에서 불러 쓸 수 있습니다. 두 줄입니다.

    <ui:Template src="Portrait.uxml" name="Portrait" />
    <ui:Instance template="Portrait" name="player1" />
    <ui:Instance template="Portrait" name="player2" />

    Template 이 파일을 가리키면서 이름을 붙이고, Instance 가 그 이름으로 찍어 냅니다. 프리팹과 제일 비슷한 것이 이것입니다.

    구조만 같고 값은 다르게 넣고 싶으면 앞 편에서 본 데이터 바인딩을 걸거나, 코드에서 찾아서 채웁니다. 원본 uxml 을 고치면 찍어 낸 자리가 전부 따라오는 것도 프리팹과 같습니다.

    여기까지로 되는 경우가 생각보다 많습니다. 구조가 고정이고 동작이 붙지 않는다면 C# 을 쓸 이유가 없습니다. 문서도 이쪽을 “a simpler solution with a fixed UI structure” 라고 적어 뒀습니다.

    동작이 붙으면 C# 으로 요소를 만듭니다

    값을 넣으면 막대가 그만큼 차야 한다든가, 누르면 속이 접혀야 한다든가 하면 그때부터는 요소를 만듭니다. 만드는 조건이 셋입니다.

    • [UxmlElement] 를 클래스에 붙입니다
    • 클래스를 partial 로 선언합니다
    • VisualElement 나 그 자손을 상속합니다
    using UnityEngine.UIElements;
    
    [UxmlElement]
    public partial class StatBar : VisualElement { }

    이 세 줄이면 uxml 에서 태그로 씁니다.

    <ui:UXML xmlns:ui="UnityEngine.UIElements">
        <StatBar />
    </ui:UXML>

    partial 이어야 하는 이유가 있습니다. 문서 표현으로 “a corresponding UxmlSerializedData class is generated in the partial class” 입니다. 유니티가 그 클래스 안에 직렬화용 코드를 생성해 넣습니다. 우리가 적지 않은 절반이 컴파일 때 붙는 셈입니다.

    UI Builder 의 Library 탭에도 자동으로 올라옵니다. 안 보이게 하려면 [HideInInspector] 를 붙입니다.

    인스펙터에 값을 내놓으려면 UxmlAttribute

    이벤트는 루트에서 내려갔다가 다시 올라온다에서 인스펙터로 함수 를 걸 수 없다고 했습니다. 값은 내놓을 수 있습니다. 필드나 프로퍼티에 [UxmlAttribute] 를 붙이면 됩니다.

    [UxmlElement]
    public partial class StatBar : VisualElement
    {
        [UxmlAttribute] public string title { get; set; }
        [UxmlAttribute] public float value { get; set; }
        [UxmlAttribute("max-value")] public float myMaxValue { get; set; }
    }
    이름 하나가 세 곳에 나타납니다 C# [UxmlAttribute] float myMaxValue uxml my-max-value="100" UI Builder 인스펙터 My Max Value 대문자에서 잘라 하이픈으로 잇습니다. myIntValue 는 my-int-value 가 됩니다 마음에 안 들면 [UxmlAttribute("max-value")] 처럼 직접 적습니다
    한 곳에 적으면 나머지 둘이 따라옵니다.

    이름은 알아서 바뀝니다. 대문자에서 잘라 하이픈으로 잇습니다. myIntValue 는 my-int-value 가 됩니다. 마음에 안 들면 위의 셋째 줄처럼 직접 적습니다.

    붙이고 나면 세 곳에서 같이 보입니다. uxml 에 속성으로 적을 수 있고, UI Builder 의 인스펙터에 칸이 생기고, 코드에서 그냥 프로퍼티입니다.

    <StatBar title="HP" value="72" max-value="100" />

    속을 채우고 클래스를 답니다

    요소를 만들었으면 그 안에 무엇이 들어갈지는 생성자에서 정합니다. 여기서 2편에서 본 규칙을 그대로 씁니다. 유니티가 unity-toggle__label 같은 이름을 붙여 두는 그 방식입니다.

    밖에서는 태그 하나, 안에는 부품 둘 쓰는 쪽 uxml <StatBar title="HP" /> 생성자가 만드는 속 Label VisualElement 붙는 클래스 이름 .stat-bar 바깥 틀 .stat-bar__label 글자 .stat-bar__fill 막대 쓰는 쪽에서 속을 건드릴 손잡이가 이 이름들뿐입니다
    유니티가 자기 컨트롤에 이름을 붙여 두는 방식 그대로입니다.
    [UxmlElement]
    public partial class StatBar : VisualElement
    {
        public static readonly string ussClassName = "stat-bar";
        public static readonly string labelUssClassName = ussClassName + "__label";
        public static readonly string fillUssClassName = ussClassName + "__fill";
    
        readonly Label m_Label = new Label();
        readonly VisualElement m_Fill = new VisualElement();
    
        public StatBar()
        {
            AddToClassList(ussClassName);
            m_Label.AddToClassList(labelUssClassName);
            m_Fill.AddToClassList(fillUssClassName);
            Add(m_Label);
            Add(m_Fill);
        }
    
        [UxmlAttribute]
        public string title
        {
            get => m_Label.text;
            set => m_Label.text = value;
        }
    }

    클래스 이름을 상수로 뽑아 두는 것은 습관 이상입니다. 쓰는 쪽에서 속을 건드릴 손잡이가 그 이름뿐 이기 때문입니다. .stat-bar__fill 에 색을 걸면 막대 색이 바뀌고, 그 이름이 없으면 손댈 방법이 없습니다.

    속이 복잡하면 uxml 로 그려 두고 생성자에서 불러올 수도 있습니다.

    public StatBar()
    {
        var asset = Resources.Load<VisualTreeAsset>("StatBar");
        asset.CloneTree(this);
    }

    문서가 나누는 기준이 분명합니다. “use UXML to define the structure, use USS to define the look, and use C# to define the logic of your custom control.” 구조가 조건에 따라 달라져야 하면 이쪽이고, 고정이면 위의 템플릿으로 충분합니다.

    옛 방식이 아직 남아 있습니다

    인터넷에서 찾은 예제가 전혀 다르게 생겼다면 옛 방식입니다. 유니티 2023.2 이전에는 UxmlFactory 와 UxmlTraits 를 직접 적었습니다.

    public class StatBar : VisualElement
    {
        public new class UxmlFactory : UxmlFactory<StatBar, UxmlTraits> { }
        public new class UxmlTraits : VisualElement.UxmlTraits
        {
            UxmlFloatAttributeDescription m_Value =
                new UxmlFloatAttributeDescription { name = "value", defaultValue = 0 };
            public override void Init(
                VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                ((StatBar)ve).value = m_Value.GetValueFromBag(bag, cc);
            }
        }
        public float value { get; set; }
    }
    [UxmlElement]
    public partial class StatBar : VisualElement
    {
        [UxmlAttribute]
        public float value { get; set; }
    }

    옛 방식이 지워진 것은 아닙니다. 문서 표현으로 “If an element lacks the UxmlElement attribute, Unity defaults to the UxmlTraits and UxmlFactory systems for serialization” 입니다. [UxmlElement] 가 없으면 옛 경로로 떨어집니다. 그래서 옛 코드가 그대로 돌아가고, 새로 쓸 때만 새 방식을 쓰면 됩니다.

    둘을 섞지는 마세요. 한 클래스에 둘 다 적어 두면 어느 쪽이 쓰이는지 헷갈립니다.

    에디터 창도 같은 것으로 만듭니다

    UI Toolkit 은 화면에 오브젝트를 놓지 않는다에서 유니티가 에디터 쪽에는 UI Toolkit 을 권한다 고 했습니다. 그 실제 모습이 이렇습니다.

    public class StatWindow : EditorWindow
    {
        [MenuItem("Window/Stat")]
        static void Open() => GetWindow<StatWindow>();
    
        void CreateGUI()
        {
            rootVisualElement.Add(new StatBar { title = "HP", value = 72 });
        }
    }

    CreateGUI 가 시작점입니다. 문서 표현으로 “UI Toolkit uses the CreateGUI method to add controls to Editor UI, and Unity calls the CreateGUI method automatically when the window needs to be displayed” 입니다. uxml 과 uss 를 읽는 코드도 여기에 넣으라고 적혀 있습니다.

    여기서 하나가 드러납니다. 위에서 만든 StatBar 를 에디터 창에서도 그대로 씁니다. uGUI 에서는 게임 화면의 UI 와 에디터 창이 완전히 다른 체계였습니다. 게임 쪽은 Canvas 였고 에디터 쪽은 IMGUI 였습니다. UI Toolkit 은 같은 VisualElement 라서 한 번 만든 것을 양쪽에서 씁니다.

    툴을 만들 일이 있는 팀에게는 이게 UI Toolkit 을 배우는 이유가 되기도 합니다. 게임 UI 를 uGUI 로 두더라도 에디터 툴은 이쪽으로 짜게 됩니다.

    정리

    • 묶는 방법이 둘입니다. 구조가 고정이면 uxml 템플릿, 동작이 붙으면 C# 요소 입니다
    • <ui:Template src name> 으로 이름을 붙이고 <ui:Instance template> 으로 찍어 냅니다. 프리팹에 제일 가깝습니다
    • C# 요소는 [UxmlElement] + partial + VisualElement 상속 셋입니다. partial 인 이유는 직렬화 코드가 생성돼 붙기 때문입니다
    • [UxmlAttribute] 를 붙이면 uxml 속성과 UI Builder 인스펙터 칸이 같이 생깁니다. 이름은 myIntValue → my-int-value 로 바뀝니다
    • 속 부품에는 블록__부품 식으로 클래스를 답니다. 쓰는 쪽에서 속을 건드릴 손잡이가 그 이름뿐입니다
    • [UxmlElement] 가 없으면 옛 UxmlFactory 경로로 떨어집니다. 옛 코드는 그대로 돌고, 새로 쓸 때만 새 방식입니다
    • 에디터 창은 CreateGUI 에서 rootVisualElement 에 붙입니다. 만든 요소를 게임과 에디터 양쪽에서 씁니다