[Unity2D] #7 - CardDeck UI-1

qweasfjbv·2024년 3월 25일

Unity2D

목록 보기
7/15
post-thumbnail

개요

기본적인 맵과 Enemy AI 까지 만들었습니다.
이제 카드 UI를 제작해보겠습니다.
저는 카드를 꺼내서 맵에 몹 생성/함정/버프가 가능하도록 만들 예정입니다.

카드게임 이라 하면 가장 유명한 게임 두 개가 있습니다.
하스스톤슬레이 더 스파이어 입니다.
저는 둘 중 더 즐겨하는 슬더스를 참고하여 작성해보겠습니다.

혹시 해당 게임을 잘 모르신다면 유튜브 참고하시거나 플레이 해보시는걸 추천드립니다.

구현

크게 CardBaseCardInHand 클래스로 나누겠습니다.
CardBase 는 추상클래스로 나중에 각 카드의 기능을 추상함수로 구현할 예정입니다.
CardInHand 는 각 카드를 자식으로 두고 List로 관리합니다.

CardBase 부터 보겠습니다.

CardBase

public abstract class CardBase : MonoBehaviour
    , IPointerEnterHandler
    , IPointerExitHandler
    , IBeginDragHandler
    , IDragHandler
    , IEndDragHandler

우선 여러 인터페이스들을 상속받습니다.
IPointerHandler 는 Hover를 구현하기 위해 있고,
IDragHandler 는 카드를 드래그해서 실제 맵에 효과를 구현하기 위해 상속했습니다.


    private void Update()
    {
        var targetV = UtilFunctions.CardLerp(rect.anchoredPosition, targetPos, 6f);
        this.rect.anchoredPosition = new Vector3(targetV.x, targetV.y);
        this.rect.localPosition = new Vector3(rect.localPosition.x, rect.localPosition.y, 0);

        this.rect.localScale= UtilFunctions.CardLerp(rect.localScale, targetScale, cardMoveSpeed);

        var rotateZ = rect.localRotation.eulerAngles.z;
        if (rotateZ >= 180) rotateZ = rotateZ - 360;

        this.rect.localRotation = Quaternion.Euler(0, 0, UtilFunctions.CardRotateLerp(rotateZ, targetAngle, 6f));


    }

핵심적인 Update문 입니다.
poistion, angle, scale 각각 target변수를 따로 두고 lerp로 따라가게 했습니다.
rotateZ는 그냥 쓰면 0~360 사이의 값이 나옵니다. lerp를 사용하기 위해 180 이상은 음수로 변환해줍니다.
또한 anchoredPosition을 사용해서 화면의 비율도 고려했습니다.


다음은 일부 인터페이스 멤버를 살펴보겠습니다.

    public void OnBeginDrag(PointerEventData eventData)
    {
        isDragged = true;
        transform.parent.GetComponent<CardInHand>().UpdateCardLayout();
    }

OnBeginDrag 함수입니다.
isDragged 변수를 true로 두고 CardInHandUpdateCardLayout 를 호출합니다.
간단하게 말하면 카드의 개수에 따라 위치를 조정하는 함수입니다.
밑에서 자세하게 살펴보겠습니다.


    public void OnDrag(PointerEventData eventData)
    {

        var mousePos = transform.parent.InverseTransformPoint(Camera.main.ScreenToWorldPoint(Input.mousePosition));
        SetTargetPosX(mousePos.x);
        SetTargetPosY(mousePos.y);

        var tmpColor = GetComponent<Image>().color;
        if (mousePos.y > CARD_HEIGHT)
        {
            isInField = true;
            tmpColor.a = UtilFunctions.ColorAlphaLerp(tmpColor.a, 0f, 10f);
            GetComponent<Image>().color = tmpColor;
        }
        else
        {
            isInField = false;
            tmpColor.a = UtilFunctions.ColorAlphaLerp(tmpColor.a, 1f, 6f);
            GetComponent<Image>().color = tmpColor;
        }

    }

OnDrag 함수입니다. 드래그 하는동안 계속 호출됩니다.
마우스 위치를 CardInHand 기준으로 변환해서 target값을 바꿔줍니다.
또한, 자연스러운 효과를 위해 일정 높이 이상이면 점점 투명해지게 만들었습니다.
이 때 isInField 를 바꿔주어서 카드를 뗄 떼, 손으로 들어올지 맵에 기능을 구현할지 결정합니다.



   
    public void OnEndDrag(PointerEventData eventData)
    {
        CameraController.CanMove = true;
        isDragged = false;

        if (isInField)
        {
            ActivateEffect(Camera.main.ScreenToWorldPoint(Input.mousePosition));
            transform.parent.GetComponent<CardInHand>().RemoveCardInHand(transform.GetSiblingIndex());
        }
        else
        {

            SetColor(Color.white);
            transform.parent.GetComponent<CardInHand>().UpdateCardLayout();
        }

    }

드래그를 끝낼 때 호출되는 OnEndDrag 함수입니다.
isInFieldtrue 면 삭제시키고, ActiveEffect 추상 함수를 호출합니다.
false 면 색을 원래대로 되돌리고 카드배치를 업데이트합니다.

