Chapter 7-4. UI : UI 자동화

Date:     Updated:

카테고리:

태그:

인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 7. UI


🚀 UI 자동화

✈ UI 자동화를 위한 클래스 구조

UI 구조

  • 📜UI_Base 👉 모든 UI 의 조상, 모든 UI 캔버스들이 가지고 있는 공통적인 부분들.
    • 📜UI_Scene 👉 씬 UI 의 조상, 팝업처럼 차례대로 뜨는게 아니라 원래 자리잡고 있는 고정적인 UI 캔버스들의 공통적인 부분들.
    • 📜UI_PopUp 👉 팝업 UI 의 조상, 팝업 UI 캔버스들의 공통적인 부분들.
      • 📜UI_Button

UI Canvas 의 종류를 고정적인 형태의 1️⃣Scene, 스택으로 관리되는 팝업 형태의 2️⃣PopUp 로 구분할 것.

바인딩, 이벤트 관련 함수

  • 📜Util
  • 📜UI_EventHandler
  • 📜Define


✈ Bind : 이름으로 UI 오브젝트 찾아와 자동 할당

📜UI_Button

UI_Button이라는 이름의 Canvas 에 붙인다.

  • 캔버스 프리팹이다.
    • 팝업 UI 형태. 📜UI_PopUp 상속 받음
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class UI_Button : UI_Popup
{
    enum Buttons
    {
        PointButton
    }

    enum Texts
    {
        PointText,
        ScoreText,
    }

    enum GameObjects
    {
        TestObject,
    }

    enum Images
    {
        ItemIcon,
    }
		    Bind<Button>(typeof(Buttons));
		    Bind<Text>(typeof(Texts));
		    Bind<GameObject>(typeof(GameObjects));
		    Bind<Image>(typeof(Images));

image

  • UI_Button 캔버스 프리팹은 아래와 같은 이름의 오브젝트들로 이루어져있다. (사진참고)
    • 1 개의 UI Button (PointButton), 2 개의 UI Text (PointText, ScoreText), 1 개의 GameObject (TestObject), 1 개의 UI Image (ItemIcon)
    • 오브젝트의 이름들을 각각 같은 종류의 enum 으로 묶어 관리한다.
      • 평소같으면 아래와 같이 public 혹은 [SerializeField]를 통해 유니티 툴에서 직접 해당 오브젝트들을 바인딩 해주어야 했겠지만! 툴에서 바인딩을 하지 않고 코드를 통해 바인딩 되도록 UI 자동화를 할 것이다.
        // 평소같으면 아래와 같이 일일이 선언해주고 유니티 툴에서 연결해주어야 했을 것. 작은 게임이면 괜찮지만 프로그램 규모가 커지면 비효율적일 것이다.
        [SerializeField] private Button pointButton; // "PointButton" UI 오브젝트 연결
        [SerializeField] private Text pointText; // "PointText" UI 오브젝트 연결
        [SerializeField] private Text scoreText; // "ScoreText" UI 오브젝트 연결
        [SerializeField] private GameObject go; // "TestObject" 오브젝트 연결
        [SerializeField] private Image itemIcon; // "ItemIcon" UI 오브젝트 연결
        
      • 따라서 여러 타입의 Bind 함수를 만들어서 enum 별로, 즉 오브젝트 종류별로 실제 오브젝트들을 로드하여 Dictionary (조상인 📜UI_Base 로부터 상속 받음) 자료구조에 담을 수 있도록 바인딩 할 것이다.
        • 유니티 툴에서 연결해줄 필요 없이 코드로 로딩해주는 것!
        • C# 은 Reflection 기능이 있으므로 이를 통해 enum 에 들어있는 것들의 이름도 string 으로 받아올 수 있는 기능이 있다. C++에는 없는 기능..
          • typeof(Buttons) 👉 Type에는 모든 정보가 들어있다. Buttons Enum 에 관한 모든 정보가 Type 객체로 리턴된다.
            • typeof는 클래스 자체의 Type을 가져온다. GetType() 함수는 인스턴스 객체의 Type을 가져온다.
          • Reflection 포스트 참고


📜UI_Base

  • 📜UI_Base 👉 모든 UI 의 조상, 모든 UI들이 가지고 있는 공통적인 부분들.
    • Bind UI 오브젝트 이름으로 찾아 바인딩해주기
    • Get UI 오브젝트 가져오기
    • BindEvent UI 오브젝트에 이벤트 등록하기
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public abstract class UI_Base : MonoBehaviour
{
	protected Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();

	public abstract void Init();
  • _objects 👉 KeyType이며, Value는 오브젝트들이 담긴 배열인 Dictionary
    • 유니티 씬 상에 존재하는 오브젝트들을 로드하여 이 곳에 바인딩하여 보관
    • 📜UI_Button 에서 UI_Button 캔버스의 구성 오브젝트들의 이름을 Enum 별로 분류해 정의했었다. 이 Enum Type을 Key 로 하여 그 Enum 에 담긴 이름들에 해당하는 실제 오브젝트들을 배열로 담아 Value 로 할 것이다.
      • Buttons 가 Key 라면 버튼 UI 오브젝트들이 담긴 배열이 Value.
    • 📜UI_Button 는 📜UI_Base 를 상속받는다. 즉, 캔버스 UI 프리팹들에 붙을 스크립트는 모두 이 📜UI_Base 를 상속받게 할 것이므로 캔버스 프리팹들 각각 본인들의 _objects에서 자신들을 구성하는 오브젝트들을 바인딩하여 담게 된다!
    • UnityEngine.Object는 모~든 오브젝트와 컴포넌트들의 조상이라 UnityEngine.Object로 모든 오브젝트, 컴포넌트들을 업캐스팅하는게 가능하다.
	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); // Dictionary 에 추가

    // T 에 속하는 오브젝트들을 Dictionary의 Value인 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]})");
		}
	}
  • _objects는 Dictionary 고, objects_objects Dictionary에 Value로 담기 위한 배열이다.
  • Bind 함수
    • 📜UI_Base 를 상속 받는 모든 캔버스 UI 들에서 사용할 수 있어야 하므로 protected
    • 프로토타입
      • T에는 컴포넌트 혹은 오브젝트가 들어간다. Button, GameObject, Image 같은!
        • where T : UnityEngine.Object : TUnityEngine.Object를 상속받는 것이어야 한다.
          • 컴포넌트도 Object 를 상속 받는다. Object - Component - Behaviour - MonoBehaviour
      • type에는 오브젝트들의 이름을 담아 종류별로 구분한 Enum 클래스들의 Reflection 정보가 들어간다.
        • Bind<Button>(typeof(Buttons)); Buttons enum 클래스의 정보를 넘긴다.
          • Buttons enum 클래스에 Button 오브젝트들의 이름이 정의되어있으니 이 이름에 매치되는 오브젝트들을 씬상에서 찾아 Value인 배열의 원소 하나하나에 로드할 것이다. T인 Button 을 Key 로 하여 오브젝트들을 _objects Dictionary 의 Value로서 저장
    • Enum.GetNames 함수
      • C# 의 Enum 은 String처럼 그 자체의 함수들을 지원한다. Enum 에서 GetNames 함수를 호출하면 파라미터로 넘긴 Enum 클래스의 요소들의 이름을 string 배열로 리턴한다.
      • 각각의 Enum 에는 Enum 이름에 걸맞는 오브젝트의 이름들이 있으니 이를 Enum.GetNames 함수로 이 이름들을 담은 string 배열names 배열에 담는다.
    • 로드
      • T가 주로 컴포넌트이겠지만 (Button, Image 등등) 컴포넌트가 없는 빈 오브젝트 GameObject일 수도 있다. GetComponent로 찾아서 로드할 것이기 때문에 케이스를 나눈다. Util.FindChildUtil.FindChild<T>
      • Util.FindChild 함수의 역할
        • 파라미터로 넘긴 자기 자신 gameObject(캔버스)의 모~~~든 자식 오브젝트들에 접근하여 names[i] 이름과 일치하는 오브젝트를 로드해온다.
        • names[i] 이름과 일치하는 오브젝트를 씬에서 찾아서 _objects Dictionary의 Value인 배열의 원소 하나하나 objects[i]에 로드한다.


