내일배움캠프 Unity 34일차 TIL - 개인과제 리팩토링

Wooooo·2023년 12월 14일
0

내일배움캠프Unity

목록 보기
36/94

오늘의 키워드

오늘은 어제 제출한 개인과제의 UI_BaseUIEventHandler 클래스를 리팩토링하기로 했다.
이번 주에 있었던 클린코드 특강에서 switch문의 사용도 자제하는게 좋다고 하셨는데, 마침 UI_Base에서 switch문으로 40줄이나 써먹고 있었기 때문에 조금 더 객체지향적으로 바꿔보기로 했다.


Switch문의 사용을 피하자

    public static void BindEvent(GameObject obj, Action action = null, Action<BaseEventData> dragAction = null, UIEvent type = UIEvent.Click)
    {
        UIEventHandler uIEventHandler = Utilities.GetOrAddComponent<UIEventHandler>(obj);

        switch (type)
        {
            case UIEvent.Click:
                uIEventHandler.onClickHandler -= action;
                uIEventHandler.onClickHandler += action;
                break;
            case UIEvent.Pressed:
                uIEventHandler.onPointerPressedHandler -= action;
                uIEventHandler.onPointerPressedHandler += action;
                break;
            case UIEvent.LeftPounterUp:
                uIEventHandler.onLeftPointerUpHandler -= action;
                uIEventHandler.onLeftPointerUpHandler += action;
                break;
            case UIEvent.LeftPointerDown:
                uIEventHandler.onLeftPointerDownHandler -= action;
                uIEventHandler.onLeftPointerDownHandler += action;
                break;
            case UIEvent.RightPounterUp:
                uIEventHandler.onRightPointerUpHandler -= action;
                uIEventHandler.onRightPointerUpHandler += action;
                break;
            case UIEvent.RightPounterDown:
                uIEventHandler.onRightPointerDownHandler -= action;
                uIEventHandler.onRightPointerDownHandler += action;
                break;
            case UIEvent.Drag:
                uIEventHandler.onDragHandler -= dragAction;
                uIEventHandler.onDragHandler += dragAction;
                break;
            case UIEvent.BeginDrag:
                uIEventHandler.onBeginDragHandler -= dragAction;
                uIEventHandler.onBeginDragHandler += dragAction;
                break;
            case UIEvent.EndDrag:
                uIEventHandler.onEndDragHandler -= dragAction;
                uIEventHandler.onEndDragHandler += dragAction;
                break;
        }
    }

UI_Base에서 이벤트를 바인드 할 때, switch문으로 이벤트의 종류를 구분하여 알맞는 이벤트에 등록중이다. 거의 40줄이나 되는 분량이다.

이 코드가 무서운 점은, PointerEnter/Exit나 Drop와 같은 이벤트를 추가로 구현하려면, UIEvent라는 열거형에 이벤트를 종류를 추가로 정의해주고, UIEventHandler에 새로운 callback delegate 선언해주고, 이벤트를 호출할 메서드 만들어주고, UI_Base에서 BindEvent할 때 switch문에 새로운 이벤트를 등록하는 로직까지 추가해줘야한다.

겨우 이벤트 하나 추가하는데 거의 3개의 클래스를 건드려야한다. 이런 케이스를 샷건 수술이라고 하는데, switch문과 함께 객체지향에서 피해야하는 상황 중 하나라고 이번 주 특강에서 튜터님이 말씀해주셨다.


UIEventHandler 리팩토링

UI_Base 클래스의 switch문을 지우기 위해선, 먼저 UIEventHandler를 더욱 객체 지향적으로 바꿔야한다.

지금의 UIEventHandler 클래스는 EventSystem과 관련된 인터페이스들을 상속받고, 그에 알맞는 delegate를 선언해서 관리중이다.

public class UIEventHandler : MonoBehaviour, IPointerClickHandler, IDragHandler, IPointerDownHandler, IPointerUpHandler, IBeginDragHandler, IEndDragHandler
{
    public Action onClickHandler = null;
    public Action onPointerPressedHandler = null;
    public Action onLeftPointerDownHandler = null;
    public Action onLeftPointerUpHandler = null;
    public Action onRightPointerDownHandler = null;
    public Action onRightPointerUpHandler = null;
    public Action<BaseEventData> onDragHandler = null;
    public Action<BaseEventData> onBeginDragHandler = null;
    public Action<BaseEventData> onEndDragHandler = null;

그런데, 유니티엔 이미 저 인터페이스들을 상속받아 구현하고 있는 컴포넌트가 있다.

바로 EventTrigger 컴포넌트다.
그래서, UIEventHandlerEventTrigger를 상속받아서, 내 입맛대로 커스터마이징 해보기로 했다.

EventTrigger 내부 코드 핥아보기

EventTrigger의 핵심은 3가지이다.

