포스트

EventSystem을 이용한 이벤트 강제 발생

UI 오브젝트가 Ray를 가리는 문제로 필드에 카드가 드랍되지 않던 오류를 해결하려고 했습니다.

EventSystem을 이용한 이벤트 강제 발생

타워 오브 가디언즈를 개발하면서 겪었던 문제 상황과 그에 대한 해결 방법이다.


문제 상황

카드를 주로 다루는 게임에서는 카드의 애니메이션과 상호작용을 처리하는 것이 중요하다.

나는 전반적인 모든 UI 부분의 개발을 맡으면서 카드의 상호작용도 맡게 되었다.
그러던 도중 카드를 드래그하여 필드에 드랍하는 과정에서 문제가 발생했다.

카드를 드래그해서 필드에 드랍해도 드랍이 되지 않는다!

인벤토리에서 드래그/드랍으로 아이템을 옮길 때 도와주는 DragSlot과 같은 역할이 없던 것이 발단이다.



임시 카드 매개체

처음에는 단순히 ‘DragSlot의 역할을 하는 오브젝트가 있으면 되지 않을까?’ 싶었다.
하지만 생각해보면 문제가 많았다.

  1. 카드를 잘못된 곳에 드랍한 경우, 카드가 되돌아서 와야 한다.
  2. 드랍한 위치에서 알맞은 레이아웃으로 자연스럽게 이동해야 한다.

DragSlot과 같은 매개체가 껴버리면 자연스러운 카드의 이동을 구현할 수 없다고 판단했다.



OnEndDrag와 OnDrop 사이의 타이밍

그래서 처음 생각한 방법이 다음과 같았다.

  1. 카드에서 OnEndDrag가 발생할 때 카드의 Block Raycastsfalse로 처리한다.
  2. 필드에서 OnDrop이 발생할 때 다시 카드의 Block Raycaststrue로 처리한다.

단순히 타이밍의 문제라고 생각했다. 하지만 늘 트러블 슈팅이 그렇듯 제대로 작동하지 않았다.

이유는 OnDrop이 호출되는 경우에 한해서는 OnDropOnEndDrag보다 먼저 호출되는 규칙이 있었다.
반대로 OnEndDragOnDrop에서 처리하는 카드의 Block Raycasts를 반대로 변경하면 OnDrop이 호출조차 안된다.

이 방법은 결과적으로 실패했다.



Event System으로의 개입

이 방법은 제일 하기 싫은 방법이었다.
직관적으로 Handler를 구현하고만 싶었지 Event System에 개입하여 해결한다고 하니 영 찝찝하지 않은가.

아이디어는 이랬다.

  1. 카드에서 OnEndDrag가 발생할 때 카드의 Block Raycastsfalse로 처리한다.
  2. 마우스 좌표에 위치하는 모든 UI에 대한 정보를 받아온다.
  3. 이 UI들 중 필드가 있다면 그 필드에 OnDrop을 강제로 발생시킨다.
  4. 카드의 Block Raycasts를 다시 true로 처리한다.

OnDrop이 호출되지 않더라도 마우스 좌표에 위치한 모든 정보를 받아오려면 Block Raycastsfalse로 처리해야만 한다.


구현

현재 내 코드의 구조는 다음과 같다.

  1. Card UI에서 이벤트를 감지한다.
  2. Hand UI에서 이벤트를 발생시킨 Card UI를 처리한다.

우선적으로 카드에서 이벤트 감지를 하던 부분에 Block Raycasts를 비활성화하는 코드를 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HandCardView : ...
{
    ...
    
    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번 과정
    }
}

그 다음으로 Hand UI에서 Card UI의 이벤트 처리를 하는 부분이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class HandEventController : ..
{
    ...
    
    // 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;
    }
}



마무리

내 생각엔 깔끔한 방법은 아니었다고 생각한다.
분명히 이것보다 더 좋은 생각이 있을 것이라고 생각한다.

하지만 지금의 내 실력으로는 이 정도가 최선이다.
프로젝트를 꾸준히 진행하면서 변경할 수 있다면 변경하는 방향으로 가야할 것만 같다.

아래는 결과적으로 해결된 모습이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.