📜Util

    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;
    }
  • FindChild 가 하는 일
    • go 오브젝트의 모든 자식들 중 T 컴포넌트를 가지며, name과 이름이 일치하는 오브젝트를 찾아 리턴한다.
      • 만약 name이 null이면 (즉, 호출시 안넘겨줬다면) 그냥 바로 그 T컴포넌트에 해당 되는 것만 찾으면 된다. *if (string.IsNullOrEmpty(name)   component.name == name)*
    • recursive를 true로 받았다면 go의 손자들 증손자들까지 recursive하게 모~~~~든 자식들 중에서 T컴포넌트를 가진 자식 찾음
      • GetComponentsInChildren 함수를 통해 T 컴포넌트를 가진 모~~~~~든! 자식들 검사
    • recursive를 false로 받았다면 go의 직속 자식들 중에서만 T컴포넌트를 가진 자식 찾음
      • GetChild(int) 함수를 통해 직속 자식을 Transform 타입으로 리턴받을 수 있음. 몇 번째 자식인지 인덱스를 파라미터로 넘김.
public class Util
{
    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;
    }
  • 컴포넌트 없는 GameObject 오브젝트만 (go) 넘겨 받을 예정인 public static GameObject FindChild
    • 마찬가지로 static 이라 외부에서 그냥 Util.FindChild로 호출할 수 있다.
    • where T : UnityEngine.Object Generic이 아니니까 제약이 필요 없다.
      • 컴포넌트 없는 빈 오브젝트만 이 함수에 넘길 것이라서 Generic으로 구현할 필요가 X
    • 빈 오브젝트 포함 모든 오브젝트는 Transform 컴포넌트를 가지므로 이 Transform 컴포넌트를 가진 오브젝트를 찾아오도록 FindChild<Transform>를 호출한다.


