Search

UI매니저

Class
Type
Created
2023/06/10 01:10
updated
2023/06/10 04:30
날짜
UI매니저 ver 0.1

UI 매니저

우선, UI 매니저 소개에 앞서 앞으로 소개할 UI 매니저는 모두 제 개인적인 의견이 많이 포함된 것을 알려드립니다.
또한, 모든 소스는 아래에 오픈소스로 제공하겠습니다.
UIManager
AnSSa1996

UI 매니저 주요 기능

제가 만든 UI 매니저의 주요 기능은 다음과 같습니다.
UI 자동화
자동 바인딩(툴)
UI 관리(최적화 포함)

UI 자동화(UIBase)

모든 UI에서 사용할 기본적인 기능들을 바인딩, 애니메이션, 위치, 로테이션 등으로 제공하는 클래스 입니다. 이 클래스는 모든 UI에서 상속받아 사용합니다.

바인딩

제가 사용할 바인딩의 주요 목적은 자동화입니다. 아래 자동 바인딩 툴 또한 자동화를 목적으로 제작했습니다.
public class UIBase : MonoBehaviour { protected enum EComponents { } ... } }
C#
복사
모든 UI에서는 EComponents라는 값을 반드시 작성해야 합니다.
여기서 EComponents에는 바인딩할 오브젝트의 이름을 적어야 합니다.
만약 바인딩할 오브젝트가 없더라도, EComponents는 반드시 작성되어야 합니다.
(다만, 사실상 바인딩할 오브젝트가 없는 경우는 거의 없습니다.)
protected Dictionary<string, Transform> _bindingWidgetTransformDictionary = new Dictionary<string, Transform>(); protected virtual void BindingUI() { string[] enumStrings = System.Enum.GetNames(GetType().GetNestedType("EComponents")); var allTransform = transform.GetComponentsInChildren<RectTransform>(true); foreach (var currentTransform in allTransform) { var currentName = currentTransform.name; foreach (var enumString in enumStrings) { if (currentName.Contains(enumString)) { _bindingWidgetTransformDictionary.Add(enumString, currentTransform); } } } }
C#
복사
UIBase를 상속받은 클래스에 EComponents에 값을 자동으로 바인딩하려면 BindingUI를 호출해야 합니다.
그리고 EComponents에 선언된 모든 값에 해당하는 객체를 바인딩합니다.
바인딩 방식은 (오브젝트의 이름, RectTransform)입니다.
protected T GetControl<T>(Enum enumType) where T : Component { var enumName = enumType.ToString(); if (_bindingWidgetTransformDictionary.TryGetValue(enumName, out var componentTransform) == false) return null; return componentTransform.GetComponent<T>(); }
C#
복사
위 함수는 바인딩된 오브젝트에서 원하는 컴포넌트를 추출하는 함수입니다.
예를 들어, Test_Button이라는 오브젝트에 포함된 버튼 컴포넌트를 추출하려면 아래와 같이 추출하면 됩니다.
var button = GetControl<UIButton>(ECompoents.Test_Button);
C#
복사
보기에는 복잡해 보이지만 실제 사용 방법은 단 하나의 과정만 필요합니다.
아래는 바인딩의 규칙입니다.
바인딩할 오브젝트들은 자식 클래스의 EComponents에서만 관리합니다.
바인딩 처리는 모두 UIBase에서 담당합니다.
추후에는 개발자가 UIBase를 상속받아 EComponents에 등록하기만 하면 모든 기능을 사용할 수 있습니다.
따라서, UIBase가 발전하거나 수정되더라도 위의 규칙은 계속 유지됩니다.

이벤트 자동화

