TIL

[멋쟁이사자처럼 부트캠프 TIL 회고]

Cadi 2025. 2. 17. 17:46

 

오늘의 목표

01. 타이머 만들기

02. 하트 만들기

 

01. 타이머 만들기

 

내가 한 방식 : 게임매니저에 우겨넣기

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GamePanelController : MonoBehaviour
{
    public GameObject _firstQuizCardObject;
    private GameObject _secondQuizCardObject;
    private List<QuizData> _quizDataList;

    private int _lastGeneratedQuizIndex;
    private int _lastStageIndex;

    private GoyaTimer _currentTimer;
    private GoyaTimer _nextTimer;

    private void Start()
    {
        _lastStageIndex = UserInformations.LastStageIndex;
        InitQuizCards(_lastStageIndex);
    }

    private void InitQuizCards(int stageIndex)
    {
        _quizDataList = QuizDataController.LoadQuizData(stageIndex);

        _firstQuizCardObject = ObjectPool.Instance.GetObject();
        _firstQuizCardObject.GetComponent<QuizCardController>()
            .SetQuiz(_quizDataList[0], 0, OnCompletedQuiz);

        _secondQuizCardObject = ObjectPool.Instance.GetObject();
        _secondQuizCardObject.GetComponent<QuizCardController>()
            .SetQuiz(_quizDataList[1], 1, OnCompletedQuiz);

        SetQuizCardPosition(_firstQuizCardObject, 0);
        SetQuizCardPosition(_secondQuizCardObject, 1);

        _currentTimer = _firstQuizCardObject.GetComponentInChildren<GoyaTimer>();
        _currentTimer.onTimerEnd += ()=>GameOver();
        _currentTimer.StartTimer();
        _nextTimer = _secondQuizCardObject.GetComponentInChildren<GoyaTimer>();
        _nextTimer.ResetTimer();

        // 마지막으로 생성된 퀴즈 인덱스
        _lastGeneratedQuizIndex = 1;
    }

    private void OnCompletedQuiz(int cardIndex)
    {
        if (cardIndex >= Constants.MAX_QUIZ_COUNT - 1)
        {
            if (_lastStageIndex >= Constants.MAX_STAGE_COUNT - 1)
            {
                // TODO: 올 클리어 연출

                GameManager.Instance.QuitGame();
                _currentTimer.PauseTimer();
            }
            else
            {
                // TODO: 스테이지 클리어 연출
                _lastStageIndex += 1;
                InitQuizCards(_lastStageIndex);
                return;
            }
        }

        ChangeQuizCard();
    }

    private void SetQuizCardPosition(GameObject quizCardObject, int index)
    {
        var quizCardTransform = quizCardObject.GetComponent<RectTransform>();
        if (index == 0)
        {
            quizCardTransform.anchoredPosition = new Vector2(0, 0);
            quizCardTransform.localScale = Vector3.one;
            quizCardTransform.SetAsLastSibling();
        }
        else if (index == 1)
        {
            quizCardTransform.anchoredPosition = new Vector2(0, 160);
            quizCardTransform.localScale = Vector3.one * 0.9f;
            quizCardTransform.SetAsFirstSibling();
        }
    }

    private void ChangeQuizCard()
    {
        if (_lastGeneratedQuizIndex >= Constants.MAX_QUIZ_COUNT) return;
        _currentTimer.PauseTimer();
        _currentTimer.ResetTimer();
        _currentTimer.onTimerEnd -= ()=>GameOver();

        var temp = _firstQuizCardObject;
        _firstQuizCardObject = _secondQuizCardObject;
        _secondQuizCardObject = ObjectPool.Instance.GetObject();

        if (_lastGeneratedQuizIndex < _quizDataList.Count - 1)
        {
            _lastGeneratedQuizIndex++;
            _secondQuizCardObject.GetComponent<QuizCardController>()
                .SetQuiz(_quizDataList[_lastGeneratedQuizIndex], _lastGeneratedQuizIndex, OnCompletedQuiz);
        }

        SetQuizCardPosition(_firstQuizCardObject, 0);
        
        _currentTimer = _firstQuizCardObject.GetComponentInChildren<GoyaTimer>();
        _currentTimer.ResetTimer();
        _currentTimer.StartTimer();
        _currentTimer.onTimerEnd += ()=>GameOver();
        _nextTimer = _secondQuizCardObject.GetComponentInChildren<GoyaTimer>();
        if (_nextTimer != null)
        {
            _nextTimer.ResetTimer();
        }
        SetQuizCardPosition(_secondQuizCardObject, 1);

        ObjectPool.Instance.ReturnObject(temp);
    }

    public void GameOver()
    {
        Debug.Log("Game Over");
        _currentTimer.PauseTimer();
        _currentTimer.onTimerEnd -= ()=>GameOver();
    }
}

 

