[Unity] UI

이정석·2023년 6월 27일
0

Unity

목록 보기
7/22

UI

UI도 Unity에서는 하나의 GameObject이다. UI Component를 생성하면 Canvas, Button, Text순으로 하나의 GameObject가 생성된다.

Canvas는 여러개가 될 수 있다. 팝업되는 창이 하나의 Canvas가 될 수 있으며 Canvas가 하나의 Prefab으로 저장될 수 있다.

1. Rect Transform

기존의 3D Object의 Transform과 다르게 UI는 Rect Transform을 가진다. Rect Transform은 기본적인 Transform보다 더 많은 정보를 가지고 있다.

  • 너비(Width)
  • 높이(Height)
  • 피벗(Pivot): 회전하기 위한 중심점
  • 앵커(Anchor): 부모 Object에 대한 위치를 의미하는데 화면해상도에 따라 GameObject의 모양이 바뀌는 것을 생각하면 된다.

앵커는 부모 Object와의 거리 비율을 지키고, 자신의 Object와의 거리를 보장한다. 헷갈리지만 앵커를 여러 프리셋을 적용하고 Panel을 움직여보자.

UI는 원근법을 적용받지 않아 Z값이 커져도 가시거리 안에 있으면 같은 크기로 출력된다.

2. Button Event

Unity에서 버튼을 눌렀을 때 동작하는 Event는 툴에서 아래 사진과 같이 설정할 수 있다.

  • Click이벤트를 알릴 Object가 필요하다.
  • Object의 어떤 함수를 실행시킬지 설정해야 한다.

Event를 전송할 Object와 실행할 함수를 설정하면 버튼을 눌렀을 때 이벤트가 발생해 함수가 설정한 Object의 함수가 실행된다.

UI를 클릭했는데 UI처리와 동시에 Scene의 마우스 입력으로 처리될 수도 있다.

예를들어, 총게임에서 인벤토리의 아이템을 옮기기 위해 아이템을 클릭한 순간 총쏘기 입력이 동시에 입력되어 총이 나가는 상황이 발생할 수 있다. 총쏘기 입력을 처리하는 곳에서 UI를 클릭하고 있을 때의 예외를 처리해야 한다.

EventSystem.current.isPointerOverGameObject()는 마우스 입력이 UI Object위에 있는지 판단하는 함수이다.

3. Canvas

앞서 말한바와 같이 Canvas는 여러개가 될 수 있다. 그렇다면 Canvas간의 우선순위는 어떻게 될까?

  • 장비창과 인벤토리를 동시에 열었고 겹치는 부분을 눌렀을 때 어떤 UI가 선택되어야 할까?
  • 뒤에 있는 UI를 가장 앞으로 바꿀수는 없을까?
  • Canvas내에서 또 다른 Canvas를 불렀을 때 상위 Canvas를 누를 수 없도록 할 수는 없을까?

가장 중요한 것은 Canvas간의 우선순위를 정해야 하는데 이것은 Canvas Component의 Sort Order를 조절하면 된다.

또 다른 중요한 점이 남아있다. 새로운 UI를 만들때마다 Event를 보낼 Object를 설정하고 어떤 함수를 실행시키고 또 다른 UI를 팝업시키는 일을 하나씩 지정하는 것은 프로젝트 단위에서 시간을 오래 잡아먹는 요인 중 하나이다.

  • 한 UI에 버튼이 수십개 필요하다면?
  • 한 UI에 Icon 이미지, 캐릭터 이미지 각기 다른 이미지요소가 필요하다면?

모든 상황에 대한 UI를 만들기 보다, UI를 만드는 과정을 일반화시키고 자동화시킴으로 UI를 생성할때 다른 세부사항들을 설정하도록 할 필요가 있다.

가장 중요한 사실, Tool에서 할 수 있는 일은 Code에서도 가능하다


UI 자동화

