Chapter 7-4. UI : UI 자동화
카테고리: Unity Lesson 2
태그: Unity Game Engine
인프런에 있는 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));
UI_Button
캔버스 프리팹은 아래와 같은 이름의 오브젝트들로 이루어져있다. (사진참고)- 1 개의 UI Button (
PointButton
), 2 개의 UI Text (PointText
,ScoreText
), 1 개의 GameObject (TestObject
), 1 개의 UI Image (ItemIcon
)Blocker
의 정체는 다음 포스트에!!
- 오브젝트의 이름들을 각각 같은 종류의 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 포스트 참고
- 평소같으면 아래와 같이 public 혹은 [SerializeField]를 통해 유니티 툴에서 직접 해당 오브젝트들을 바인딩 해주어야 했겠지만! 툴에서 바인딩을 하지 않고 코드를 통해 바인딩 되도록 UI 자동화를 할 것이다.
- 1 개의 UI Button (
📜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
👉 Key는Type
이며, 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
:T
는UnityEngine.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로서 저장
- Bind<Button>(typeof(Buttons));
Enum.GetNames
함수- C# 의 Enum 은 String처럼 그 자체의 함수들을 지원한다. Enum 에서 GetNames 함수를 호출하면 파라미터로 넘긴 Enum 클래스의 요소들의 이름을 string 배열로 리턴한다.
- 각각의 Enum 에는 Enum 이름에 걸맞는 오브젝트의 이름들이 있으니 이를
Enum.GetNames
함수로 이 이름들을 담은 string 배열을names
배열에 담는다.
- 로드
T
가 주로 컴포넌트이겠지만 (Button, Image 등등) 컴포넌트가 없는 빈 오브젝트GameObject
일 수도 있다. GetComponent로 찾아서 로드할 것이기 때문에 케이스를 나눈다. Util.FindChild 와 Util.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
컴포넌트를 가진 모~~~~~든! 자식들 검사
- GetComponentsInChildren 함수를 통해
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>를 호출한다.
- 마찬가지로 static 이라 외부에서 그냥
✈ 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
클릭 이벤트라면OnClickHandler
에action
이 등록된다. 이제go
에 마우스 클릭 이벤트가 발생하면OnClickHandler
에 등록된 액션이 실행된다.
- 액션에 액션을 더하는 것도 가능하다.
- 만약 이벤트에 해당 함수가 없는데도 빼려하는건 에러 안난다. 그냥 무시될 뿐!
📜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를 가져올 수 있다.
- 그래서 UI 오브젝트들도
- OnButtonClicked
- PointerEventData data 를 쓰진 않을거라도, 매개 변수로 설정해 주어야 한다. 왜냐면
OnClickHandler
액션에 등록할건데 이 액션은 PointerEventData data 매개 변수를 갖고 있는 함수만 등록할 수 있기 떄문.
- PointerEventData data 를 쓰진 않을거라도, 매개 변수로 설정해 주어야 한다. 왜냐면
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기