오브젝트 캐싱을 자동화한 후에는, 다음으로 사용할 오브젝트의 컴포넌트에 대한 자동화가 필요합니다.
protected void BindingEvent<T>(Enum enumType, UnityAction action) where T : Component { Transform componentTransform = null; var enumName = enumType.ToString(); if (_bindingWidgetTransformDictionary.TryGetValue(enumName, out componentTransform) == false) return; var type = typeof(T); if (typeof(UIButton).IsAssignableFrom(type)) { var uiButton = componentTransform.GetComponent<UIButton>(); uiButton?.onClick.AddListener(() => action()); } }
C#
복사
위 함수는 UIButton 컴포넌트에 대해 자동으로 클릭 액션을 등록하는 방법을 설명합니다.
예를 들어, 아래와 같이 사용할 수 있습니다.
BindingEvent<UIButton>(EComponents.Button_Test, () => Debug.Log("Test"));
C#
복사
위 코드는 Button_Test의 버튼을 클릭할 때 Test 로그를 찍는 방법입니다.
사실, 이 방법은 1인 개발자나 소수 개발자, 또는 바인딩 이벤트 규칙을 정확하게 명명하지 않으면 사용할 수 없습니다. BindingEvent의 타입에 따라 자동으로 등록될 규칙을 정해야 하기 때문입니다.
예를 들어, 위와 같은 버튼의 경우는 자동으로 '클릭' 이벤트에 들어갑니다. 그렇다면 스크롤 같은 경우는 어떨까요? 스크롤될 때? 맨 마지막 스크롤이 활성화될 때?
즉, 아주 복잡한 경우에도 바인딩 할 지, 아니면 기본적인 경우에만 바인딩 할 지를 정하고 그 규칙에 대해서도 정해야 합니다.
하지만 이런 규칙이 있고 기본적인 경우에만 적용한다면, 위 기능은 개발자가 개발하는 시간을 크게 줄여줄 수 있는 기능입니다.
제 이벤트 자동화 규칙은 다음과 같습니다.
버튼, 텍스트 등 기본적으로 커스텀되지 않은 기능들에 대해서만 바인딩합니다.
컴포넌트의 기능을 사용할 때, 가장 많이 사용되는 기능에 대해서만 바인딩합니다.
바인딩되는 이벤트는 컴포넌트당 하나만 등록됩니다. 즉, 버튼의 클릭에 등록되었다면, 바인딩 이벤트로 버튼의 Hover 등의 다른 이벤트는 등록할 수 없습니다.

애니메이션 자동화

해당 기능은 UI의 오픈, 클로즈 등의 애니메이션에 대한 기능입니다.
즉, UI가 어떤 상태가 되었을 때 전체적으로 진행될 애니메이션입니다.
[SerializeField] private ATransitionComponent animIn; [SerializeField] private ATransitionComponent animOut; public ATransitionComponent AnimIn { get { return animIn; } set { animIn = value; } } public ATransitionComponent AnimOut { get { return animOut; } set { animOut = value; } }
C#
복사
UIBase 에는 위와 같이 각각의 상태에서 실행될 애니메이션(ATransitionComponent)이 있습니다.
public abstract class ATransitionComponent : MonoBehaviour { public abstract void Animate(Transform target, bool fadeIn, UnityAction callWhenFinished); }
C#
복사
ATransitionComponent는 다음과 같은 내용을 가지고 있습니다.
기본적으로 이 추상 클래스는 애니메이션을 코루틴으로 진행하기 위해 상속받아 사용합니다. 애니메이션이 끝나면 callWhenFinished를 실행합니다.
여기서 중요한 점은 UI의 애니메이션을 위 클래스의 설계 방식에 의해, 애니메이션 혹은 트윈 등 다양하게 커스텀할 수 있다는 점입니다.
또한, 위 장점으로 아래와 같은 함수를 설계할 수 있습니다.
if (AnimIn.IsUnityNull()) AnimIn = transform.GetOrAddComponent<SimpleTransition>(); if (AnimOut.IsUnityNull()) AnimOut = transform.GetOrAddComponent<SimpleTransition>();
C#
복사
이런 식으로, 기본적으로 UI의 클래스에 애니메이션이 담겨있지 않다면, 기본적으로 사용자가 만든 클래스를 상속 받아 실행할 수 있습니다.
다음은 UI 애니메이션의 규칙입니다.
UI 애니메이션은 다양성을 추구해야 합니다. (애니메이션, 트윈 등 모든 경우의 수를 포함할 수 있어야 합니다.)
UI 애니메이션을 선언하지 않더라도 기본 애니메이션이 자동으로 설정됩니다.
또한, 기본 애니메이션들은 코드나 인스펙터에서 수정할 수 있습니다.