어떤 창은 하나의 Canvas로 나타낼 수 있고 창 하나에 수십개의 아이콘, 버튼, 이미지가 포함될 수 있다. 위에서 말한바와 같이 수십개의 요소들을 하나씩 설정하고 동작하는것을 Canvas마다 설정해준다면 굉장히 많은 코드구현과 이후에 변경사항이 있을 때 많은 부분을 바꿔야 할 것이다.

UI를 생성하는 과정을 일반화시킨다면 공통된 코드구현부분의 양을 줄이고 쉽게 변경할 수 있다.

  • Bind: Canvas 산하의 Component들을 정의하고 할당하는 단계
  • Get: Canvas의 Component에 접근할 수 있도록 하는 getter
  • Event: Click이나 Drag같은 이벤트가 발생하였을 때 행동을 설정

1. Bind

  • Component들의 종류를 enum으로 정의하자

UI에는 버튼, 문자, 이미지 같은 컴포넌트가 있을 수 있다. 이를 위해 하나의 UI에는 어떤 컴포넌트가 포함되어 있는지 enum으로 정의한다.

    enum Buttons
    {
        PointButton,
    }

    enum Texts
    {
        PointText,
        ScoreText
    }
  • 함수에 enum은 어떻게 넘기는가?

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을 가져올 수 있다.

  • type별 Components 저장방법

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이라고 생각한다.

  • Canvas 하위에 있는 UI Component를 찾고 지정하는 방법

위 단계에서 정의한 _objects에 실제 컴포넌트들을 할당해주어야 한다. 이를 위해 transform에 내장되어 있는 변수, 함수들을 이용한다.

1) gameObject 산하에 있는 Component들의 수를 알아내기 위해 transform.childCount를 사용한다.
2) 각 idx에 대해서 transform.GetChild(idx)로 Component의 Transform을 가져온다.
3) 가져온 Transform에서 Type에 해당하는 Component를 가져온다.

위 과정을 구현한 BindFindChild의 코드는 다음과 같다.

  • Bind함수
    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]})");
        }
    }
  • FindChild
    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;
    }
}

2. Get

  • _object의 <key, value>구조

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
  • gameObject를 찾는 방법

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의 코드는 다음과 같다.

  • 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()도 바뀌어야 한다.

3. Event

UI의 Event의 처리는 EventSystem에서 다룰수 있는데 클릭, 드래그, 드롭 등 많은 이벤트를 구현할 수 있으며 어떤 이벤트와 그 이벤트에 해당하는 인터페이스는 공식문서에서 확인 할 수 있다.

  • EventHandler정의

어떤 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);
    }
}
  • EventHandler등록

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;
        }
    }
  • Extension Method

C#에서 기존의 클래스, 인터페이스, enum에 새로운 메소들르 추가할수 있게 해주는 문법으로 기존에 제공되는 Type을 수정하지 않고 새로운 동작을 정의할 수 있다.

  1. 확장 메소드를 포함할 정적 클래스를 정의한다.
  2. 클래스 내에 정적 메소드를 정의하고 첫번째 인수 앞에 this를 넣는다.

아래의 코드는 ButtonImageClick, 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

여태까지의 설명에서 GameObject와 Component를 섞어서 표현했는데 정리하자면 다음과 같다. 아래 사진을 보면 Text, Iamge, Button을 나타낸 것이다.

  • Button, Text, Image는 GameObject로 나타낼 수 있다.
  • 예를들어, 위 사진에는 ScoreText라는 Text GameObject가 있다.
  • 각 GameObject는 'Type 이름'에 해당하는 Component가 있다. 예를들어, Point Button이라는 Button GameObject에는 Button Component가 있다.
  • Button, Text, Image외의 GameObject에는 'Type 이름'에 해당하는 Component가 존재하지 않는다. GameObject에 대해 FindChild을 다시 정의한것을 떠올리면 된다.
profile
게임 개발자가 되고 싶은 한 소?년

0개의 댓글