✈ Get : 오브젝트 가져오기

📜UI_Base

	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 GameObject GetObject(int idx) { return Get<GameObject>(idx); } // 오브젝트로서 가져오기
	protected Text GetText(int idx) { return Get<Text>(idx); } // Text로서 가져오기
	protected Button GetButton(int idx) { return Get<Button>(idx); } // Button로서 가져오기
	protected Image GetImage(int idx) { return Get<Image>(idx); } // Image로서 가져오기

📜UI_Button

GameObject go = GetImage((int)Images.ItemIcon).gameObject;
  • Get이 하는 일
    • T 컴포넌트를 가지고 있으며 (혹은 오브젝트) 파라미터로 넘긴 int (예를들어 “Images.ItemIcon”을 int 로 형변환하면 enum 답게 그 정수가 리턴된다.) 인덱스에 해당하는 오브젝트를 T 타입으로 리턴함
      • _objects Dictionary에서 T Key에 대응하는 배열에서 찾아보면 됨!
        • _objects Dictionary의 Value인 배열은 Enum 안에 정의된 순서대로 들어가 있으므로 이렇게 (int)Images.ItemIcon 이런식으로 찾아도 인덱스에 대응될 것이다.
        • objects[idx] as T
  • _objects.TryGetValue(typeof(T), out objects)
    • _objects Dictionary에 typeof(T) Key 가 존재하면 True 리턴 + objects 배열에 그 typeof(T) Key 의 Value 를 저장.


✈ Event 연동

📜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;
	}
  • GetOrAddComponent
    • go 오브젝트의 T 컴포넌트를 GetComponent 가져온다. T 컴포넌트가 없다면 AddComponent 붙여준 후 가져온다.
    • T에는 컴포넌트만 올 수 있다. 오브젝트는 올 수 없다. where T : UnityEngine.Component
      • Object - Component - Behaviour - MonoBehaviour