UI Transform 자동화

UI 요소는 기본적으로 애니메이션 또는 특정 경우에 위치, 회전, 크기 등의 변환(Transform)이 발생할 수 있습니다.
이때, 기본값으로 설정하면(위치 0,0,0, 회전 0,0,0, 크기 1,1,1 등) 해당 UI의 프리팹에 등록한 위치, 회전, 크기 등의 값들까지 초기화될 수 있습니다.
이를 위한 기능이 UI Transform 자동화입니다.
protected virtual void Awake() { _rectTransform = GetComponent<RectTransform>(); if (_rectTransform.IsUnityNull()) return; _originPos = _rectTransform.anchoredPosition; _originRot = _rectTransform.rotation; ... }
C#
복사
UI가 Awake되었을 때, 가장 초기 상태일 경우 프리팹에 설정되어있는 현재 값들을 저장합니다.
그리고 UI가 Open될 때, 아래와 같이 코드를 실행합니다.
public void Open(UIPriority priority = default) { ... _rectTransform.anchoredPosition = _originPos; _rectTransform.rotation = _originRot; _rectTransform.localScale = Vector3.one; ... }
C#
복사
즉, 포지션 로테이션은 초기 프리팹에 설정된 값으로 초기화됩니다.
여기서 의아한 부분은 왜 스케일이 (1,1,1)로 초기화되는지입니다.
이는 제 UI 설계 규칙 때문입니다.
UI 프리팹의 최상위 UI의 스케일은 1,1,1로 기본값입니다.
최상위 프리팹은 Text, Button 등과 같은 UI 컴포넌트를 갖고 있을 수 없습니다.
즉, 스케일을 조정해야 하는 경우 하위 오브젝트들의 스케일을 변경하여 조정합니다.

UI SortingOrder 하이러키 자동화

전 UI, 캔버스의 SortingOrder를 사용하지 않습니다.
아래는 제 설계 방법 중 가장 큰 규칙입니다.
캔버스는 단 두 개만 사용합니다.
Screen Space - Camera
World Space
일반적으로는 Screen Space - Overlay를 가장 많이 사용하지만, Space를 이용해 구현하는 것도 가능합니다. 이 방식을 사용하면 UI를 3D처럼 사용할 수 있어서 큰 장점이 있습니다. 또한, 유니티에서 UI를 표시할 때 하이러키의 순서에 따라 UI를 표시하는 기능과 합쳐졌을 때, Overlay는 필요하지 않습니다.
설계 과정에서는 다음과 같은 의문이 들 수 있습니다.
Overlay를 사용하지 않을 경우 해상도 대응은 어떻게 할까요? → 캔버스 스케일러를 이용하면 됩니다.
기존 UI 파티클은 어떻게 사용할 건데? → 기존 방식으로 사용하면 됩니다. 다만, UICamera에서만 파티클이 활용되고 그 크기도 조정되어야 합니다. (원래 파티클은 Space가 기본값입니다)
이외에도 다양한 이슈가 있을 수 있지만, 모두 해결 가능합니다.
캔버스를 극한으로 줄이는 이유는 UI의 관리 때문입니다. 캔버스를 여러개 사용하는 아틀라스를 진행할 경우 캔버스마다 DC가 발생합니다. 하지만, 제 방식을 사용한다면, 두 캔버스에서 활용되는 아틀라스의 갯수만이 DC가 발생하게 됩니다.
위 방식은 최적화를 극한으로 끌어올릴 수 있지만, 개발 비용은 높아질 것입니다.
그래서 선택한 방법이 바로 캔버스를 두 개로 줄이는 방법입니다.
서론이 매우 길었습니다. 그러나 이제 캔버스의 솔팅 오더를 사용하지 않더라도 UI들의 솔팅오더를 관리하는 방법을 설명하겠습니다.
우선, 기본적으로 UI의 기본 하이러키는 오브젝트 아래로 들어가면서 솔팅해줍니다. 이는 UIManager가 하는 기능이므로, 아래의 UI 관리에서 다시 설명하도록 하겠습니다.
protected virtual void HierarchyFixOnShow() { }
C#
복사
위는 UIBase가 가지고 있는 함수입니다.
이 함수는 상속을 목적으로 개발되었습니다. 위 함수를 통해서, UIManager를 통해 하이러키를 다시 조정하거나, 혹은 특정 UI를 찾아 그 UI 위에 혹은 바로 아래에 위치하는 등 다양한 방식으로 활용할 수 있습니다.
다만, 대부분의 UI는 UIManager로 하이러키를 조정하는 기능만으로 충분합니다.
우선, 제 자동화 방식은 유니티는 캔버스당 리프레시가 되기 때문에 이에 대해선 부하가 많이 발생하는 방식입니다. 그렇기에 CPU와 GPU의 성능을 잘 생각해가며 자동화가 필요합니다. 개발 비용을 줄이길 원하시면, 제 방식을 사용하시면 됩니다. 저도 추후, PopupUI, ScreenUI, ScreenUI_Low등으로 캔버스를 나눌 생각입니다.

