내가 만든 요소도 UXML 에 태그로 들어간다
움직임은 클립이 아니라 두 값 사이에서 만들어진다까지가 유니티가 준 것으로 조립하는 이야기였습니다. 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; }
}
이름은 알아서 바뀝니다. 대문자에서 잘라 하이픈으로 잇습니다. myIntValue 는 my-int-value 가 됩니다. 마음에 안 들면 위의 셋째 줄처럼 직접 적습니다.
붙이고 나면 세 곳에서 같이 보입니다. uxml 에 속성으로 적을 수 있고, UI Builder 의 인스펙터에 칸이 생기고, 코드에서 그냥 프로퍼티입니다.
<StatBar title="HP" value="72" max-value="100" />
속을 채우고 클래스를 답니다
요소를 만들었으면 그 안에 무엇이 들어갈지는 생성자에서 정합니다. 여기서 2편에서 본 규칙을 그대로 씁니다. 유니티가 unity-toggle__label 같은 이름을 붙여 두는 그 방식입니다.
[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에 붙입니다. 만든 요소를 게임과 에디터 양쪽에서 씁니다