오늘은 어제 제출한 개인과제의 UI_Base
와 UIEventHandler
클래스를 리팩토링하기로 했다.
이번 주에 있었던 클린코드 특강에서 switch문의 사용도 자제하는게 좋다고 하셨는데, 마침 UI_Base
에서 switch문으로 40줄이나 써먹고 있었기 때문에 조금 더 객체지향적으로 바꿔보기로 했다.
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문과 함께 객체지향에서 피해야하는 상황 중 하나라고 이번 주 특강에서 튜터님이 말씀해주셨다.
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
컴포넌트다.
그래서, UIEventHandler
에 EventTrigger
를 상속받아서, 내 입맛대로 커스터마이징 해보기로 했다.
EventTrigger
의 핵심은 3가지이다.
EventSystem
에서 호출하는 인터페이스들을 모두 상속하고 구현하고 있다.List<Entry>
로 등록된 모든 callback들을 관리한다.Execute()
메서드는, List<Entry>
를 모두 순회하며 EventTriggerType
이 같은 Entry
의 callback을 invoke한다.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할 때 누른 버튼이 어떤 버튼인지도 구분해서 등록해줄 수 있다.
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
에서 어떤 버튼인지까지 구분해서 호출하도록 오버로딩해줬다.
바뀐 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
를 이용하여 이벤트를 바인딩할 때, 이전보다는 조금 더 명확하게 할 수 있는 것 같다.
대략적인 구조는 매개변수로 받아온 데이터를 바탕으로 Entry
나 ButtonEntry
객체를 생성하여 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에 등록한다.
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);
어떤 상황, 어떤 버튼이 눌렸을 때 호출할 것인지를 바인딩 시점에 정했기 때문에 보다 명확하게 사용할 수 있을 것 같다.
간단한 드래그앤드랍으로 인벤토리의 슬롯끼리 서로 자리를 바꾸는 기능을 구현해봤다.