UI 바인딩(툴)

이 기능은 위에서 설명한 UI 버인딩을 편하게 사용하기 위한 방법입니다.
프리팹: UI 프리팹
스크립트: UI 스크립트 (위 UI 프리팹의 컴포넌트로 있어야 합니다.)
컴포넌트: 검색할 컴포넌트를 입력합니다.
위와 같이 TMPro.TextMeshProUGUI, UIButton을 입력할 경우 해당 컴포넌트가 등록되어 있는 오브젝트를 찾아 자동으로 EComponents에 등록해줍니다.
ComponentNameInjector.cs
10.3KB

UI관리

마지막으로 UI를 관리하는 방법입니다.
UIManager를 사용하기 위해선, 아래 규칙을 우선 맞춰야 사용할 수 있습니다.
UI 프리팹과 UI 클래스의 이름이 같아야 합니다.
UIManager에서 UI를 생성할 때, 클래스의 이름으로 프리팹을 생성합니다. 그렇기 때문에 클래스의 이름과 프리팹의 이름이 같아야 합니다.
모든 UI는 UIBase를 상속 받아야 합니다.
각 UI를 관리하는 상위 UIController가 있어야 합니다.
예를 들어, PopupUI, ScreenUI 등으로 나누는 경우 PopupUIController와 ScreenUIController가 필요합니다.
public class UIManager : Singleton<UIManager> { [SerializeField] private WorldUIController _worldUIController; private ScreenUIController _screenController; private PopupUIController _popupController; ... }
C#
복사
위 코드는 제가 직접 사용하는 Controller들입니다.
public async UniTask<T> OpenUI<T>(UIPriority priority = UIPriority.Default) where T : UIBase { Type type = typeof(T); if (typeof(PopupUI).IsAssignableFrom(type) && priority != UIPriority.Default) { return await OpenPopupUI<T>(priority); } if (typeof(PopupUI).IsAssignableFrom(type)) { return await OpenPopUI<T>(); } if (typeof(ScreenUI).IsAssignableFrom(type) && priority != UIPriority.Default) { return await OpenScreenUI<T>(priority); } if (typeof(ScreenUI).IsAssignableFrom(type)) { return await OpenScreenUI<T>(); } return null; }
C#
복사
위의 로직은 가장 중요한 로직 중 하나입니다.
UI 클래스가 어떤 UI를 상속받고 있는지에 따라 UI를 열어줍니다.
var uiTest = UIManager.Instance.OpenUI<UITest>();
C#
복사
위와 같이 UI를 언제 어디서든 자유롭게 열고 닫을 수 있습니다.
추가로 다음 업데이트 내용입니다. (ver 0.2 업데이트할 내용)
UIBase
Show 기능 추가
Hide 기능 추가
UIController
각 하위 오브젝트 캔버스 분리
Container.cs 개발
버튼과 같은 공용 프리팹등의 하위 오브젝트들 위한 자동 바인딩 기능
위 Container,cs 개발 후 ComponentNameInjector.cs 수정
Container.cs 하위 오브젝트들은 등록하지 않게 수정.