안녕하세요 여러분, 제 이름은 Dmitrii Ivashchenko이고 MY.GAMES의 소프트웨어 엔지니어입니다. 이 글에서는 요소의 상태와 마크업을 기반으로 Unity에서 사용자 인터페이스를 개발하는 방법에 대해 설명하겠습니다.
우선, 문서에 따르면 여전히 런타임에 권장되는 Unity UI (uGUI) 기술의 맥락에서 이야기할 것이라는 점에 유의해야 합니다. 설명된 접근 방식은 UI Toolkit , IMGUI 또는 기타 UI 구축 시스템에는 적용되지 않습니다.
Unity 프로젝트에서는 대부분 MonoBehaviour
에서 상속되고 수많은 SerializeField
필드가 포함된 View 클래스를 기반으로 구축된 UI 구현을 접하게 됩니다. 이 접근 방식을 사용하면 UI 동작을 완전히 제어할 수 있지만 사용된 아키텍처에 따라 뷰 및 프레젠터 수준에서 많은 양의 코드를 작성해야 합니다.
종종 프로젝트 개발이 계속됨에 따라 이러한 클래스는 엄청난 크기로 늘어나고 GameObject 자체의 구성 요소는 내부 개체에 대한 수많은 링크로 덮여 있습니다.
이와 같이 구성 요소를 수정하는 것도 즐겁지 않습니다. 클래스의 새 요소에 대한 참조를 얻으려면 SerializeField
추가하고, 코드를 다시 컴파일하고, 프리팹 구성 요소에서 새 필드를 찾아 필요한 개체를 그 안으로 끌어야 합니다. 프로젝트가 성장함에 따라 컴파일 시간, 필드 수, 프리팹 구성의 복잡성도 차례로 증가합니다.
결과적으로 우리는 MonoBehaviour
의 거대하고 과부하된 하위 클래스(또는 선호도에 따라 다수의 작은 하위 클래스)로 끝납니다.
그러한 UI의 동작에 대한 모든 변경 사항은 프로그래머의 작업이며 해당 작업에는 코드 검토, 병합 충돌 해결, 테스트를 통한 코드 적용 등 모든 관련 비용이 따른다는 점을 고려해 볼 가치가 있습니다.
SerializeField
필요하며, 그런 다음 요구 사항에 따라 작동하도록 많은 양의 코드가 작성됩니다. . 당연히 프로그래머만이 이를 처리할 수 있으며 구현은 시간이 오래 걸리고 비용이 많이 들고 매우 효율적입니다(종종 누구라도 알아챌 수 있는 것보다 훨씬 더 효율적입니다).
이제 uGUI 작업의 몇 가지 어려움을 강조했으므로 이 문제를 해결하기 위한 다른 접근 방식에 대해 이야기하고 싶습니다.
애완동물 프로젝트 중 하나를 진행하는 동안 저는 Unity에서 구조화된 UI 개발을 위한 라이브러리를 개발했습니다. 나중에 우리 팀과 저는 이를 프로덕션 환경에서 테스트했고 결과에 만족했습니다.
라이브러리의 소스 코드는 GitHub에서 다운로드할 수 있습니다 .
라이브러리의 핵심 요소는 StatefulComponent
구성 요소입니다. 이 구성 요소는 각 화면의 루트 GameObject에 배치되며 탭 전체에 분산된 내부 요소에 대한 모든 필수 참조를 포함합니다.
각 링크의 이름은 해당 역할 에 따라 지정됩니다. 코드 관점에서 볼 때 역할 집합은 일반 enum
입니다. 각 UI 요소 유형(버튼, 이미지, 텍스트 등)에 대해 별도의 역할 세트가 준비됩니다.
public enum ButtonRole { ... } public enum ImageRole { ... } public enum TextRole { ... } ...
역할은 구성 요소에서 직접 생성되므로 enum
수동으로 편집할 필요가 없습니다. 이러한 enum
형 요소는 생성 후 즉시 사용할 수 있으므로 역할을 생성할 때 재컴파일을 기다릴 필요도 없습니다.
병합 충돌을 단순화하기 위해 열거형 값은 요소 이름을 기반으로 계산됩니다.
[StatefulUI.Runtime.RoleAttributes.ButtonRoleAttribute] public enum ButtonRole { Unknown = 0, Start = -1436209294, // -1436209294 == "Start".GetHashCode() Settings = 681682073, Close = -1261564850, Quests = 1375430261, }
이를 통해 귀하와 동료가 동시에 다른 분기의 버튼에 대한 새 역할을 생성하는 경우 프리팹에서 직렬화된 값이 손상되는 것을 방지할 수 있습니다.
각 유형의 UI 요소(버튼, 텍스트, 이미지)는 자체 탭에 있습니다.
역할을 사용하면 프리팹 내부의 모든 요소에 대한 완전한 마크업이 달성됩니다. 이미지와 텍스트에 액세스하는 데 SerializeField
세트가 더 이상 필요하지 않으며 StatefulComponent
에 대한 참조가 하나 있고 스프라이트 교체 등을 위해 원하는 이미지의 역할을 아는 것으로 충분합니다.
UnityEngine.UI.Text
및 TextMeshProUGUI
포함한 텍스트UnityEngine.UI.InputField
및 TMP_InputField
포함한 TextInput
주석이 달린 객체로 작업하기 위한 해당 방법이 있습니다. 코드에서 StatefulComponent
에 대한 참조를 사용하거나 StatefulView
에서 클래스를 상속할 수 있습니다.
public class ExamplePresenter { private StatefulComponent _view; public void OnOpen() { _view.GetButton(ButtonRole.Settings).onClick.AddListener(OnSettingsClicked); _view.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); _view.GetSlider(SliderRole.Volume).onValueChanged.AddListener(OnVolumeChanged); } } public class ExampleScreen : StatefulView { private void Start() { SetText(TextRole.Title, "Hello World"); SetTextValues(TextRole.Timer, hours, minutes, seconds); SetImage(ImageRole.UserAvatar, avatarSprite); } }
텍스트가 있는 탭에는 개체에 대한 역할 및 링크 외에 다음 열이 포함되어 있습니다.
라이브러리에는 번역 작업을 위한 내장 하위 시스템이 포함되어 있지 않습니다. 지역화 시스템을 연결하려면 ILocalizationProvider
인터페이스 구현을 만들어야 합니다. 예를 들어 백엔드, ScriptableObjects 또는 Google Sheets를 기반으로 구성할 수 있습니다.
public class HardcodeLocalizationProvider : ILocalizationProvider { private Dictionary<string, string> _dictionary = new Dictionary<string, string> { { "timer" , "{0}h {1}m {2}s" }, { "title" , "Título do Jogo" }, { "description" , "Descrição longa do jogo" }, }; public string GetPhrase(string key, string defaultValue) { return _dictionary.TryGetValue(key, out var value) ? value : defaultValue; } }
현지화 복사 버튼을 클릭하면 코드 및 값 열의 내용이 Google 스프레드시트에 붙여넣기에 적합한 형식으로 클립보드에 복사됩니다.
재사용을 용이하게 하기 위해 UI의 개별 부분을 별도의 프리팹으로 추출하는 경우가 많습니다. 또한 StatefulComponent
사용하면 각 구성 요소가 자체 하위 인터페이스 요소와만 작동하는 구성 요소 계층 구조를 만들 수 있습니다.
내부 구성 요소 탭에서는 내부 구성 요소에 역할을 할당할 수 있습니다.
구성된 역할은 다른 유형의 요소와 유사하게 코드에서 사용할 수 있습니다.
var header = GetInnerComponent(InnerComponentRole.Header); header.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); header.SetText(TextRole.Title, "Header Title"); var footer = GetInnerComponent(InnerComponentRole.Footer); footer.GetButton(ButtonRole.Continue).onClick.AddListener(OnContinueClicked); footer.SetText(TextRole.Message, "Footer Message");
유사한 요소 목록을 만들려면 ContainerView
구성 요소를 사용할 수 있습니다. 인스턴스화를 위한 프리팹과 루트 객체를 지정해야 합니다(선택 사항). 편집 모드에서는 StatefulComponent
사용하여 요소를 추가하고 제거할 수 있습니다.
인스턴스화된 프리팹의 콘텐츠를 마크업하기 위해 StatefulComponent
사용하는 것이 편리합니다. 런타임에서는 AddInstance<T>
, AddStatefulComponent
또는 FillWithItems
메서드를 사용하여 컨테이너를 채울 수 있습니다.
var container = GetContainer(ContainerRole.Players); container.Clear(); container.FillWithItems(_player, (StatefulComponent view, PlayerData data) => { view.SetText(TextRole.Name, data.Name); view.SetText(TextRole.Level, data.Level); view.SetImage(ImageRole.Avatar, data.Avatar); });
표준 Object.Instantiate()
객체 생성에 적합하지 않은 경우 예를 들어 Zenject를 사용한 인스턴스화의 경우 이 동작을 재정의할 수 있습니다.
StatefulUiManager.Instance.CustomInstantiateMethod = prefab => { return _diContainer.InstantiatePrefab(prefab); };
내부 구성 요소와 컨테이너는 각각 StatefulComponent
에 대한 정적 및 동적 중첩을 제공합니다.
우리는 프리팹의 마크업, 현지화, 인스턴스화를 고려했습니다. 이제 가장 흥미로운 부분인 상태 기반 UI 개발로 넘어갈 시간입니다.
우리는 프리팹에 대한 명명된 변경 집합으로 상태 개념을 고려해 보겠습니다. 이 경우 이름은 StateRole
열거형의 역할이며 프리팹에 대한 변경 예는 다음과 같습니다.
일련의 변경 사항(상태 설명)은 상태 탭에서 구성할 수 있습니다. 구성된 상태는 검사기에서 직접 적용할 수 있습니다.
SetState
메서드를 사용하여 코드에서 구성된 상태를 적용할 수 있습니다.
switch (colorScheme) { case ColorScheme.Orange: SetState(StateRole.Orange); break; case ColorScheme.Red: SetState(StateRole.Red); break; case ColorScheme.Purple: SetState(StateRole.Purple); break; }
도구 탭에서 활성화 시 초기 상태 적용 매개 변수가 활성화되면 개체 인스턴스화 시 즉시 적용되는 상태를 구성할 수 있습니다.
상태를 사용하면 View 클래스 수준에서 필요한 코드 양을 크게 줄일 수 있습니다. 화면의 각 상태를 StatefulComponent
의 변경 사항 집합으로 설명하고 게임 상황에 따라 코드에서 필요한 상태를 적용하면 됩니다.
실제로 상태를 기반으로 UI를 개발하는 것은 매우 편리합니다. 시간이 지남에 따라 또 다른 문제가 발생합니다. 프로젝트가 발전함에 따라 단일 창의 상태 목록이 감당할 수 없을 만큼 길어져서 탐색하기 어려워질 수 있습니다. 또한 일부 다른 상태의 맥락에서만 의미가 있는 상태도 있습니다. 이 문제를 해결하기 위해 Statful UI에는 상태 트리라는 또 다른 도구가 있습니다. 상태 탭의 상태 트리 편집기 버튼을 클릭하여 액세스할 수 있습니다.
상위 상태(이 예에서는 Reward )는 하위 상태가 호출될 때마다 적용됩니다.
구성된 StatefulComponent
관리는 필요한 데이터로 구성 요소를 채우고 적절한 순간에 상태를 전환하는 간단하고 이해하기 쉬운 최소한의 코드로 귀결됩니다.
public void ShowIntro() { SetState(StateRole.Intro); } public void ShowReward(IReward reward) { // Update the inner view with the reward reward.UpdateView(GetInnerComponent(InnerComponentRole.Reward)); // Switch on the type of reward switch (reward) { case ICardsReward cardsReward: SetState(StateRole.Cards); break; case IMoneyReward moneyReward: SetState(StateRole.Money); break; case IEmojiReward emojiReward: SetState(StateRole.Emoji); break; } } public void ShowResults(IEnumerable<IReward> rewards) { SetState(StateRole.Results); // Fill the container with the rewards GetContainer(ContainerRole.TotalReward) .FillWithItems(rewards, (view, reward) => reward.UpdateView(view)); }
역할은 나중에 코드에서 사용할 수 있도록 링크와 상태의 이름을 지정하는 편리하고 명확한 방법을 제공하기 위한 것입니다. 그러나 상태를 설명하는 데 너무 긴 이름이 필요한 상황이 있으므로 이 링크가 가리키는 것이 무엇인지, 상태가 어떤 동작을 반영하는지에 대해 간단한 설명을 남기는 것이 더 편리할 것입니다. 이러한 경우 StatefulComponent
의 각 링크와 상태를 사용하여 설명을 추가할 수 있습니다.
각 탭에서 API 복사 및 문서 복사 버튼을 이미 보셨을 것입니다. 이 버튼은 선택한 섹션에 대한 정보를 복사합니다. 그 외에도 도구 탭에는 유사한 버튼이 있습니다. 이 버튼은 모든 섹션에 대한 정보를 한 번에 복사합니다. API 복사 버튼을 클릭하면 이 StatfulComponent
개체를 관리하기 위해 생성된 코드가 클립보드에 복사됩니다. 보상 창의 예는 다음과 같습니다.
// Insert the name of the chest here SetText(TextRole.Title, "Lootbox"); // Button to proceed to the reward issuance phase GetButton(ButtonRole.Overlay); // Button to display information about the card GetButton(ButtonRole.Info); // Container for displaying the complete list of awarded rewards GetContainer(ContainerRole.TotalReward); // Insert the card image here SetImage(ImageRole.Avatar, null); // Animated appearance of a chest SetState(StateRole.Intro);
문서 복사 버튼을 클릭하면 이 프리팹에 대한 문서가 마크다운 형식으로 클립보드에 복사됩니다.
### RewardScreen Buttons: - Overlay - Button to proceed to the reward issuance phase - Info - Button to display information about the card Texts: - Title - Insert the name of the chest here Containers: - TotalReward - Container for displaying the complete list of awarded rewards Images: - Avatar - Insert the card image here States: - Intro - Animated appearance of a chest - Cards - Displaying rewards in the form of a card - Money - Displaying rewards in the form of game currency - Emoji - Displaying rewards in the form of an emoji - Results - Displaying a complete list of issued rewards
이렇게 상세한 설명으로 이 화면을 구현할 때 실수를 저지르는 것은 꽤 어려운 일임은 분명합니다. 프로젝트의 지식 베이스에서 UI 조직에 대한 최신 정보를 쉽게 유지할 수 있습니다.
동시에 Stateful UI를 사용하면 UI 프리팹 생성을 위임할 수 있습니다. 실제로 상태 기반 마크업을 사용하면 프로그래머에게 프리팹을 전달하기 전에 프리팹의 동작을 완전히 테스트할 수 있습니다. 이는 게임 디자이너 , 기술 디자이너 또는 별도의 UI 개발자가 프리팹을 준비할 수 있음을 의미합니다. 게다가 코드와 프리팹 사이에 API가 생성되므로 프리팹 프로그래밍과 구성을 동시에 수행할 수 있습니다! 필요한 것은 API를 미리 공식화하는 것뿐입니다. 그러나 프리팹 구성 작업이 프로그래머에게 남아 있더라도 Stateful UI를 사용하면 이 작업 속도가 크게 향상됩니다.
앞서 살펴보았듯이 Stateful UI는 UI 요소 상태 작업을 크게 단순화합니다. SerializeField를 생성하고, 코드를 다시 컴파일하고, 수많은 View 클래스 필드에서 참조를 검색하는 데 더 이상 긴 주기가 필요하지 않습니다. View 클래스 자체에서는 개체를 켜고 끄거나 텍스트 색상을 변경하는 등의 반복 작업을 위해 더 이상 많은 양의 코드를 작성할 필요가 없습니다.
라이브러리를 사용하면 프로젝트의 레이아웃 구성, 프리팹 내의 객체 표시, 상태 생성, UI 요소에 연결, UI 관리를 위한 API 및 문서 제공에 대한 일관된 접근 방식을 사용할 수 있습니다. 또한 UI 프리팹 생성을 위임하고 작업 속도를 높일 수 있습니다.
상태 기능 확장, 새로운 유형의 애니메이션, 상태에서 사운드 재생 등과 같은 설명의 새로운 유형의 UI 변경 지원
텍스트 및 이미지 색상 지정을 위한 색상 팔레트 지원 추가
GameObjects 재사용을 통해 항목 목록에 대한 지원 추가
더 많은 수의 Unity UI 요소 지원
현지화를 위해 추가된 텍스트 언로드 자동화
테스트 프레임워크 구현. 프리팹에 대한 철저한 마크업이 있으므로 다음 형식으로 설정하기 쉬운 ScriptableObject 기반 시나리오를 만들 수 있습니다.
ButtonRole.Settings
버튼을 클릭합니다.
TextRole.SomeText
의 텍스트가 "일부 값"과 같은지 확인하세요.
ImageRole.SomeImage
에서 이미지를 확인하여 특정 스프라이트와 동일한지 확인하세요.
튜토리얼 시스템. 테스트와 유사하게 표시된 레이아웃을 사용하면 " ButtonRole.UpgradeHero
버튼에 포인터 표시"와 같은 지침 형식으로 ScriptableObject 기반 튜토리얼 시나리오를 생성할 수 있습니다.
프로젝트 소스 코드는 GitHub에서 사용할 수 있습니다 . 이슈를 생성하거나 라이브러리에 기여하는 것을 환영합니다!