타워 오브 가디언즈라는 턴제 덱빌딩 로그라이크 게임을 만들게 되었다.
덱빌딩 장르라 그런건지 세세한 디테일이 많은 기획이라 그런지 꽤나 신경 쓸 부분이 많다.
전투 시스템 UI의 전반적인 구현을 맡게 되면서 카드의 애니메이션과 상호작용을 주로 다루고 있던 도중에 카드를 드래그하여 필드에 드랍하는 과정에서 문제가 발생했다.

드래그해서 필드에 드랍해도 드랍이 되지 않는다!
단순히 인벤토리를 구현하는 과정과 같이 OnEndDrag 이후 OnDrop이 자연스럽게 호출되어야 했지만 인벤토리와는 달리 Drag Slot과 같이 중간 다리 역할을 해주는 오브젝트가 없는 것이 발단이었다.
처음에는 단순히 Drag Slot의 역할을 하는 오브젝트가 있으면 되지 않을까? 이런 생각을 했지만 그렇게 될 걸 머릿 속에서 그려보니 자연스러운 카드의 애니메이션이 나오지 않았다.
그래서 처음 생각한 방법이 다음과 같았다.
1. 카드에서 OnEndDrag가 발생할 때 카드의 Block Raycasts를 false로 처리한다.
2. 필드에서 OnDrop이 발생할 때 다시 카드의 Block Raycasts를 true로 처리한다.
단순히 타이밍의 문제라고 생각했다. 하지만 늘 트러블 슈팅이 그렇듯 제대로 작동하지 않았다.
왜 그런가를 살펴보니 OnDrop이 호출되는 경우에 한해서는 OnDrop이 OnEndDrag보다 먼저 호출되는 구조를 가지고 있었다.
반대로 OnEndDrag와 OnDrop에서 처리하는 카드의 Block Raycasts를 반대로 변경하면 OnDrop이 아예 호출되지도 않는다. (순환 참조 느낌이다. 정말로.)
이 방법은 결과적으로 실패했다.
이 방법은 제일 하기 싫은 방법이었다. 직관적으로 Handler를 구현하고만 싶었지 Event System에 개입하여 해결한다고 하니 사실 영 찝찝하지 않은가.
아이디어는 이랬다.
1. 카드에서 OnEndDrag가 발생할 때 카드의 Block Raycasts를 false로 처리한다.
2. 마우스 좌표에 위치하는 모든 UI에 대한 정보를 받아온다.
3. 이 UI들 중 필드가 있다면! 그 필드에 OnDrop을 강제로 발생시킨다.
4. 카드의 Block Raycasts를 다시true로 처리한다.
어쨌거나 OnDrop이 호출되지 않더라도 마우스 좌표에 위치한 모든 UI들의 정보를 받아오려면 Block Raycasts를 false로 처리해야만 한다.
내 코드 구조는 다음과 같다.
Card UI에서 이벤트 감지 → Hand UI에서 이벤트가 발생된 Card를 처리
우선적으로 카드에서 이벤트 감지를 하던 부분을 다음과 같이 구현했다.
이는 카드의 Block Raycasts를 단순히 토글하며 이벤트를 발생시킨다.
public class Card : ...
{
...
public void ToggleRaycast(bool active)
=> m_canvas_group.blocksRaycasts = active;
...
public void OnEndDrag(PointerEventData eventData)
{
ToggleRaycast(false); // 1번 과정
OnEndDragAction?.Invoke(); // 여기서 2번과 3번을 처리할 예정
ToggleRaycast(true); // 4번 과정
}
}
public class Hand : ..
{
...
// Card UI에서 발생한 이벤트를 Hand UI에서 처리한다는 점을 말하고 싶었다.
public IHandCardView InstantiateCardView()
{
var card_obj = ObjectPoolManager.Instance.Get(m_card_prefab);
card_obj.transform.SetParent(transform, false);
var card_view = card_obj.GetComponent<IHandCardView>();
card_view.OnPointerEnterAction += () => { OnPointerEnterInCard(card_view); };
card_view.OnPointerExitAction += () => { OnPointerExitFromCard(); };
card_view.OnBeginDragAction += () => { OnBeginDragCard(); };
card_view.OnDragAction += (position) => { OnDragCard(position); };
card_view.OnEndDragAction += () => { OnEndDragCard(); };
return card_view;
}
...
// 3번 과정
private void OnEndDragCard()
{
...
var hit = CheckField(out var pointer_data);
var drop_handler = hit?.gameObject.GetComponent<IDropHandler>();
// 필드가 존재한다면 필드에 강제로 OnDrop 이벤트를 발생시킨다.
if(drop_handler != null)
ExecuteEvents.Execute(hit?.gameObject, pointer_data, ExecuteEvents.dropHandler);
...
}
// 2번 과정
private RaycastResult? CheckField(out PointerEventData pointer_data)
{
// 현재 마우스 좌표를 기준으로 새로운 이벤트 데이터를 생성한다.
pointer_data = new PointerEventData(EventSystem.current);
pointer_data.position = Input.mousePosition;
pointer_data.pointerDrag = (m_presenter.HoverCard as HandCardView).gameObject;
// 생성한 이벤트 데이터를 바탕으로 현재 마우스 좌표에 위치한 모든 UI를 가져온다.
var ray_hits = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointer_data, ray_hits);
// 이 UI들에서 IDropHandler를 구현하는 UI가 있다면 그것이 필드다.
foreach(var hit in ray_hits)
{
var drop_handler = hit.gameObject.GetComponent<IDropHandler>();
if(drop_handler != null)
return hit;
}
// 발견하지 못했다면 필드가 없는 것이다.
return null;
}
}
위의 코드와 같이 강제로 이벤트를 발생시키면 OnDrop이 호출 됨을 확인했다.