게임 매니저야 '해줘' 라고 말하는 듯한...

중간에 어려웠던 문제가 있었다. 

InitQuizCard하면서 두 번째 카드가 항상 있을줄 알았더니, 

 

  if (_lastGeneratedQuizIndex < _quizDataList.Count - 1)
        {
            _lastGeneratedQuizIndex++;
            _secondQuizCardObject.GetComponent<QuizCardController>()
                .SetQuiz(_quizDataList[_lastGeneratedQuizIndex], _lastGeneratedQuizIndex, OnCompletedQuiz);
        }

요 부분을 보면 마지막에서 한 번 뒤로 갔을 때, 그러니까 각 스테이지의 마지막 문제에서는 다음 퀴즈카드 오브젝트가

설정되지 않는다. 따라서 그냥 다음 타이머를 설정하려고 하면 오류가 발생한다. 

_nextTimer = _secondQuizCardObject.GetComponentInChildren<GoyaTimer>();
if (_nextTimer != null)
{
    _nextTimer.ResetTimer();
}

 

이렇게 예외 처리를 해 주었다. 

 

여전히 문제가 있다. 

스테이지 2로 갔을 때, 뒤쪽으로는 타이머가 나타나지 않는다. 

스테이지 1도 타이머가 나타나지 않던가, 스테이지 2도 뒤쪽에 타이머가 나타나던가 둘 중 하나로 통일을 해야 한다.

 

문제가 발생하는 이유는 오브젝트 풀에서 계속해서 새로운 카드들이 생성되기 떄문. 

 

 

이는 스테이지 넘어갈 때에 ReturnCard를 해주지 않았기 때문이다.

 else
            {
                // TODO: 스테이지 클리어 연출
                _lastStageIndex += 1;
                 ObjectPool.Instance.ReturnObject(_firstQuizCardObject);
                 ObjectPool.Instance.ReturnObject(_secondQuizCardObject);
                InitQuizCards(_lastStageIndex);
                return;   
            }

요렇게 스테이지 넘어갈 때 카드를 오브젝트 풀에 다시 넣어주면 된다. 

 

 

 

 

 

강사님이 하신 방식, 확장성이 높게 QuizCard에 넣어 놓은 방식

 

우선 타이머 자체에 타이머 시간이 다 된다면 실행되는 Delegate를 만들어 둔다. 

 

public delegate void GoyaTimerDelegate();
public GoyaTimerDelegate OnTimeout;

 

