TIL

[멋사 부트캠프 TIL 회고] 퀴즈게임 마무리단계(1)

Cadi 2025. 2. 19. 20:15

 

 

오늘 배운 것

1. GamePanel이 컨트롤하지 않는, QuizCardController에 카드 관련 작업 위임.

2. 1을 바탕으로 애니메이션 제작

3. 각자 완성시켜보기. 

 

01. GamePanel이 컨트롤하지 않는, QuizCardController에 카드 관련 작업 위임.

public interface IQuizCardPositionState
{
    void Trasition(bool withAnimation, Action onComplete = null);
}

// 퀴즈 카드의 위치 상태 전이를 관리할 목적
public class QuizCardPositionStateContext
{
    private IQuizCardPositionState _currentState;

    public void SetState(IQuizCardPositionState state, bool withAnimation, Action onComplete = null)
    {
        if (_currentState == state) return;
        
        _currentState = state;
        _currentState.Trasition(withAnimation, onComplete);
    }
}

public class QuizCardPositionState
{
    protected QuizCardController _quizCardController;
    protected RectTransform _rectTransform;
    protected CanvasGroup _canvasGroup;

    public QuizCardPositionState(QuizCardController quizCardController)
    {
        _quizCardController = quizCardController;
        _rectTransform = _quizCardController.gameObject.GetComponent<RectTransform>();
        _canvasGroup = _quizCardController.gameObject.GetComponent<CanvasGroup>();
    }
}

// 퀴즈 카드가 첫 번째 위치에 나타날 상태를 처리할 상태 클래스
public class QuizCardPositionStateFirst: QuizCardPositionState, IQuizCardPositionState
{
    public QuizCardPositionStateFirst(QuizCardController quizCardController) : base(quizCardController) { }

    public void Trasition(bool withAnimation, Action onComplete = null)
    {
        var animationDuration = (withAnimation) ? 0.2f : 0f;
        
        _rectTransform.DOAnchorPos(Vector2.zero, animationDuration);
        _rectTransform.DOScale(1f, animationDuration);
        _canvasGroup.DOFade(1f, animationDuration).OnComplete(() => onComplete?.Invoke());

        _rectTransform.SetAsLastSibling();
    }
}
// 퀴즈 카드가 두 번째 위치에 나타날 상태를 처리할 상태 클래스
public class QuizCardPositionStateSecond: QuizCardPositionState, IQuizCardPositionState
{
    public QuizCardPositionStateSecond(QuizCardController quizCardController) : base(quizCardController) { }

    public void Trasition(bool withAnimation, Action onComplete = null)
    {
        var animationDuration = (withAnimation) ? 0.2f : 0f;
        
        _rectTransform.DOAnchorPos(new Vector2(0f, 160f), animationDuration);
        _rectTransform.DOScale(0.9f, animationDuration);
        _canvasGroup.DOFade(0.7f, animationDuration).OnComplete(() => onComplete?.Invoke());

        _rectTransform.SetAsFirstSibling();
    }
}
// 퀴즈 카드가 사라질 상태를 처리할 상태 클래스
public class QuizCardPositionStateRemove: QuizCardPositionState, IQuizCardPositionState
{
    public QuizCardPositionStateRemove(QuizCardController quizCardController) : base(quizCardController) { }

    public void Trasition(bool withAnimation, Action onComplete = null)
    {
        var animationDuration = (withAnimation) ? 0.2f : 0f;
        _rectTransform.DOAnchorPos(new Vector2(0f, -280f), animationDuration);
        _canvasGroup.DOFade(0f, animationDuration).OnComplete(() => onComplete?.Invoke());
    }
}

 

 

퀴즈 카드 컨트롤러에 들어갈 것

각각의 상태들을 만들고, 그 상태들의 Transition을 호출했을 때 그 상태에 맞는 위치로 올믹는 애니메이션을 재생하고

받아온 onComplete 함수를 호출한다. 

private IQuizCardPositionState _positionStateFirst;
private IQuizCardPositionState _positionStateSecond;
private IQuizCardPositionState _positionStateRemove;
private QuizCardPositionStateContext _positionStateContext;

