본문 바로가기

Unity 2D 구현 - 카드 배치 연출 Ver.3

@코야딩구2025. 7. 3. 20:13

1. 게임 시작 시 카드 배치 연출

- 게임이 시작되면, 카지노 딜러가 카드를 한 장씩 나눠주는 것처럼, 카드가 화면 아래에서 회전하며 하나씩 순서대로 날아가며 지정 위치에 배치된다.

- 이때 각 카드는 빠르게 출발하여, 도착 위치에서 부드럽게 착지한다.

- 배치 중인 카드는 클릭할 수 없고, 모든 카드가 제자리에 배치된 후, 플레이어가 짝을 맞추는 게임을 시작할 수 있도록 설계했다.

- 렌더링 순서는 맨위에 먼저 날아갈 카드가 먼저 보이도록 했다.

- 처음에는 가까운 카드부터 배치했지만, 이 방식은 뒤에 배치될 카드가 날아갈 시, 앞쪽 카드에 가려지는 문제가 있어, 최종적으로 먼 곳부터 먼저 배치하는 방식으로 변경하였다.

2. 카드 이동 및 회전 방식

- 각 카드는 도착해야 할 위치와 회전값이 미리 정해져 있으며, 아래와 같은 방식으로 이동과 회전을 연출하였다.

- 아래 과정을 통해 카드 하나하나가 회전하며 날아가 정해진 자리에 도착하는, 시각적으로도 만족스러운 연출을 구현할 수 있었다.

2-1. 순차 배치 연출

- 각 카드는 일정 시간 (cardTime) 간격으로 차례대로 날아간다.

- 즉, 0번 카드는 0초, 1번 카드는 cardTime초 후에 날아가며, 카드가 날아갈 준비가 안 된 상태일 땐 해당 프레임에서 처리를 건너뛴다.

- Coroutine을 이용해 각 카드마다 WaitForSeconds()로 배치 간 딜레이를 줘도 된다고 하지만, 아직 안 배운 기능이라, 배우면 활용해 보려 한다.

2-2. 이동

- Vector2.Lerp()를 사용해 카드가 지정된 위치로 부드럽게 이동하도록 구성했다.

- 여기에 Ease Out 효과를 추가해, 초반에는 빠르게 움직이다가 도착지에 도달할수록 속도가 줄어드는 자연스러운 궤적을 만들었다.

2-3. 회전

- 카드가 날아가는 동안에는 초기 회전값에서 시작하여, 총 두 바퀴 정도를 회전한 후, 최종적으로  정방향(0도)으로 정렬되도록 만들었다.

- Mathf.Lerp()로 회전값을 보간한 후, Quaternion.Euler()를 사용해 적용하였다.

2-4. 전체 구현 코드

//cs 내 코드
enum SetCard
{
    Ready, Start, Throw, End
}
// 첫 상태는 준비
SetCard setState = SetCard.Ready;
// 인스펙터 창에서 설정할 프리팹 카드
public GameObject card;

// 보간 사용을 위한 값 저장 배열
List<Vector2> endV2; // 날아갈 위치
List<GameObject> tmpG; // 카드 오브젝트 저장
List<Vector2> StartPosA; // 펼쳐질 위치 저장
List<float> EndTheta; // 끝 위치 각도
// 시작 위치
[SerializeField] Vector2 startV2 = new Vector2(0f, -5f);
// 시작 위치 각도
float startTheta = Mathf.PI;
//Lerp용 Time
float lerpTime = 0.0f;
// 반지름
float tmpR = 0.6f;

// 카드 뿌리는 간격
[SerializeField] float cardTime = 0.25f;
// 카드 날아가는 총 시간
public float cardTotalTime = 5.5f;
// 카드 총 개수
[SerializeField] int cardCnt = 12;

private void Awake()
{
    // 날아가는 시간 세팅
    // 카드 날리는 간격 * 카드 인덱스만큼 + 날아가는 시간
    cardTotalTime = cardTime * (cardCnt - 1) + 1.0f;
}
void Start()
{
    int[] arr = { 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6 };
    for (int i = arr.Length - 1; i > 0; i--) // 셔플 알고리즘
    {
        int j = UnityEngine.Random.Range(0, i + 1);
        (arr[i], arr[j]) = (arr[j], arr[i]);
    }

    // 새로운 방식, 카드 생성하고 원하는 위치에 뿌리기
    endV2 = new List<Vector2>();   // 도착 지점 저장용 배열
    EndTheta = new List<float>();  // 끝 위치 회전 값 저장용 배열
    tmpG = new List<GameObject>(); // 카드 오브젝트 저장용 배열
    StartPosA = new List<Vector2>(); // 시작 지점 저장용 배열
    // 카드 첫 배치
    for (int i = 0; i < cardCnt; i++)
    {
        tmpG.Add(Instantiate(card, this.transform));

        // 배치 위치 지정하기
        float x = (i % 4) * 1.4f - 2.1f;
        float y = (i / 4) * 1.4f - 3.0f;
        endV2.Add(new Vector2(x, y)); // 도착지머

        // Ver3: 한점에서 펼쳐지기
        float tmpTheta = 180.0f / (cardCnt - 1) * i * Mathf.Deg2Rad;
        EndTheta.Add(tmpTheta);
        StartPosA.Add(new Vector2(0f + tmpR * Mathf.Cos(tmpTheta), -5f + tmpR * Mathf.Sin(tmpTheta)));
        tmpG[i].transform.position = new Vector2(0f + tmpR * Mathf.Cos(startTheta), -5f + tmpR * Mathf.Sin(startTheta));

        // 모두 거꾸로 회전해서 시작, 그래야 펼쳤을 때, 맨 위의 카드가 정면
        tmpG[i].transform.rotation = Quaternion.Euler(0f, 0f, 180); 
		
        // tmp로 Card 컴포넌트 한 번만 접근하기
        Card tmpCard = tmpG[i].GetComponent<Card>();
        // 카드에 인덱스, 사진 등 세팅해주기
        tmpCard.Setting(arr[i]);
        // 카드 애니메이션 재생 중지하기
        tmpCard.anim.speed = 0f;
        // 첫번째 카드가 가장 위로 가게 하기
        tmpCard.backSprite.sortingOrder = 20 - i;
        tmpCard.backCanvas.sortingOrder = 20 - i;
    }
    // 기존에는 가까운 곳부터 먼 곳으로, 왼쪽부터 오른쪽으로 배치 위치를 지정했지만,
    // 카드 겹침 상태가 원하는 대로 나오지 않아, 먼 곳부터 배치하기
    endV2.Reverse();
}

