UI도 Unity에서는 하나의 GameObject이다. UI Component를 생성하면 Canvas
, Button
, Text
순으로 하나의 GameObject가 생성된다.
Canvas는 여러개가 될 수 있다. 팝업되는 창이 하나의 Canvas가 될 수 있으며 Canvas가 하나의 Prefab으로 저장될 수 있다.
기존의 3D Object의 Transform과 다르게 UI는 Rect Transform을 가진다. Rect Transform은 기본적인 Transform보다 더 많은 정보를 가지고 있다.
앵커
는 부모 Object와의 거리 비율을 지키고, 자신의 Object와의 거리를 보장한다. 헷갈리지만 앵커
를 여러 프리셋을 적용하고 Panel을 움직여보자.
UI는 원근법을 적용받지 않아 Z값이 커져도 가시거리 안에 있으면 같은 크기로 출력된다.
Unity에서 버튼을 눌렀을 때 동작하는 Event는 툴에서 아래 사진과 같이 설정할 수 있다.
Event를 전송할 Object와 실행할 함수를 설정하면 버튼을 눌렀을 때 이벤트가 발생해 함수가 설정한 Object의 함수가 실행된다.
UI를 클릭했는데 UI처리와 동시에 Scene의 마우스 입력으로 처리될 수도 있다.
예를들어, 총게임에서 인벤토리의 아이템을 옮기기 위해 아이템을 클릭한 순간 총쏘기 입력이 동시에 입력되어 총이 나가는 상황이 발생할 수 있다. 총쏘기 입력을 처리하는 곳에서 UI를 클릭하고 있을 때의 예외를 처리해야 한다.
EventSystem.current.isPointerOverGameObject()
는 마우스 입력이 UI Object위에 있는지 판단하는 함수이다.
앞서 말한바와 같이 Canvas는 여러개가 될 수 있다. 그렇다면 Canvas간의 우선순위는 어떻게 될까?
가장 중요한 것은 Canvas간의 우선순위
를 정해야 하는데 이것은 Canvas Component의 Sort Order
를 조절하면 된다.
또 다른 중요한 점이 남아있다. 새로운 UI를 만들때마다 Event를 보낼 Object를 설정하고 어떤 함수를 실행시키고 또 다른 UI를 팝업시키는 일을 하나씩 지정하는 것은 프로젝트 단위에서 시간을 오래 잡아먹는 요인 중 하나이다.
모든 상황에 대한 UI를 만들기 보다, UI를 만드는 과정을 일반화시키고 자동화시킴으로 UI를 생성할때 다른 세부사항들을 설정하도록 할 필요가 있다.
가장 중요한 사실, Tool에서 할 수 있는 일은 Code에서도 가능하다
어떤 창은 하나의 Canvas로 나타낼 수 있고 창 하나에 수십개의 아이콘, 버튼, 이미지가 포함될 수 있다. 위에서 말한바와 같이 수십개의 요소들을 하나씩 설정하고 동작하는것을 Canvas마다 설정해준다면 굉장히 많은 코드구현과 이후에 변경사항이 있을 때 많은 부분을 바꿔야 할 것이다.
UI를 생성하는 과정을 일반화시킨다면 공통된 코드구현부분의 양을 줄이고 쉽게 변경할 수 있다.
Click
이나 Drag
같은 이벤트가 발생하였을 때 행동을 설정UI에는 버튼
, 문자
, 이미지
같은 컴포넌트가 있을 수 있다. 이를 위해 하나의 UI에는 어떤 컴포넌트가 포함되어 있는지 enum으로 정의한다.
enum Buttons
{
PointButton,
}
enum Texts
{
PointText,
ScoreText
}
c#에서 함수의 인자로 Type이라는 클래스를 넘길 수 있다. Bind라는 함수를 정의한다 했을때 Generic과 Type을 이용해 다음과 같은 함수를 정의할 수 있다.
void Bind<T>(Type type) where T : UnityEngine.Object {
//내용
}
T
에는 컴포넌트의 이름(Button
, Text
등)이 해당되고 Type에는 enum Type이 해당된다.
Enum.GetNames(type)을 사용하면 해당 enum에 있는 모든 Name을 가져올 수 있다.
UI는 각 컴포넌트의 type에 따라 컴포넌트들을 저장해야 한다. 예를들어, Button
Type에는 UI에 저장된 모든 Button
컴포넌트들이 Mapping되어야 한다.
이를위해 C#에서 지원하는 Dictionary를 사용한다 <key, value>를 <type, component[]>로 사용한다.
Dictionary<Type, UnityEngine.Object[]> _objects = new
Dictionary<Type, UnityEngine.Object[]>();
저장되는 자료구조는 자유롭지만 type과 component[]를 매핑하는데 가장 유리한 자료구조는
Dictonary
,HashSet
이라고 생각한다.
위 단계에서 정의한 _objects
에 실제 컴포넌트들을 할당해주어야 한다. 이를 위해 transform
에 내장되어 있는 변수, 함수들을 이용한다.
1) gameObject 산하에 있는 Component들의 수를 알아내기 위해 transform.childCount
를 사용한다.
2) 각 idx에 대해서 transform.GetChild(idx)
로 Component의 Transform을 가져온다.
3) 가져온 Transform에서 Type에 해당하는 Component를 가져온다.
위 과정을 구현한 Bind
와 FindChild
의 코드는 다음과 같다.
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]})");
}
}
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;
}
}
Bind를 구현할 때 사용한 자료구조는 다음과 같은 구조를 가지는 Dictionary이다.
Dictionary<Type, UnityEngine.Object[]> _objects = new
Dictionary<Type, UnityEngine.Object[]>();
key는 Type
, value는 UnityEngine.Object[]
으로 Type
을 입력하면 UnityEngine.Object[]
가 매핑되는 구조이다.
하나의 컴포넌트를 제공하기 위해 배열에 접근한 index가 추가로 필요할 것이고 이를 위한 getter를 다음과 같이 정의할 수 있다.
T Get<T>(int idx) where T : UnityEngine.Object
UI에는 Button
, Text
, Image
외에 내부에 GameObject를 두거나 외부의 GameObject를 알아야 할 때가 있다. 하지만, 위의 내용으로 Bind<GameObject>
를 하면 오류가 발생할 것이다. transform.GetComponent<T>()
에서 GameObject라는 Component를 가지지 않기 때문인데 GameObject를 위한 FindChild
를 구현함으로 해결할 수 있다.
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;
}
위 과정을 구현한 Get
의 코드는 다음과 같다.
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;
}
_objects의 자료구조가 변경된다면 Get()도 바뀌어야 한다.
UI의 Event의 처리는 EventSystem
에서 다룰수 있는데 클릭
, 드래그
, 드롭
등 많은 이벤트를 구현할 수 있으며 어떤 이벤트와 그 이벤트에 해당하는 인터페이스는 공식문서에서 확인 할 수 있다.
어떤 Event가 발생했을 때 해당 Event에 대한 Action을 가지고 있는 GameObject에게 알리는 클래스가 필요하다. 이를위해 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);
}
}
UI에 속해있는 GameObject에 대해서 어떤 GameObject가 어떤 Event에 어떤 행동을 해야하는지를 설정하는 방법이 필요하다. 이를 위해 다음과 같은 AddUIEnvent()를 정의할 수 있다.
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;
}
}
C#에서 기존의 클래스, 인터페이스, enum에 새로운 메소들르 추가할수 있게 해주는 문법으로 기존에 제공되는 Type을 수정하지 않고 새로운 동작을 정의할 수 있다.
this
를 넣는다.아래의 코드는 Button
과 Image
에 Click
, Drag
이벤트를 추가하는 과정에서 Extension Mehtod
의 사용유무를 보여주는 코드이다.
GetButton((int)Buttons.PointButton).gameObject.AddUIEvent(OnButtonClicked);
GameObject go = GetImage((int)Images.ItemIcon).gameObject;
AddUIEvent(go, (PointerEventData data) => { go.transform.position = data.position; }, Define.UIEvent.Drag);
여태까지의 설명에서 GameObject와 Component를 섞어서 표현했는데 정리하자면 다음과 같다. 아래 사진을 보면 Text
, Iamge
, Button
을 나타낸 것이다.
Button
, Text
, Image
는 GameObject로 나타낼 수 있다.Text
GameObject가 있다.Point Button
이라는 Button GameObject에는 Button Component
가 있다.Button
, Text
, Image
외의 GameObject에는 'Type 이름'에 해당하는 Component가 존재하지 않는다. GameObject에 대해 FindChild을 다시 정의한것을 떠올리면 된다.