private void Awake()
{
    // 숨겨진 패널의 좌표 저장
    _correctBackPanelPosition = correctBackPanel.GetComponent<RectTransform>().anchoredPosition;
    _incorrectBackPanelPosition = incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition;
    
    //상태 관리를 위한 Context 객체 생성 -- 추가
    _positionStateContext = new QuizCardPositionStateContext();
    
    // this는 현재 퀴즈카드 컨트롤러를 전달.
    _positionStateFirst = new QuizCardPositionStateFirst(this);
    _positionStateSecond = new QuizCardPositionStateSecond(this);
    _positionStateRemove = new QuizCardPositionStateRemove(this);
    
    //애니메이션 없이 Remove상태로 바뀔 수 있게 초기화
    _positionStateContext.SetState(_positionStateRemove, false);


}

 

앞에서는 클래스를 선언만 했기 때문에 함수를 기능시키기 위해서 퀴즈 카드가 생길 때 상태 클래스 3개를 생성해준다.

그리고 생성자에 퀴즈카드 스테이트를 넣어줘 동자갛게 한다. 

 

앞에 있는 것만 보이게 해 주었던

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

다음 코드도 통합할 것이다. (바로 밑의 코드에 통합될 예정이다)

 

저 위의 세 상태를 묶어줄 주고, 호출을 쉽게 해 줄 코드는 다음과 같다. 

public void SetQuizCardPosition(QUizCardPositionType quizCardPositionType, bool withAnimation, Action onComplete = null)
{
    switch (quizCardPositionType)
    {
        case QUizCardPositionType.First:
            _positionStateContext.SetState(_positionStateFirst, withAnimation);
            break;
        case QUizCardPositionType.Second:
            _positionStateContext.SetState(_positionStateSecond, withAnimation);
            break;
        case QUizCardPositionType.Remove:
            _positionStateContext.SetState(_positionStateRemove, withAnimation);
            break;
        
    }
}

 

 

원래 코드와 비교해보자면 ! 

원래는 이런 식으로, 게임 패널 컨트롤러에서  카드를  직접 움직였다. 

if (_quizCardQueue.Count > 0)
{
    // 2. 가장 첫 번째 오브젝트를 Peek 해서 사이즈와 위치 조절하고
    var firstQuizCardObject = _quizCardQueue.Peek();
    firstQuizCardObject.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, 0);
    firstQuizCardObject.transform.localScale = Vector3.one;
    firstQuizCardObject.transform.SetAsLastSibling();
    firstQuizCardObject.GetComponent<QuizCardController>().SetVisible(true);
}

이제는 퀴즈 카드 컨트롤러 자체에서 움직임을 갖고 있고(상태에 따른) 우리는 게임패널 컨트롤러에서 간단하게 SetQuizCardPosition만 불러줄 뿐, 위치나 투명도 등은 퀴즈 카드 컨트롤러에 위임한다. 

//변경한 이유, SetQuizCardPosition함수를 사용하기 위해
private Queue<QuizCardController> _quizCardQueue = new Queue<QuizCardController>();

public void AddQuizCardObject(QuizData? quizData, bool isInit = false)
{
    //원래 있었던, 빼낼 퀴즈 카드를 저장하는 컨트롤러 객체
    QuizCardController tempQuizCardController = null;
    
    
    // 1단계 : First 영역의 카드 제거
    void RemoveFirstQuizCard(Action onComplete = null)
    {
        tempQuizCardController = _quizCardQueue.Dequeue();
        tempQuizCardController.SetQuizCardPosition(QuizCardController.QUizCardPositionType.Remove,true, onComplete);
    }
    
    // 2단계 : Second 영역의 카드를 First 영역으로 이동
    void SecondQuizCardToFirst(Action onComplete = null)
    {
        var firstQuizCardController = _quizCardQueue.Peek();
        firstQuizCardController.SetQuizCardPosition(QuizCardController.QUizCardPositionType.First, true, onComplete);
    }
    
    // 3단계 : 새로운 퀴즈 카드를 Second 영역에 생성
    void AddNewQuizCard(Action onComplete = null)
    {
        if (quizData.HasValue)
        {
            var quizCardObject = ObjectPool.Instance.GetObject();
            var quizCardController = quizCardObject.GetComponent<QuizCardController>();
            quizCardController.SetQuiz(quizData.Value, OnCompletedQuiz);
            _quizCardQueue.Enqueue(quizCardController);
            quizCardController.SetQuizCardPosition(QuizCardController.QUizCardPositionType.Second, ture, onComplete);
        }
    }
}

 

 

