새소식

⇥ 2D Game/Unity

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 이라는 형식의 자식 컴포넌트를 만들어서 연결해주는 작업이 진행되고 있다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.