void Update()
{
    switch (setState)
    {
    	// 처음부터 카드를 날리면, 세팅하는 장면을 놓칠 수 있어, 씬 시작 후의 대기 시간
        case SetCard.Ready: 
            lerpTime += Time.deltaTime;
            if (lerpTime >= 0.5f)
            {
                lerpTime = 0.0f;
                setState = SetCard.Start;
            }
            break;
        // 시작이 되면 카드를 펼치기
        case SetCard.Start:
            lerpTime += Time.deltaTime;
            for (int i = 0; i < cardCnt; i++)
            {
            	// lerpTime * 2하여 0.5초가 된다면 펼치기가 끝나도록
                // 보간을 사용해, 시작 점 기준, 다음 라디안 구하기
                float targetTheta = Mathf.Lerp(startTheta, EndTheta[i], lerpTime * 2);
                // 삼각함수를 사용에 다음에 위치할 곳으로 위치 갱신해주기 
                tmpG[i].transform.position = new Vector2(startV2.x. + tmpR * Mathf.Cos(targetTheta), -startV2.y + tmpR * Mathf.Sin(targetTheta));
                // 펼쳐짐에 따라 카드 회전 z값도 수정해주기 
                float nextZ = targetTheta * Mathf.Rad2Deg;
                // 회전 적용
                tmpG[i].transform.rotation = Quaternion.Euler(0f, 0f, nextZ);
            }
            if (lerpTime >= 0.75f) // 실제 펼쳐지는 시간 = 0.5xxxx, 유예 시간 약 0.25초
            {
                setState = SetCard.Throw;
                lerpTime = 0.0f;
            }
            break;
        case SetCard.Throw:
            // 시간에 따라 카드가 날아간다
            lerpTime += Time.deltaTime;
            // 카드 날리기
            for (int i = 0; i < cardCnt; i++) // 0번 카드 부터 카드 개수 - 1번 카드까지
            {
                // lerpTime과 cardTime에 따라 날아가는 인덱스 설정하기
                // ex) lerpTime = 1.2f, cardTime = 0.5f -> tmpI = (int)(1.2f / 0.5f) = 2 
                //     (2번 인덱스 까지 카드 날리기 진행)
                // 삼항 연산자로 cardTime이 0이라면 바로 다 날아가도록 설정
                int tmpI = cardTime == 0.0f ? cardCnt : (int)(lerpTime / cardTime);

                // tmpI보다 인덱스가 작으면 리턴하기
                // 해당 인덱스부터는 업데이트가 필요 없기때문에 continue가 아닌 return
                if (tmpI < i) return;

                // tmpLerpTime: 실제 보간용 float 변수
                // 각 인덱스마다 보간 값을 0에서 1.0으로 변환하기 위한 변수
                float tmpLerpTime = lerpTime - cardTime * i;

                // Ease Out 효과 주기
                // 0이하거나 1을 넘어가면 오류가 나기 때문에 범위 0부터 1까지만 지정하기
                float t = Mathf.Clamp01(tmpLerpTime);
                // 기본은 제곱으로 설정, 제곱 수 올릴 수록 처음에 빠르고, 마지막에 느려진다
                float easedOut = 1 - Mathf.Pow(1 - t, 2);

                // 위치 보간하여 이동하기
                tmpG[i].transform.position = Vector2.Lerp(StartPosA[i], endV2[i], easedOut); // 방식2
                                                                                             // 회전 보간하여 회전하기
                // 회전 보간하기, 0도에서 720도 회전, 실제로는 720도 - 카드 날리기 전 각도
                float thetaOffset = Mathf.Lerp(0f, 4 * Mathf.PI - EndTheta[i], easedOut); // 두 바퀴 회전 용
                float nextZ = (EndTheta[i] + thetaOffset) * Mathf.Rad2Deg;
                // 회전 적용
                tmpG[i].transform.rotation = Quaternion.Euler(0f, 0f, nextZ);
            }

            // lerpTime이 총 카드 배치 시간을 넘어가면 -1로 설정하여 Update() 안되게 하기
            if (lerpTime >= cardTotalTime)
            {
                setState = SetCard.End;
                // 시간 초 재생
                GameManager.instance.progress = GameProgress.StartGame;
                // 카드 배치가 끝났다면 애니메이션을 재생시키고, 카드 클릭 가능하게 하기
                for (int i = 0; i < cardCnt; i++)
                {
                    // 애니메이션 재생
                    tmpG[i].GetComponent<Card>().anim.speed = 1.0f;
                    // 카드 상태 배치 중에서 클릭 가능으로 변경
                    tmpG[i].GetComponent<Card>().cardState = Card.CardState.Ready;
                }
            }
            break;
            case SetCard.End:
            // Udate() 최소화
            break;
        default:
            break;
    }
}

3. 구현 영상

목차