  • EventSystem에서 호출하는 인터페이스들을 모두 상속하고 구현하고 있다.
  • List<Entry>로 등록된 모든 callback들을 관리한다.
  • Execute() 메서드는, List<Entry>를 모두 순회하며 EventTriggerType이 같은 Entry의 callback을 invoke한다.

커스터마이징

Entry

Entry 클래스는 callback이 UnityAction으로 정의돼있다. 나는 그냥 Action을 쓰고 싶어서 new 키워드로 상속된 Entry를 숨기는 새로운 Entry 클래스를 만들었다.

    new public class Entry
    {
        public EventTriggerType eventID = EventTriggerType.PointerClick;
        public Action<BaseEventData> callback;
    }

그리고, 이 Entry를 상속받는 ButtonEntry라는 클래스도 만들었다.

    public class ButtonEntry : Entry
    {
        public PointerEventData.InputButton inputButton = PointerEventData.InputButton.Left;
    }

버튼에 대한 정보도 저장해서, 이제 Bind할 때 누른 버튼이 어떤 버튼인지도 구분해서 등록해줄 수 있다.

Execute

    private void Execute(EventTriggerType id, BaseEventData eventData)
    {
        for (int i = 0; i < triggers.Count; ++i)
        {
            var ent = triggers[i];
            if (ent.eventID == id)
                ent.callback?.Invoke(eventData);
        }
    }

    private void Execute(EventTriggerType id, PointerEventData eventData)
    {
        for (int i = 0; i < triggers.Count; ++i)
        {
            if (triggers[i] is not ButtonEntry)
                continue;

            var ent = triggers[i] as ButtonEntry;
            if (ent.eventID == id && eventData.button == ent.inputButton)
                ent.callback?.Invoke(eventData);
        }
    }

Execute 메서드도 재정의 해줬다.
List를 순회하며 이벤트 id가 같은 Entry의 callback을 실행하는 구조는 유지했지만, PonterEventData를 수신하는 경우엔 ButtonEntry에서 어떤 버튼인지까지 구분해서 호출하도록 오버로딩해줬다.


UI_Base 리팩토링

BindEvent

바뀐 UIEventHandler에 맞춰 구조를 바꾼다. switch문을 삭제할 수 있게 됐다.
결과적으로 40줄이 대략 10줄로 줄어들었다!

    public static void BindEvent(GameObject obj, Action action = null, EventTriggerType eventType = EventTriggerType.PointerClick, PointerEventData.InputButton? inputButton = PointerEventData.InputButton.Left)
    {
        UIEventHandler uIEventHandler = Utilities.GetOrAddComponent<UIEventHandler>(obj);

        if (inputButton.HasValue)
        {
            UIEventHandler.ButtonEntry entry = new();
            entry.eventID = eventType;
            entry.inputButton = inputButton.Value;
            entry.callback += x => action?.Invoke();
            uIEventHandler.triggers.Add(entry);
        }
        else
        {
            UIEventHandler.Entry entry = new();
            entry.eventID = eventType;
            entry.callback += x => action?.Invoke();
            uIEventHandler.triggers.Add(entry);
        }
    }

이제 BindEvent를 이용하여 이벤트를 바인딩할 때, 이전보다는 조금 더 명확하게 할 수 있는 것 같다.
대략적인 구조는 매개변수로 받아온 데이터를 바탕으로 EntryButtonEntry 객체를 생성하여 List에 추가해주는 방식이다.

매개변수를 설명해보자면,,

GameObject obj

이벤트를 바인딩할 오브젝트. 실행 후 UIEventHandler 컴포넌트가 추가된다.

Action action / Action<BaseEventData> action

이벤트가 발생하면 실행될 Action이다.
오버로딩으로 action에 매개변수가 있는 경우, 없는 경우 모두 바인딩 가능하게 해봤다.

EventTriggerType eventType

어떤 이벤트가 발생했을 때 실행할 것인지 정한다.
열거형으로, 모든 이벤트 타입이 정의돼있다. (ex. Click, PointerDown, PointerEnter, Drag, Drop 등등..)

PointerEventData.InputButton? inputButton

어떤 버튼이 눌렸을 때 실행할 것인지 정한다.
nullable로 한 이유는, PointerEventData가 아닌 BaseEventData를 수신하는 이벤트도 있기 때문이다. 이 경우엔 어떤 버튼이 눌렸는지의 정보는 필요없기 때문에 null로 지정해주면, ButtonEntry가 아닌 Entry 타입으로 List에 등록한다.


EventBind 예제

gameObject.BindEvent(BeginDrag, EventTriggerType.BeginDrag, PointerEventData.InputButton.Left);
gameObject.BindEvent(Dragging, EventTriggerType.Drag, PointerEventData.InputButton.Left);
gameObject.BindEvent(Drop, EventTriggerType.Drop, PointerEventData.InputButton.Left);
gameObject.BindEvent(EndDrag, EventTriggerType.EndDrag, PointerEventData.InputButton.Left);

어떤 상황, 어떤 버튼이 눌렸을 때 호출할 것인지를 바인딩 시점에 정했기 때문에 보다 명확하게 사용할 수 있을 것 같다.
간단한 드래그앤드랍으로 인벤토리의 슬롯끼리 서로 자리를 바꾸는 기능을 구현해봤다.

profile
game developer

0개의 댓글