📜UI_EventHandler

public class UI_EventHandler : MonoBehaviour, IPointerClickHandler, IDragHandler
{
    public Action<PointerEventData> OnClickHandler = null;
    public Action<PointerEventData> OnDragHandler = null;

	public void OnPointerClick(PointerEventData eventData) // 클릭 이벤트 오버라이딩
	{
		if (OnClickHandler != null)
			  OnClickHandler.Invoke(eventData); // 클릭와 관련된 액션 실행
	}

	public void OnDrag(PointerEventData eventData) // 드래그 이벤트 오버라이딩
    {
		if (OnDragHandler != null)
        OnDragHandler.Invoke(eventData); // 드래그와 관련된 액션 실행
	}
}

IPointerClickHandler, IDragHandler 같은 인터페이스를 상속 받아 오버라이딩 하면, 해당 이벤트가 실행될 때 자동으로 이벤트 함수가 실행된다.

  • 이벤트 함수
    • 콜백 함수로, 유니티 상에서 해당 이벤트 발생시 메세지를 쫙 뿌린다.
    • PointerEventData eventData에 온갖 해당 이벤트와 관련 정보가 자동으로 담긴다. (마우스라면 마우스의 위치 좌표라던가..)
  • IPointerClickHandler
    • 마우스 클릭 이벤트가 발생하면 자동으로 실행된다.
    • OnClickHandler 액션에 등록된 함수들이 모두 실행되도록 한다.
      • Action은 미리 C# 내에서 선언이 되어 있는 델리케이트로, 리턴 타입이 없는 함수만을 등록할 수 있는 제네릭 델리게이트이다.
      • PointerEventData 타입의 매개변수를 하나 받는 함수들만 액션에 등록할 수 있다.
  • OnDrag
    • 마우스 클릭 이벤트가 발생하면 자동으로 실행된다.
    • OnDragHandler 액션에 등록된 함수들이 모두 실행되도록 한다.

이 스크립트가 붙는 오브젝트들은 해당 이벤트를 받을 수 있다. 따라서 이 이벤트를 필요한 오브젝트에 붙여주는 작업 또한 밑에서 할 것이다. 유니티 툴에서 하지 않고 코드로 한다!


📜Define

public class Define
{
    public enum UIEvent
    {
        Click,
        Drag,
    }

    public enum MouseEvent
    {
        Press,
        Click,
    }

    public enum CameraMode
    {
        QuarterView,
    }
}

이벤트도 종류별로 Enum 으로 묶어 📜Define 에 정리하였다.


📜UI_Base

	public static void BindEvent(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;
		}
	}
  • BindEvent 가 하는 일
    • go 오브젝트에 📜UI_EventHandler를 붙여 go 오브젝트가 이벤트 콜백을 받을 수 있도록 한다.
    • 📜UI_EventHandler 에 정의되어 있는 이벤트들이 발생하면 action 액션에 등록된 것들이 실행되도록 한다. 👉 📜UI_EventHandler의 액션 멤버 OnClickHandler, OnDragHandler
      • 람다 함수나 함수 이름, 즉 함수 포인터가 파라미터로 넘겨지고 이를 매개변수 action에서 받는다.
    • 📜Define에 정의된 이벤트 종류별 enum 도 같이 넘겨서 어떤 액션에 등록할 것인지를 받는다.
      • Define.UIEvent.Click 클릭 이벤트라면 OnClickHandleraction이 등록된다. 이제 go에 마우스 클릭 이벤트가 발생하면 OnClickHandler에 등록된 액션이 실행된다.
  1. 액션에 액션을 더하는 것도 가능하다.
  2. 만약 이벤트에 해당 함수가 없는데도 빼려하는건 에러 안난다. 그냥 무시될 뿐!


