TIL

[멋쟁이사자처럼 부트캠프 TIL 회고] 유니티 게임개발 3기 55일차 퀴즈게임 제작4

HYttt 2025. 2. 18. 01:10

퀴즈게임 제작 

퀴즈카드에 타이머 추가

퀴즈가 시작되면 타이머 작동

뒤에 있는 카드는 타이머 작동하지 않도록

  • SetVisible로 첫번쨰 카드만 타이머가 작동하도록 구현
  • 보기를 클릭하면 타이머 일시정지
public struct QuizData
{
    public string question;
    public string description;
    public int type;
    public int answer;
    public string firstOption;
    public string secondOption;
    public string thirdOption;
}

public class QuizCardController : MonoBehaviour
{
    // Timer
    [SerializeField] private Timer timer;
    
    public void SetVisible(bool isVisible)
    {
        if (isVisible)
        {
            timer.InitTimer();
            timer.StartTImer();
        }
        else
        {
            timer.InitTimer();
        }
    }
    
    public void OnClickOptionButton(int buttonIndex)
    {
        timer.PauseTimer();
        
        if (buttonIndex == _answer)
        {
            Debug.Log("정답");
            // Todo : 정답 연출
            
            SetQuizPanelActive(QuizCardPanelType.CorrectBack);
        }
        else
        {
            Debug.Log("오답");
            // Todo : 오답 연출
            
            SetQuizPanelActive(QuizCardPanelType.IncorrectBack);
        }
    }
}
// GamePanelController

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

타이머 종료시 오답처리

  • 델리게이트로 타이머가 종료되면 알림
  • QuizCardController에서 타이머가 종료되면 오답패널을 띄우도록 구독
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;

public class Timer : MonoBehaviour
{
   [SerializeField] private Image fillImage;
    [SerializeField] private float totalTime;
    [SerializeField] private Image headCapImage;
    [SerializeField] private Image tailCapImage;
    [SerializeField] private TMP_Text timeText;
    
    public float CurrentTime { get; private set; }
    private bool _isPaused;

    public delegate void TimerDelegate();
    public TimerDelegate OnTimeout;
    
    private void Awake()
    {
        _isPaused = true;
    }
    private void Update()
    {
        if(!_isPaused)
        {
            CurrentTime += Time.deltaTime;
            if(CurrentTime >= totalTime)
            {
                headCapImage.gameObject.SetActive(false);
                tailCapImage.gameObject.SetActive(false);
                _isPaused = true;
                OnTimeout?.Invoke();
            }
            else
            {
                fillImage.fillAmount = 1 - CurrentTime / totalTime;
                headCapImage.transform.localRotation = Quaternion.Euler(new Vector3(0,0,-fillImage.fillAmount * 360));
                var timeTextTime = totalTime - CurrentTime;
                timeText.text = timeTextTime.ToString("F0");
            }
        }
    }

    public void StartTImer()
    {
        _isPaused = false;
    }
    public void PauseTimer()
    {
        _isPaused = true;
    }
    public void InitTimer()
    {
        CurrentTime = 0;
        fillImage.fillAmount = 1;
        timeText.text = totalTime.ToString("F0");
        headCapImage.gameObject.SetActive(true);
        tailCapImage.gameObject.SetActive(true);
        _isPaused = true;
    }
}
public struct QuizData
{
    public string question;
    public string description;
    public int type;
    public int answer;
    public string firstOption;
    public string secondOption;
    public string thirdOption;
}

public class QuizCardController : MonoBehaviour
{
    // Timer
    [SerializeField] private Timer timer;
    
    private void Start()
    {
        timer.OnTimeout += () =>
        {
            SetQuizPanelActive(QuizCardPanelType.IncorrectBack);
        };
    }
    
    public void SetVisible(bool isVisible)
    {
        if (isVisible)
        {
            timer.InitTimer();
            timer.StartTImer();
        }
        else
        {
            timer.InitTimer();
        }
    }
}