if (!_isPaused)
{
    CurrentTime += Time.deltaTime;
    if (CurrentTime >= totalTime)
    {
        headCapImage.gameObject.SetActive(false);
        tailCapImage.gameObject.SetActive(false);
        _isPaused = true;
        
        OnTimeout?.Invoke();
    }

 

 

그리고 카드 프리팹에 각각의 타이머를 등록해주고

[SerializeField] private Timer timer;

 

각각의 TimeOut함수에 패널을 끄고 키는 함수를 등록한다.

private void Start()
{
    timer.OnTimeout = () =>
    {
        // TODO: 오답 연출
        SetQuizCardPanelActive(QuizCardPanelType.InCorrectBackPanel);
    };
}

 

이것까지는 원래 있었던, 오답 패널을 껐다 키는는 부분이기 때문에 타이머가 돌아가는 것과는 관련이 없다.

타이머를 멈추고, 다시 리셋하는 기능을 추가해 주어야 한다. 

 

public void SetVisible(bool isVisible)
{
    if (isVisible)
    {
        timer.InitTimer();
        timer.StartTimer();
    }
    else
    {
        timer.InitTimer();
    }
}

 

이렇게 타이머를 초기화하고, 시작할 수 있는 함수를 만들어준다. 

 

private void SetQuizCardPosition(GameObject quizCardObject, int index)
{
    var quizCardTransform = quizCardObject.GetComponent<RectTransform>();
    if (index == 0)
    {
        quizCardTransform.anchoredPosition = new Vector2(0, 0);
        quizCardTransform.localScale = Vector3.one;
        quizCardTransform.SetAsLastSibling();
        
        quizCardObject.GetComponent<QuizCardController>()
            .SetVisible(true);
    }
    else if (index == 1)
    {
        quizCardTransform.anchoredPosition = new Vector2(0, 160);
        quizCardTransform.localScale = Vector3.one * 0.9f;
        quizCardTransform.SetAsFirstSibling();
        
        quizCardObject.GetComponent<QuizCardController>()
            .SetVisible(false);
    }
}

 

다른 함수에서 불러주면 된다. ( 이 함수는 카드가 넘겨질 때마다 불림)

첫 번째 카드는 SetVisible(true), 두 번째 카드는 SetVisible(false)로 해 주면 된다. 

 

 

02. 하트 만들기

 

ContentSizeFilter - 역할 알아보기

텍스트가 늘어나면 같이 늘어남

 

하트 컨트롤러를 만들 것이다.

 

하트가 늘어나는 것이나 줄어들 때 애니메이션 관련한 것.

DoTween을 사용해서 할 것이다. 

[SerializeField] private GameObject _heartRemoveImageObject;
[SerializeField] private TMP_Text _heartCountText;

RemoveImageObject는 하트가 커지면서 연해져 사라지는 것처럼 보이게 할 이미지이고

heartCountText는 표시될 하트의 숫자이다. 

 

public void InitHeartCount(int heartCount)
{
    _heartCount = heartCount;
    _heartCountText.text = _heartCount.ToString();
}

 

우선, 처음 하트의 개수를 설정할 수 있는 함수이다. 

public void RemoveHeart()
{
    // 하트 사라지는 연출
    _heartRemoveImageObject.SetActive(true);
    _heartRemoveImageObject.transform.localScale = Vector3.zero;
    _heartRemoveImageObject.GetComponent<Image>().color = Color.white;
    
    _heartRemoveImageObject.transform.DOScale(3f, 1f);
    _heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
    
    // 하트 수 텍스트 떨어지는 연출
    DOVirtual.DelayedCall(1.5f, () =>
    {
        _heartCountText.rectTransform.DOAnchorPosY(-40f, 1f);
        _heartCountText.DOFade(0f, 1f).OnComplete(() =>
        {
            // 하트 개수 감소
            _heartCount--;
            _heartCountText.text = _heartCount.ToString();
            
            var textLength = _heartCountText.text.Length;
            GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);

            _heartCountText.rectTransform.DOAnchorPosY(40f, 0f);
            _heartCountText.rectTransform.DOAnchorPosY(0, 1f);
            _heartCountText.DOFade(1f, 1f);
        });
    });
    
}

 

이런 식으로 사라지는 연출은 RemvoeImage를 커지게 하면서 알파값을 조정시켜 해결하고

그 후에 텍스트가 떨어지며 사라지게 한뒤 완료되면 다시 위에서 나타나게 한다. (줄어든 숫자로)

 

* DoTween에는 Sequence라는 것이 있음

Sequence는 트윈들의 집합이며 실행 순서 제어, 간격 조절, 반복 및 루핑, 그룹화 등 다양한 기능을 사용할 수 있다.

  • DOTween.Squence() : 시퀸스 생성
  • Append(), Join(), Insert()  : 시퀸스에 트윈을 추가
    Append() : 시퀸스에 트윈을 순차적으로 추가
    Join() : 시퀸스에 트윈을 동시에 추가
    Insert(): 특정 시간 후에 트윈이 실행되도록 시퀸스에 추가
  • SetLoops(), SetDelay() : 반복 횟수, 지연 시간 등 설정
  • AppendCallback : 시퀸스에 콜백 함수를 추가 시퀀스가 해당 지점에 도달했을 때 실행

 

나중에 이 모든 애니메이션이 끝나고 다음 패널을 넘기든, 무언가 다른 행위를 하든 했으면 좋겠어서 델리게이트를

등록해 주었다. 

 