이제 조건문을 토대로 카드를 생성, 넘겨준다.

if (_quizCardQueue.Count > 0)
{
    if (isInit)
    {
        SecondQuizCardToFirst();
        AddNewQuizCard();
    }
    else
    {
        RemoveFirstQuizCard(()=>
        {
            SecondQuizCardToFirst(() =>
            {
                AddNewQuizCard(() =>
                {
                    if ( tempQuizCardController != null)
                        ObjectPool.Instance.ReturnObject(tempQuizCardController.gameObject);
                });
            });
        });
       
    }
}
// 하나도 없다 : 만들어진 퀴즈가 없다. 
else
{
    
}

 

동작은 똑같다.

02.  1을 바탕으로 애니메이션 제작

 

 

새로운 스테이트인 flip 상태를추가했다

미처 생각하지 못한 방식이었는데 상당히 신기했다.

public class QuizCardFlip : QuizCardPositionState, IQuizCardPositionState
{
    public QuizCardFlip(QuizCardController quizCardController) : base(quizCardController)
    {
    }


    public void Trasition(bool withAnimation, Action onComplete = null)
    {
        var animationDuration = (withAnimation) ? 0.2f : 0f;
        _rectTransform.DORotate(new Vector3(0f, 90f, 0f),
            animationDuration / 2).OnComplete(() =>
        {
            _rectTransform.DORotate(new Vector3(0f, -90f, 0f), 
                    animationDuration / 2)
                .OnComplete(() => onComplete?.Invoke());
        });
    }
}

 

노가다로 풀던 나와는 다르신 모습...... 

퀴즈 카드 객체에서 자동으로 돌아버린다. (마찬가지로 SetQuizCardPosition에 넣고 호출함)

 
private void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType)
{
    switch (quizCardPanelType)
    {
        case QuizCardPanelType.Front:
            frontPanel.SetActive(true);
            correctBackPanel.SetActive(false);
            incorrectBackPanel.SetActive(false);

            correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
            incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
            break;
        case QuizCardPanelType.CorrectBackPanel:
            frontPanel.SetActive(false);
            incorrectBackPanel.SetActive(false);
            _positionStateContext.SetState(_positionStateFlip, true,() =>
            {
                correctBackPanel.SetActive(true);
                correctBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
                incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;

            });

           
            break;
        case QuizCardPanelType.InCorrectBackPanel:
            frontPanel.SetActive(false);
            correctBackPanel.SetActive(false);

            _positionStateContext.SetState(_positionStateFlip, true, () =>
            {
                incorrectBackPanel.SetActive(true);
                correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
                incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;

            });
            
            break;
    }
}

 

 

잘 되는줄 알았으나 버그가 있다. 

 

 

 

현재 코드에서 

public void SetState(IQuizCardPositionState state, bool withAnimation, Action onComplete = null)
{
    if (_currentState == state) return;

    _currentState = state;
    _currentState.Trasition(withAnimation, onComplete);
}

 

같은 상태이면 return 해 버리는  문제가 있다. 따라서 flip을 할 때에 currentState를 flip 상태로 설정해 놨기 때문에,

이를 바꿔주지 않으면 flip을 다시 할 수 없다. 

 

그래서 이름만 다른 상태를 하나 만들어준다.

public class QuizCardFlipNormal : QuizCardPositionState, IQuizCardPositionState
{
    public QuizCardFlipNormal(QuizCardController quizCardController) : base(quizCardController)
    {
    }


    public void Trasition(bool withAnimation, Action onComplete = null)
    {
        var animationDuration = (withAnimation) ? 0.2f : 0f;
        _rectTransform.DORotate(new Vector3(0f, 90f, 0f),
            animationDuration / 2).OnComplete(() =>
        {
            _rectTransform.DORotate(new Vector3(0f, 0f, 0f), 
                    animationDuration / 2)
                .OnComplete(() => onComplete?.Invoke());
        });
    }
}

 

밑을 올바르게 바꿔주면 Clear  ~

