Unity 게임 만들기 프로젝트 - UI Manager, 자동화
- -
1. UI Manager
게임에는 정말 많은 UI 가 들어간다. 작은 Image 부터 버튼, 팝업 등등 많은 오브젝트를 처리해야하기 때문에 작은 규모의 게임에선 일일히 오브젝트 생성 및 스크립트 연결을 해줄 수 있지만 규모가 커지면 한계가 있다.
따라서 오브젝트를 자동으로 생성, 스크립트를 연결해주는 등의 작업을 미리 해놓아서 Prefabs 만 만든다면 언제든 게임에 코드로 사용할 수 있도록 자동화 하는 작업이다.
그 전에 편리한 메서드를 만들어 놓아 자동화를 쉽게 구현할 수 있도록 돕는 Util을 작성한다.
# Util.cs
using UnityEngine;
using System.Collections;
public class Util
{
public static T GetOrAddComponent<T>(GameObject go) where T : UnityEngine.Component
{
T component = go.GetComponent<T>();
if (component == null)
component = go.AddComponent<T>();
return component;
}
public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
{
Transform transform = FindChild<Transform>(go, name, recursive);
if (transform == null)
return null;
return transform.gameObject;
}
public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object
{
if (go == null)
return null;
if (recursive == false)
{
for (int i = 0; i < go.transform.childCount; i++)
{
Transform transform = go.transform.GetChild(i);
if (string.IsNullOrEmpty(name) || transform.name == name)
{
T component = transform.GetComponent<T>();
if (component != null)
return component;
}
}
}
else
{
foreach (T component in go.GetComponentsInChildren<T>())
{
if (string.IsNullOrEmpty(name) || component.name == name)
return component;
}
}
return null;
}
}
- GetOrAddComponent : GameObject 의 매개변수를 입력하고 Generic Type 으로 선언되어 있어 원하는 Type 을 넣으면 해당 컴포넌트를 반환해주는 함수, 만약 해당 컴포넌트가 없다면 생성한 후에 생성된 컴포넌트를 반환한다.
- FindChild<T> : 입력된 GameObject 의 자식 중 Generic Type 의 자식을 찾는 함수, 재귀적으로 GO 밑의 모든 자식 중에 찾을 것인지도 정할 수 있다. 자식 중에 입력된 이름과 동일한 컴포넌트가 있다면 반환한다.
- FindChild : 위는 T 타입의 컴포넌트를 찾는 함수였다면, 이 함수는 자식 GameObject 를 찾는데 사용되는 함수이다.
이로써 원하는 타입의 컴포넌트 혹은 오브젝트를 찾고, 참조할 수 있는 방안이 마련되었다.
코드 어디에서든 특정 GameObject 에 대한 컴포넌트를 찾거나 생성할 수 있고, 자식 중에 입력한 이름을 가진 컴포넌트나 Go 를 찾을 수 있다.
# Extension.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public static class Extension
{
public static T GetOrAddComponent<T>(this GameObject go) where T : UnityEngine.Component
{
return Util.GetOrAddComponent<T>(go);
}
public static void ADDUIEvent(this GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
{
UI_Base.ADDUIEvent(go, action, type);
}
}
extension 은 입력한 형식에서 바로 사용이 가능한 함수를 만드는 작업이다.
this GameObject go 라고 매개변수를 받고 있기 때문에, 해당 코드로 인해 GameObject 형식에서 바로 GetOrAddComponent, ADDUIEvent 의 함수를 사용할 수 있게 된다.
여기까지 편의를 위한 메서드를 먼저 작성해 놓은 뒤, 해당 코드를 참고하여 UI 기능을 구현한다.
# UI_EventHandler.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class UI_EventHandler : MonoBehaviour, IPointerClickHandler, IDragHandler
{
public Action<PointerEventData> OnClickHandler = null;
public Action<PointerEventData> OnDragHandler = null;
public void OnDrag(PointerEventData eventData)
{
if (OnDragHandler != null)
OnDragHandler.Invoke(eventData);
}
public void OnPointerClick(PointerEventData eventData)
{
if (OnClickHandler != null)
OnClickHandler.Invoke(eventData);
}
}
UI 는 특정 기능을 가지고 있다. 드래그 혹은 클릭을 통해 이벤트가 발생할 수 있기 때문에, 먼저 이벤트를 감지하는 EventHandler 를 작성해준다. IPointerClickHandler, IDragHandler 를 상속받고 OnDrag, OnPointerClick 인터페이스를 통해 이벤트를 생성해준다.
내부에서 클릭된 Pointer에 대한 값을 eventData로 전달해주면서 Invoke 를 발생시켜주고 있다.
# UI_Base.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public abstract class UI_Base : MonoBehaviour
{
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
public abstract void Init();
protected void Bind<T>(Type type) where T : UnityEngine.Object
{
string[] names = Enum.GetNames(type);
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
for (int i = 0; i < names.Length; i++)
{
if (typeof(T) == typeof(GameObject))
objects[i] = Util.FindChild(gameObject, names[i], true);
else
objects[i] = Util.FindChild<T>(gameObject, names[i], true);
if (objects[i] == null)
Debug.Log($"Failed to bind({names[i]})");
}
}
protected T Get<T>(int idx) where T : UnityEngine.Object
{
UnityEngine.Object[] objects = null;
if (_objects.TryGetValue(typeof(T), out objects) == false)
return null;
return objects[idx] as T;
}
protected Text GetText(int idx)
{
return Get<Text>(idx);
}
protected Button GetButton(int idx)
{
return Get<Button>(idx);
}
protected Image GetImage(int idx)
{
return Get<Image>(idx);
}
protected GameObject GetObject(int idx)
{
return Get<GameObject>(idx);
}
public static void ADDUIEvent(GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
{
UI_EventHandler evt = Util.GetOrAddComponent<UI_EventHandler>(go);
switch (type)
{
case Define.UIEvent.Click:
evt.OnClickHandler -= action;
evt.OnClickHandler += action;
break;
case Define.UIEvent.Drag:
evt.OnDragHandler -= action;
evt.OnDragHandler += action;
break;
}
evt.OnDragHandler += ((PointerEventData data) => { evt.gameObject.transform.position = data.position; });
}
}
이제 핵심적인 UI Code 라고 볼 수 있는 UI_Base.cs 이다. 추가될 UI 에 대한 스크립트는 해당 스크립트를 상속받아 사용하게 될 것이다.
주 목적은 특정 Object 와 그 자식 Object 들에 대한 지정된 형식들을 모두 binding 해서 코드 상에서 간편하게 접근할 수 있도록 하는 메서드들이 작성된다.
- Dictionary<Type, UnityEngine.Object[]> _objects : Dictionary 형식으로 Type 과 Object 를 저장한다.
- public abstract void Init(); : 상속받은 다른 곳에서 Init 해줄 수 있도록 abstract 로 선언해준다.
- protected void Bind<T>(Type type) where T : 가장 중요한 Bind 함수, 상속 받은 아이들이 사용할 수 있게 protected,
enum 을 매개변수로 전달해준다.
unity object 형식의 배열이 enum 길이 만큼 생성되며, 해당 배열은 위에서 선언된 _objects 에 타입, 배열 형식으로 저장된다.
그리고 생성된 배열 내부에 앞의 Util 에서 작성된 FindChild, FindChild<T> 메서드가 사용되어 enum 에서 선언한 모든 형식에 대한 오브젝트들이 배열에 담기게 된다.
- protected T Get<T>(int idx) where T : 위에서 _objects 에 해당 enum type 과 오브젝트 배열이 저장되었다면 이제 Get 을 통해 해당 object 를 꺼내올 수 있어야 한다. 딕셔너리에서 원하는 형식의 배열을 가져온 뒤 입력한 idx 번째 object를 꺼내준다.
이 Get 을 래핑해서 GetText, GetButton, GetImage, GetObject 등의 인터페이스도 추가로 구현하여 사용이 쉽게 만들어준다.
이제 enum 형식과 Bind, Get 을 통해 원하는 Type 의 오브젝트를 코드 상에서 손쉽게 접근할 수 있는데, 해당 UI 에 이벤트를 추가해줄수 있어야한다.
public static void ADDUIEvent : 입력된 GameObject 에 GetOrAddComponent 를 통해 UI_EventHandler 를 생성하면 해당 오브젝트에 스크립트가 연결된다. 그리고 콜백함수로 클릭, 드래그 이벤트 를 구독해주면 된다.
마지막에 OnDragHandler 이벤트가 발생한다면 입력된 포인터로 GameObject 의 position을 바꿔주는 이벤트를 작성해준다.
Util 을 통해 편리한 함수를 정리하고, UI_Base 를 통해 UI 관련 오브젝트를 자동으로 접근, 이벤트 생성에 용이하게 틀이 잡혔다.
이제는 UIManager 를 통해 직접적으로 UI 생성에 관여해야 한다.
# UIManagers.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIManager
{
int _order = 10;
Stack<UI_Popup> _popupStack = new Stack<UI_Popup>();
UI_Scene _sceneUI = null;
public GameObject Root
{
get
{
GameObject root = GameObject.Find("@UI_Root");
if (root == null)
root = new GameObject { name = "@UI_Root" };
return root;
}
}
public void SetCanvas(GameObject go, bool sort = true)
{
Canvas canvas = Util.GetOrAddComponent<Canvas>(go);
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.overrideSorting = true;
if (sort)
{
canvas.sortingOrder = _order;
_order++;
}
else
{
canvas.sortingOrder = 0 ;
}
}
public T MakeSubItem<T>(Transform parent = null, string name = null) where T : UI_Base
{
if (string.IsNullOrEmpty(name))
name = typeof(T).Name;
GameObject go = Managers.Resource.Instantiate($"UI/SubItem/{name}");
if (parent != null)
go.transform.SetParent(parent);
return Util.GetOrAddComponent<T>(go);
}
public T ShowSceneUI<T>(string name = null) where T : UI_Scene
{
if (string.IsNullOrEmpty(name))
name = typeof(T).Name;
GameObject go = Managers.Resource.Instantiate($"UI/Scene/{name}");
T sceneUI = Util.GetOrAddComponent<T>(go);
_sceneUI = sceneUI;
go.transform.SetParent(Root.transform);
return sceneUI;
}
public T ShowPopupUI<T>(string name = null) where T : UI_Popup
{
if (string.IsNullOrEmpty(name))
name = typeof(T).Name;
GameObject go = Managers.Resource.Instantiate($"UI/Popup/{name}");
T popup = Util.GetOrAddComponent<T>(go);
_popupStack.Push(popup);
go.transform.SetParent(Root.transform);
return popup;
}
public void ClosePopupUI(UI_Popup popup)
{
if (_popupStack.Count == 0)
return;
if (_popupStack.Peek() != popup)
{
Debug.Log("Close Popup Failed !");
return;
}
ClosePopupUI();
}
public void ClosePopupUI()
{
if (_popupStack.Count == 0)
return;
UI_Popup popup = _popupStack.Pop();
Managers.Resource.Destroy(popup.gameObject);
popup = null;
_order--;
}
public void CloseAllPopupUI()
{
while (_popupStack.Count > 0)
ClosePopupUI();
}
}
UI 는 열린 순서의 역순으로 닫혀야 하기 때문에 Stack 자료구조가 사용되었다.
UI 가 생성될 때 오브젝트의 묶음으로 처리하기 위해서 @UI_Root 라는 오브젝트를 생성해서 모아줄 수 있도록 한다.
- public void SetCanvas(GameObject go, bool sort = true) : 입력한 go 에 컴포넌트로 Canvas 를 생성해준다.
해당 canvas 에 order 를 적용해서 팝업끼리 sorting 될 수 있도록 해준다.
- public T MakeSubItem<T>(Transform parent = null, string name = null) : 입력한 Type 에 대한 자식 Item 을 만들어서 parent 가 지정되었다면 자식 컴포넌트로 생성해주고 입력되지 않았다면 그냥 생성된 컴포넌트를 반환해준다.
- public T ShowSceneUI<T>(string name = null) : SceneUI 를 생성해서 @UI_Root에 자식 컴포넌트로 넣어주고 반환한다.
- public T ShowPopupUI<T>(string name = null) : popupUI 를 생성해서 @UI_Root 에 넣어주고 stack에 추가한 뒤 반환한다.
- public void ClosePopupUI(UI_Popup popup) : 매개변수가 있다면 입력한 popup 이 가장 마지막에 열린지 확인 후 제거한다.
매개변수가 없다면 가장 마지막 popup 을 제거해준다.
- public void CloseAllPopupUI() : 열려있는 모든 팝업을 제거해준다.
위에서 작성된 UIMnager, UI_Base, Util 코드를 활용하여 인벤토리를 작성한다고 가정한다.
Popup UI 는 스택 구조로 sorting 이 적용되어야 하고, Scene 은 오더가 적용될 필요 없는 UI 이다.
# UI_Scene.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_Scene : UI_Base
{
public override void Init()
{
Managers.UI.SetCanvas(gameObject, false);
}
}
UI_Scene 에서는 sorting 이 필요없는 Canvas 를 생성해주는 Init 함수를 작성한다.
UI_Base 에서 abstract 로 선언했었기 때문에 오버라이드로 생성해준 것이다.
# UI_Inven.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_Inven : UI_Scene
{
enum GameObjects
{
GridPanel
}
void Start()
{
Init();
}
public override void Init()
{
base.Init();
Bind<GameObject>(typeof(GameObjects));
GameObject gridPanel = Get<GameObject>((int)GameObjects.GridPanel);
foreach (Transform child in gridPanel.transform)
Managers.Resource.Destroy(child.gameObject);
for (int i = 0; i < 8; i++)
{
GameObject item = Managers.UI.MakeSubItem<UI_Inven_Item>(gridPanel.transform).gameObject;
UI_Inven_Item invenitem = item.GetOrAddComponent<UI_Inven_Item>();
invenitem.SetInfo($"집행검{i}번");
}
}
}
앞에서 모든 것을 구현해놓았기 때문에 실제 인벤토리 구현에 대한 코드가 매우 간략해졌음을 볼 수 있다.
enum GameObjects 타입으로 GridPanel 이라는 값을 추가한다. Init 을 오버라이드 하는데, 앞에서 canvas Init 을 위한 base.Init() 을 추가해주고,
GridPanel 이 담긴 GameObjects enum 을 Bind 해준다. 바인딩 되었기 때문에 Get 을 통해 해당 이름을 가진 Object 에 Get 으로 접근이 가능해졌다.
그리고 gridPanel 에 연결된 자식이 있다면 모두 제거해주고, UI_Inven_Item 이라는 형식의 자식 컴포넌트를 만들어서 연결해주는 작업이 진행되고 있다.
'⇥ 2D Game > Unity' 카테고리의 다른 글
Unity 게임 만들기 프로젝트 - SoundManager (0) | 2024.06.10 |
---|---|
Unity 게임 만들기 프로젝트 - SceneManager (0) | 2024.06.10 |
Unity 게임 만들기 프로젝트 - Camera, Click 이동 구현 (1) | 2024.06.08 |
Unity 게임 만들기 프로젝트 - RayCasting (0) | 2024.06.07 |
Unity 게임 만들기 프로젝트 - 충돌 (Collision) (0) | 2024.06.06 |
소중한 공감 감사합니다