[트러블 슈팅] 카드 삽입/스왑에 대한 고민

Jongmin Kim·5일 전

서론

어쩌다 보니 뒤늦게 기획자분께서 카드 패 내부에서의 카드 스왑이 가능했으면 좋겠다는 의견을 받았다.
카드 스왑은 한 번도 구현해본 경험이 없어서 할 수 있을지 의문이 들었다.

근데 뭐.. 못하면 어쩔건데 마인드로 하드코딩이라도 해볼 생각으로 작업에 들어갔다.



아이디어

구조에 대한 아이디어

현재는 카드 UI도 MVP 아키텍처로, 카드 UI들을 관리하는 관리자 UI도 MVP 아키텍처로 작성되어 있는 상황이다.

결국 다음과 같은 프로세스를 따라가야 한다.

  1. 사용자가 View에서 마우스 상호작용을 통해 카드를 옮긴다.
  2. 카드를 옮기면서 레이아웃에 따라 나머지 카드들은 이동해야만 한다.
  3. 카드를 옮긴 위치에 따라 Model을 갱신시킨다.

뭐 1번과 3번만 생각하면 진짜 머리가 하나도 안아픈 문제였다.
근데 2번이 발목을 잡았다. 결국 레이아웃을 관리하는건 관리자 UI였기 때문이다.


따라서 나는 카드 UI에서는 이벤트 감지만! 이벤트 처리는 관리자 UI가! 라는 생각을 했다.
생각해보면 나쁘지 않다. 결국 카드 UI는 보여주는 용도지, 따로 로직을 지닐 필요가 없다.

그래서 관리자 UI의 PresenterModel을 갱신시키도록 구현하고자 했다.


구현에 대한 아이디어

이게 직접 스토리텔링을 하려니 글재주가 없어서 단계별로 정리하는 편이 낫다고 생각한다.
나는 다음과 같은 알고리즘을 생각했다.

1. 카드를 드래그 하는 동안 마우스 위치로 레이 히트된 카드 UI를 찾는다.

2. 레이 히트된 카드 UI의 좌표와 현재 마우스의 좌표를 인덱스 기반으로 비교한다.

	2-1. 드래그 중인 카드의 인덱스 < 레이 히트된 카드의 인덱스인 경우
         : X 좌표를 비교하여 마우스 X 좌표가 더 크다면 레이 히트된 카드의 이전 인덱스까지 당긴다.
         
    2-2. 드래그 중인 카드의 인덱스 > 레이 히트된 카드의 인덱스인 경우
         : X 좌표를 비교하여 마우스 X 좌표가 더 작다면 레이 히트된 카드의 이후 인덱스를 민다.
         
3. 인덱스를 밀고 당겨서 생긴 하나의 공백의 자리에 프리뷰 카드 오브젝트를 띄운다.

4. 드래그가 끝나면 빈 인덱스 자리에 카드를 삽입한다.



구현

1. 카드를 드래그 하는 동안 마우스 위치로 레이 히트된 카드 UI를 찾는다.

드래그 중인 카드가 레이캐스팅을 막는 중이라 이벤트 시스템에 직접 개입하여 카드를 찾아낸다.

// Hand Card Event Controller의 일부

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;

    var ray_hits = new List<RaycastResult>();
    EventSystem.current.RaycastAll(pointer_data, ray_hits);

    foreach(var hit in ray_hits)
    {
	      // 드래그 중인 카드 UI가 아닌 카드 UI를 찾습니다.
        var card_hit = hit.gameObject.GetComponent<IHandCardView>();
        if(card_hit != null && m_presenter.HoverCard != card_hit)
            return hit;

		   ...
    } 

    return null;
}

2. 레이 히트된 카드 UI의 좌표와 현재 마우스의 좌표를 인덱스 기반으로 비교한다.

// Hand Card Event Controller의 일부

private void SwapInSameField(IHandCardView hand_card, Vector2 position)
{
    var target_card = m_presenter.HoverCard;
    var concrete_card = hand_card as HandCardView;

		// 드래그 중인 카드와 레이 히트된 카드의 인덱스 상의 순서를 비교한다.
    if(m_container.IsPriority(target_card, hand_card))
    {
        // 드래그 중인 카드가 앞서는 경우, 마우스 좌표가 더 크다면 앞당긴다.
        if(position.x >= concrete_card.transform.position.x)
        {
            m_container.Swap(target_card, hand_card);
            m_layout_controller.UpdateLayout(true);
        }
    }
    else
    {
        // 드래그 중인 카드가 뒤서는 경우, 마우스 좌표가 더 작다면 민다.
        if(position.x < concrete_card.transform.position.x)
        {
            m_container.Swap(target_card, hand_card);
            m_layout_controller.UpdateLayout(true);
        }
    }        
}

3. 인덱스를 밀고 당겨서 생긴 하나의 공백의 자리에 프리뷰 카드 오브젝트를 띄운다.

private void CalculatePreviewPosition()
{
    var layout_data = CardLayoutCalculator.CalculatedHandCardTransform(m_container.GetIndex(m_presenter.HoverCard),
                                                                       m_container.Cards.Count,
                                                                       m_designer.Radius,
                                                                       m_designer.Angle,
                                                                       m_designer.Depth);      
    m_preview_object.SetActive(true);

    var preview_rt = m_preview_object.transform as RectTransform;
    preview_rt.anchoredPosition = layout_data.Position;
    preview_rt.rotation = Quaternion.Euler(layout_data.Rotation);
    preview_rt.localScale = layout_data.Scale;
}

  1. 드래그가 끝나면 빈 인덱스 자리에 카드를 삽입한다.
private void SwapInSameField(IHandCardView hand_card, Vector2 position)
{
    var target_card = m_presenter.HoverCard;
    var concrete_card = hand_card as HandCardView;

    if(m_container.IsPriority(target_card, hand_card))
    {
        if(position.x >= concrete_card.transform.position.x)
        {
            // 빈 위치에 카드를 삽입한다.
            m_container.Swap(target_card, hand_card);
            m_layout_controller.UpdateLayout(true);
        }
    }
    else
    {
        if(position.x < concrete_card.transform.position.x)
        {
            // 빈 위치에 카드를 삽입한다.
            m_container.Swap(target_card, hand_card);
            m_layout_controller.UpdateLayout(true);
        }
    }        
}



해결

다음과 같이 정상적으로 카드의 위치 변경이 가능해졌다.

profile
Game Client Programmer

0개의 댓글