퀴즈카드에 하트패널 추가

Remove, Add, Empty 애니메이션, 사운드 추가

using System;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(AudioSource))]
public class HeartPanelController : MonoBehaviour
{
    [SerializeField] private GameObject heartRemoveImageObject;
    [SerializeField] private TMP_Text heartCountText;
    
    [SerializeField] private AudioClip heartRemoveAudioClip;
    [SerializeField] private AudioClip heartAddAudioClip;
    [SerializeField] private AudioClip heartEmptyAudioClip;
    
    private AudioSource _audioSource;
    
    private int _heartCount;
    
    // 1. 하트 추가 연출
    // 2. 하트 감소 연출
    // 3. 하트 부족 연출

    private void Awake()
    {
        _audioSource = GetComponent<AudioSource>();
    }

    private void Start()
    {
        heartRemoveImageObject.gameObject.SetActive(false);
        //InitHeartCount(10);
        InitHeartCount(UserInformations.HeartCount);
    }

    /// <summary>
    /// Heart Panel에 하트 수 초기화
    /// </summary>
    /// <param name="heartCount">하트 수</param>
    public void InitHeartCount(int heartCount)
    {
        _heartCount = heartCount;
        heartCountText.text = _heartCount.ToString();
    }

    private void ChangeTextAnimation(bool isAdd)
    {
        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();
            }
            
            var textLength = heartCountText.text.Length;
            GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);
            
            heartCountText.rectTransform.DOAnchorPosY(yPos, 0);
            heartCountText.rectTransform.DOAnchorPosY(0f, duration);
            heartCountText.DOFade(1, duration).OnComplete(() =>
            {
                
            });
        });
    }
    
    public void AddHeart(int heartCount)
    {
        Sequence sequence = DOTween.Sequence();

        for (int i = 0; i < 3; i++)
        {
            sequence.AppendCallback(() =>
            {
                ChangeTextAnimation(true);
                if (UserInformations.IsPlaySFX)
                {
                    _audioSource.PlayOneShot(heartAddAudioClip);
                }
            });
            sequence.AppendInterval(0.3f);
        }
    }
    
    public void EmptyHeart()
    {
        if (UserInformations.IsPlaySFX)
        {
            _audioSource.PlayOneShot(heartEmptyAudioClip);
        }
        GetComponent<RectTransform>().DOPunchPosition(new Vector3(20f, 0, 0), 1f, 7);
    }
    
    public void RemoveHeart()
    {
        // 하트 사라지는 연출
        if (UserInformations.IsPlaySFX)
        {
            _audioSource.PlayOneShot(heartRemoveAudioClip);
        }
        
        heartRemoveImageObject.gameObject.SetActive(true);
        heartRemoveImageObject.transform.localScale = Vector3.zero;
        heartRemoveImageObject.GetComponent<Image>().color = Color.white;
        
        heartRemoveImageObject.transform.DOScale(3f, 1f);
        heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
        
        ChangeTextAnimation(false);
    }
}

Incorrect Panel에 남은 하트 수 표시

  • 다시 도전 버튼을 클릭하면 하트 차감하고 Front Panel을 다시 보여줌
  • 애니메이션이 종료되면 화면이 전환되도록 델리게이트로 전달
[RequireComponent(typeof(AudioSource))]
public class HeartPanelController : MonoBehaviour
{
    public delegate void HeartPanelDelegate();

    public HeartPanelDelegate AnimationDone;
    
    private void ChangeTextAnimation(bool isAdd)
    {
        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();
            }
            
            var textLength = heartCountText.text.Length;
            GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);
            
            heartCountText.rectTransform.DOAnchorPosY(yPos, 0);
            heartCountText.rectTransform.DOAnchorPosY(0f, duration);
            heartCountText.DOFade(1, duration).OnComplete(() =>
            {
                AnimationDone?.Invoke();
            });
        });
    }
}