public void RemoveHeart(Action onComplete)
{
    // 효과음 재생
    if (UserInformations.IsPlaySFX)
        _audioSource.PlayOneShot(_heartRemoveAudioClip);

    // 하트 사라지는 연출
    _heartRemoveImageObject.SetActive(true);
    _heartRemoveImageObject.transform.localScale = Vector3.zero;
    _heartRemoveImageObject.GetComponent<Image>().color = Color.white;

    _heartRemoveImageObject.transform.DOScale(3f, 1f);
    _heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);

    // 하트 수 텍스트 떨어지는 연출
    DOVirtual.DelayedCall(1f, () => { ChangeTextAnimation(false, onComplete); });
}
public void ChangeTextAnimation(bool isAdd, Action onComplete = null)
{
    float duration = 0.2f;
    float yPos = 40f;

    heartCountText.rectTransform.DOAnchorPosY(-yPos, duration);
    heartCountText.DOFade(0, duration).OnComplete(() =>
    {
        if (isAdd)
        {
            var currentHeartCount = heartCountText.text;
            heartCountText.text = (int.Parse(currentHeartCount) + 1).ToString();
        }
        else
        {
            var currentHeartCount = heartCountText.text;
            heartCountText.text = (int.Parse(currentHeartCount) - 1).ToString();
        }

        // Heart Panel의 Width를 글자 수에 따라 변경
        var textLength = heartCountText.text.Length;
        GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);

        // 새로운 하트 수 추가 애니메이션
        heartCountText.rectTransform.DOAnchorPosY(yPos, 0);
        heartCountText.rectTransform.DOAnchorPosY(0, duration);
        heartCountText.DOFade(1, duration).OnComplete(() => { onComplete?.Invoke(); });
    });
}

 

기본 값은 null로 잡아주어 없어도 실행되게 했다. 

 

public void OnClickRetryQuizButton()
{
    if (GameManager.Instance.heartCount > 0)
    {


        heartPanel.heartCountText.text = GameManager.Instance.heartCount.ToString();
        heartPanel.RemoveHeart(() =>
        {
            GameManager.Instance.heartCount--;
            Debug.Log($"현재 GameManager의 heartCount 수{GameManager.Instance.heartCount}");
            SetQuizCardPanelActive(QuizCardPanelType.Front);

            // 타이머 초기화 및 시작
            timer.InitTimer();
            timer.StartTimer();
        });

       
    }
    else
    {
        // 하트가 부족해서 다시도전w 불가
        heartPanel.EmptyHeart();
        // TODO: 하트 부족 알림
    }
}

 

실행은 다시하기 버튼으로 실행한다. 

 

하트의 개수 관련해서는, 지금 총 3가지의 저장 혹은 표현하는 방식이 있다.

 

1. UserInformation의 HeartCount

2. GameManager의 heartCount

3. Text로 표현될 heartCountText 

 

이 세 가지를 헷갈려서 좀 고생했다. 

 

우선 중심이 될 것은 GameManager의 heartCount이다.  

계속해서 UserInformation을 고치는 것은 비효율적이니 GameManager인 것을 계속해서 변경하다

종료 시에만 바꿔줄 것이다. 

 

위에서 리트라이 버튼을 누르면 하트가 하나 주는 것까지는 봤다. 

private void OnApplicationQuit()
{
    Debug.Log("OnApplicationQuit!!");
    UserInformations.HeartCount = heartCount;
}

 

OnApplicationQuit() 에서 현재 GameManager의 heartCoiunt를 저장해준다. 

그리고 

public void OnClickRetryQuizButton()
{
    if (GameManager.Instance.heartCount > 0)
    {


        heartPanel.heartCountText.text = GameManager.Instance.heartCount.ToString();
        heartPanel.RemoveHeart(() =>
        {
            GameManager.Instance.heartCount--;
            Debug.Log($"현재 GameManager의 heartCount 수{GameManager.Instance.heartCount}");
            SetQuizCardPanelActive(QuizCardPanelType.Front);

            // 타이머 초기화 및 시작
            timer.InitTimer();
            timer.StartTimer();
        });

 

누를 때마다 text를 업데이트 해 주고 줄여준다. ( 어쩌피 줄어든 이후 바로 사라질 패널)