그 외 OnPointerEnter/Exit 는 카드의 targetPosition, angle, scale 을 조금씩만 조정해주면 돼서 생략하겠습니다.

CardInHand

CardInHand 클래스에는 크게 카드 추가, 삭제, 리스트 업데이트, 카드 배치 업데이트 등이 있습니다.

카드 추가 및 삭제 함수는 List에 Add 한 후에 카드 배치 업데이트 하는게 전부라서 생략하겠습니다.


    public void UpdateCardLayout()
    {

        UpdateCardList();
        


		// posx 로직 구현
        
        float posXUnit = 0.43f;
        float sum = 0;

        if (cardsInHand.Count % 2 == 0) sum += posXUnit;


        for (int i = cardsInHand.Count/2; i < cardsInHand.Count; i++)
        {
            cardsInHand[i].SetTargetPosX(CardBase.CARD_WIDTH * sum);
            cardsInHand[cardsInHand.Count - i - 1].SetTargetPosX(-1 * CardBase.CARD_WIDTH * sum);
            sum += posXUnit * 2;
            posXUnit -= 0.05f;
        }
        

        
         // angle과 posy 로직 구현
         

        for (int i = 0; i < cardsInHand.Count/2; i++)
        {
            cardsInHand[i].SetTargetAngle(ANGLE_PER_CARD * (cardsInHand.Count/2 - i));
            cardsInHand[cardsInHand.Count -i -1].SetTargetAngle(-1 * ANGLE_PER_CARD * (cardsInHand.Count / 2 - i));

            float offsetSum = 0;
            if (i == 0) { 
                cardsInHand[i].SetTargetPosY(0);
                cardsInHand[cardsInHand.Count - i - 1].SetTargetPosY(0);
            }
            else
            {
                for (int t = 0; t < i; t++) offsetSum += CARD_POS_Y_OFFSET * Mathf.Pow(0.86f, t);

                cardsInHand[i].SetTargetPosY(offsetSum);
                cardsInHand[cardsInHand.Count - i - 1].SetTargetPosY(offsetSum);

            }

        }
        if (cardsInHand.Count % 2 != 0)
        {
            cardsInHand[cardsInHand.Count / 2].SetTargetAngle(0);
            float offsetSum = 0;

            for (int t = 0; t < cardsInHand.Count / 2; t++) offsetSum += CARD_POS_Y_OFFSET * Mathf.Pow(0.86f, t);

            cardsInHand[cardsInHand.Count /2].SetTargetPosY(offsetSum);
        }


    }

UpdateCardList 함수는 현재 자식으로 가지는 카드들을 List에 최신화하는 함수입니다.
카드의 개수에 따라 각 카드의 targetPosition, targetAngle을 바꿔줍니다.
각 상수값들은 최대한 비슷하게 만들기 위해 조정한 값입니다.

여기까지 하고 실행화면을 한번 보겠습니다.

  • Hover 때 targetPosY에 CardWidth의 절반을 더한게 약간 어색합니다.
    더하지 말고 그냥 PosY를 CardWidth의 절반으로 설정해주겠습니다.

  • 뭔가 밋밋해 보입니다.
    카드위에 마우스가 올라갔을 때 그 카드 말고 다른 카드들도 움직이면 더 자연스러울 것 같습니다.
    따라서 함수 하나를 더 추가하겠습니다


    private void UpdateWhenHovered()
    {
        var hoverIdx = HasAnyHovoeredCard();
        if (hoverIdx == -1) return;


        // hover시 다른 카드들이 밀려나도록 만듦
        for (int i=0; i< cardsInHand.Count; i++)
        {
            if (i == hoverIdx) continue;

            cardsInHand[i].SetTargetPosX(cardsInHand[i].GetTargetPosX() + Mathf.Pow(0.3f, Mathf.Abs(i-hoverIdx)) * CardBase.CARD_WIDTH * Mathf.Sign(i-hoverIdx));
        }
    }

각 CardBase에서 OnPointerEnter 시에 호출하는 함수입니다.
카드들이 살짝식 옆으로 밀려나서 자연스럽게 만들어줍니다.
수정 후의 실행화면 보겠습니다.

마무리

주변 카드까지 움직여주니 좀더 자연스러워 졌습니다.
이번에는 복잡한 로직이 아니라서 금방 끝낼 수 있었습니다.
다음에는 카드 기능을 약간 구현하고 맵에 설치하는 기능을 만들어보겠습니다.

저는 여러 클래스를 왔다갔다 하면서 동시에 쌓아올려서 코드가 이해가 가지만, 블로그에 정리하면서 한 클래스씩 살펴보고 몇몇 부분은 생략하다보니 처음 보시는 분들은 이해가 잘 가지 않을 수도 있을 것 같습니다.
코드 전문은 제 Github에서 보실 수 있고 참고자료를 항상 하단에 적어두고 있으니, 이 글의 설명만으로 부족하시다면 참고하시기 바랍니다.

참고자료

https://store.steampowered.com/app/646570/Slay_the_Spire/

0개의 댓글