private void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType , bool withAnimaiton = true)
{
    switch (quizCardPanelType)
    {
        case QuizCardPanelType.Front:
            correctBackPanel.SetActive(false);
            incorrectBackPanel.SetActive(false);
            _positionStateContext.SetState(_positionStateFlipNormal, withAnimaiton, () =>
            {
                frontPanel.SetActive(true);
                correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
                incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;

            } );
            
            break;
        case QuizCardPanelType.CorrectBackPanel:
            frontPanel.SetActive(false);
            incorrectBackPanel.SetActive(false);
            _positionStateContext.SetState(_positionStateFlip, withAnimaiton,() =>
            {
                correctBackPanel.SetActive(true);
                correctBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
                incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;

            });

           
            break;
        case QuizCardPanelType.InCorrectBackPanel:
            frontPanel.SetActive(false);
            correctBackPanel.SetActive(false);

            _positionStateContext.SetState(_positionStateFlip, withAnimaiton, () =>
            {
                incorrectBackPanel.SetActive(true);
                correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
                incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;

            });
            
            break;
    }
}

 

이렇게 front로 갈 때에 항상 다른 State로 설정을 할 수 있게 해 두어야 한다. 

 


 

 

 

03. 각자 디테일 수정

 

 

levelPanel (스테이지 정보 표시) 애니메이션 제작

private void InitQuizCards(int stageIndex)
{
    levelPanel.SetActive(true);

    RectTransform rectTransform = levelPanel.GetComponent<RectTransform>();
    Image image = levelPanel.GetComponent<Image>();
    Sequence levelPanelsequence = DOTween.Sequence();

    rectTransform.localScale = Vector3.one * 1.5f;
    image.color = new Color(1f, 1f, 1f, 0f);
    
    
    levelPanelsequence.Append(rectTransform.DOScale(Vector3.one, 1f).SetEase(Ease.OutBack))
        .Join(image.DOFade(1f, 1f));

    levelPanelsequence.AppendInterval(1f);

    levelPanelsequence.Append(rectTransform.DOScale(Vector3.one * 1.5f, 1f))
        .Join(image.DOFade(0, 1f));


    levelPanelsequence.OnComplete(() =>
    {
        _quizDataList = QuizDataController.LoadQuizData(stageIndex);

        AddQuizCardObject(_quizDataList[0], true);
        AddQuizCardObject(_quizDataList[1], true);
        levelPanel.SetActive(false);
    });
    levelPanelsequence.Play();

 

OnClickOptionButton( 정답과 오답에 따른 애니메이션 처리) 제작

public void OnClickOptionButton(int buttonIndex)
{

    //var button = optionButtons[buttonIndex];
    var buttons = this.gameObject.GetComponentsInChildren<Button>();
    var button = buttons[buttonIndex];
    // Timer 일시 정시
    timer.PauseTimer();

    if (buttonIndex == _answer)
    {
        Debug.Log("정답!");
        button.GetComponent<Image>().color = Color.green;

        button.transform.DOPunchScale(Vector3.one * 0.2f, 1f).OnComplete(() =>
        {
            SetQuizCardPanelActive(QuizCardPanelType.CorrectBackPanel);
            button.GetComponent<Image>().color = Color.white;
        });

    }
    else
    {
        Debug.Log("오답~");
        button.GetComponent<Image>().color = Color.red;
        
        button.transform.DOShakePosition(1f,10f,10,90).OnComplete(() =>
        {
            SetQuizCardPanelActive(QuizCardPanelType.InCorrectBackPanel);
            button.GetComponent<Image>().color = Color.white;
        });

    }
}

 

 

HeartPanel을 재사용해 RetryButton에 접목

public void OnClickRetryQuizButton()
{
    if (GameManager.Instance.heartCount > 0)
    {
        _heartPanel.RemoveHeart(() =>
        {
            SetQuizCardPanelActive(QuizCardPanelType.Front);

            // 타이머 초기화 및 시작
            timer.InitTimer();
            timer.StartTimer();
            
            //하트 패널 부숴라.
            Destroy(_heartPanel.gameObject);
        });
        //heartCountText.text = GameManager.Instance.heartCount.ToString();

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

 

 


 

 

 

 

 

 

 

 

오늘 헤맸던 것

 

_rectTransform.DORotate(new Vector3(0f, 0f, 0f), 

충격 : 어제 안되던 이유 

90 - 90하면 0이 될 것이라는 '안일한 생각' 때문이다. 

저 DO 뒤에 나오는 숫자들은 전부 '목표값' 이라고 생각하면 편하다.

 

 

오늘의 목표

1. 코딩테스트 2문제 풀기

2. 비동기 관련 정리

3. 구글 플레이 콘솔, AdMob 관련 실행