📜Extension (C# 문법 중 하나인 확장메서드)

특수한 종류의 static 메서드인데, 마치 다른 클래스의 메서드인 것 처럼 호출해 사용할 수 있다.

  • 확장 메서드는 static 클래스 안에 static 메서드로 정의한다.
  • 확장 메서드의 첫 번째 매개 변수가 바로 그 다른 클래스의 메서드인 것처럼 호출할 수 있는 그 호출의 주체로 정의한다.
    • 첫 번째 매개변수 앞에 this를 써준다.
public static class Extension
{
  // 확장 메서드
	public static T GetOrAddComponent<T>(this GameObject go) where T : UnityEngine.Component 
	{
		return Util.GetOrAddComponent<T>(go);
	}

  // 확장메서드
	public static void BindEvent(this GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click) 
	{
		UI_Base.BindEvent(go, action, type);
	}
}
  • Extension 클래스가 "staic" class Extension인 것과 함수들이 static인 것에 주목! 👉 확장 메서드
    • 클래스는 Monobehavior 상속 X
  • GetOrAddComponent
    • 매개 변수가 없는 함수다!
    • GameObject 파라미터에서 호출할 수 있게 되었다. 마치 GameObject의 메서드인 것처럼 사용할 수 있게 됨.
      • this GameObject go
  • GetOrAddComponent
    • 매개 변수가 action, Define 2 개인 함수다!
    • GameObject 파라미터에서 호출할 수 있게 되었다. 마치 GameObject의 메서드인 것처럼 사용할 수 있게 됨.
      • this GameObject go
GetButton((int)Buttons.PointButton).gameObject.BindEvent(OnButtonClicked);

바로 GetButton 함수로 부터 리턴받은 버튼 오브젝트에서 바~~~로 BindEvent(OnButtonClicked) 이렇게 함수를 호출할 수 있게 되었다. 마치 GameObject 에 원래 있던 메서드를 호출하는 것처럼 호출할 수 있게 된 것이다.


📜UI_Button

    private void Start()
    {
        Init();
    }

    public override void Init()
    {
        base.Init(); // 📜UI_Button 의 부모인 📜UI_PopUp 의 Init() 호출

		    Bind<Button>(typeof(Buttons)); // 버튼 오브젝트들 가져와 dictionary인 _objects에 바인딩. 
		    Bind<Text>(typeof(Texts));  // 텍스트 오브젝트들 가져와 dictionary인 _objects에 바인딩. 
		    Bind<GameObject>(typeof(GameObjects));  // 빈 오브젝트들 가져와 dictionary인 _objects에 바인딩. 
		    Bind<Image>(typeof(Images));  // 이미지 오브젝트들 가져와 dictionary인 _objects에 바인딩. 

        // (확장 메서드) 버튼(go)에 📜UI_EventHandler를 붙이고 액션에 OnButtonClicked 함수를 OnClickHandler (디폴트)등록한다.
		    GetButton((int)Buttons.PointButton).gameObject.BindEvent(OnButtonClicked); 

        // 이미지(go)에 📜UI_EventHandler를 붙이고 파라미터로 넘긴 람다 함수를 OnDragHandler 액션에 등록한다.
		    GameObject go = GetImage((int)Images.ItemIcon).gameObject;
		    BindEvent(go, (PointerEventData data) => { go.transform.position = data.position; }, Define.UIEvent.Drag);
	  }

    int _score = 0;
 
    public void OnButtonClicked(PointerEventData data)
    {
        _score++;
        GetText((int)Texts.ScoreText).text = $"점수 : {_score}";
    }

  • 주석들 참고
  • Recttransform도 Trasnform을 상속받는다.
    • 그래서 UI 오브젝트들도 go.transform을 통해 Recttransform를 가져올 수 있다.
  • OnButtonClicked
    • PointerEventData data 를 쓰진 않을거라도, 매개 변수로 설정해 주어야 한다. 왜냐면 OnClickHandler 액션에 등록할건데 이 액션은 PointerEventData data 매개 변수를 갖고 있는 함수만 등록할 수 있기 떄문.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 2 카테고리 내 다른 글 보러가기

댓글 남기기