본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차

by Mobics 2025. 2. 27.

 

목차


    퀴즈 게임 만들기

    25.02.27

    게임 광고 넣기

    └ 전면 광고

    : 초기화는 지난 시간에 해뒀기 때문에 안 해도 된다.

     

    ※ 광고 시작 가이드

    : Google에 'Admob Unity SDK' 검색

    https://developers.google.com/admob/unity/quick-start?hl=ko

     

    시작하기  |  Unity  |  Google for Developers

    Unity에서 앱을 제작 중인 AdMob 게시자를 위한 모바일 광고 SDK입니다.

    developers.google.com

     

    >> AdmobAdsManager.cs에 전면 광고 코드 작성

     

    >> 'Main Panel'의 자식으로 'Interstitial Ad Test Button' 생성 후, OnClick()에 함수 바인딩

    : UI-Button으로 생성, 에디터 상에서 테스트를 하기 위함

     

    └ 보상형 광고

    >> AdmobAdsManager.cs에 전면 광고 코드 작성

     

    >> 'Main Panel'의 자식으로 'Interstitial Ad Test Button'을 복사하여 'Reward Ad Test Button'을 생성 후, OnClick()에 함수 바인딩

    : 버튼의 PosY만 살짝 아래로 내림

     

    모바일 환경에서 광고 테스트

    : 51일차 글을 참고하여 모바일에서 Build하자

     

    >> 모바일 환경에서 실행되는 Log 확인하는 방법

    : cmd에서 아래 명령어로 명령 실행 --> 다시 빠져나오려면 Ctrl + C

    adb logcat -s Unity

    --> 모바일에서 혹시 게임을 킨 상태라면 완전히 종료 시키고 logcat 실행, 이후 게임을 켜서 작동하면 Log를 볼 수 있다.

     

    ※ 에뮬도 구동 가능하다.

    ※ 'adb devices' 를 했을 때, 여러 Device가 나온다면 실행이 불가능하다. 1개만 남기자

     

    활동

    : Quiz Game 광고 적용 및 게임 완성 --> 강사님의 인싸퀴즈와 최대한 유사하게 만들기

    • 지금까지 제작한 Quiz Game의 파츠를 조합해서 게임을 완성해주세요.
    • 배너 광고, 전면 광고, 보상형 광고를 적용해 주세요
    • 게임이 종료 되었거나 다시 실행했을 때 마지막 스테이지와 하트 정보가 유지되게 만들어 주세요

     

    >> 우선 현재 있는 버그 해결

    1. 게임을 시작하면 Quiz 내용이 안 나오고 보기 버튼이 안 눌러진다. --> 다시 도전을 하면 제대로 Quiz 내용이 표시된다. 정답을 맞추고 다음 문제로 넘어가면 처음과 동일한 문제가 발생한다.
    2. 'QuizCardIncorrect' Animation이 동작하면 QuizCard의 Rotation.Z가 0이 아니라 -10으로 고정된다. --> 강사님의 Project를 받아서 실행해봐도 동일한 현상이 발생한다. (강사님은 -15.7에 고정)

    --> 오른쪽 사진과 같이 기울어서 멈춰있게 된다, 다른 Animation들은 정상 작동

     

    1번 문제 발견

    : QuizCard가 생성됐을 때, 'Result Panel Correct'가 Active 상태로 나와 Front Panel보다 앞에 등장하여 Quiz Text도 가리고 Button도 안 눌리게 된 것

     

    1번 문제 해결

    : QuizCardController.cs의 SetQuiz() 함수에서 ShowQuizCardResult() 함수를 호출하지 않아서 Result Panel이 계속 SetActive 상태로 나온 것이었다.

    --> ShowQuizCardResult( QuizCardResultType.None) 으로 호출하여 해결

    // Quiz Card Result Panel
    ShowQuizCardResult(QuizCardResultType.None);

     

    2번 문제 발견

    : 강사님께 여쭤보니 Incorrect Animation의 첫 프레임이 이미 회전한 상태여서 그렇다고 하신다.

     

    2번 문제 해결

    : Animation의 첫 프레임을 Rotation.Z를 0으로 변경하고 'Curves'를 건드려 Animation을 자연스럽게 수정하여 해결

     

    >> 구현해야할 것 구상

    Main 화면

    • 남은 하트 수 표시
    • 다음 Stage가 몇 Level인지 표시
    • Play 버튼을 누르라고 물결 파동처럼 Animation

     

    └ 상점

    : 광고 연결

    --> 광고를 다 봤는지 확인 여부(변수 추가)에 따라 하트를 더하기

    --> 하트가 더해지는 Animation은 다른 곳에서 구현해야 한다?

     

    └ Stage

    : Level을 선택하여 게임을 시작할 때 몇 Level인지 잠깐 띄우는 팝업

     

    Game 화면

    : GameOver 시, 하트가 없다면 종료 버튼 외에 광고 보고 하트 3개 받기 버튼 추가

     

    최종 코드

    >> AdmobAdsManager.cs

    : 내 AdUnitID는 삭제

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using GoogleMobileAds.Api;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class AdmobAdsManager : Singleton<AdmobAdsManager>
    {
    #if UNITY_ANDROID
        private string _bannerAdUnitId = "";
        private string _interstitialAdUnitId = "";
        private string _rewardedAdUnitId = "";
    #elif UNITY_IOS
        private string _bannerAdUnitId = "ca-app-pub-3940256099942544/2934735716"; // 테스트 광고 ID
        private string _interstitialAdUnitId = "ca-app-pub-3940256099942544/4411468910"; // 테스트 광고 ID
        private string _rewardedAdUnitId = "ca-app-pub-3940256099942544/1712485313"; // 테스트 광고 ID
    #endif
    
        private BannerView _bannerView;
        private InterstitialAd _interstitialAd;
        private RewardedAd _rewardedAd;
        
        private void Start()
        {
            // SDK 초기화
            MobileAds.Initialize(initStatus =>
            {
                // 배너 광고 표시 --> 광고 제거 구매 여부를 확인 후 표시할지 말지 결정해야 한다.
                LoadBannerAd();
                
                // 전면 광고 표시
                LoadInterstitialAd();
                
                // 보상형 광고 표시
                LoadRewardedAd();
            });
        }
    
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
    
        #region Banner Ads
    
        public void CreateBannerView()
        {
            Debug.Log("Creating banner view");
    
            if (_bannerView != null)
            {
                // Banner View 소멸
                _bannerView.Destroy();
                _bannerView = null;
            }
    
            _bannerView = new BannerView(_bannerAdUnitId, AdSize.Banner, AdPosition.Bottom);
        }
    
        public void LoadBannerAd()
        {
            if (_bannerView == null)
            {
                CreateBannerView();
            }
    
            var adRequest = new AdRequest();
            
            _bannerView.LoadAd(adRequest);
            RegisterBannerAdsEventHandler();
        }
    
        private void RegisterBannerAdsEventHandler()
        {
            // Raised when an ad is loaded into the banner view.
            _bannerView.OnBannerAdLoaded += () =>
            {
                Debug.Log("Banner view loaded an ad with response : "
                          + _bannerView.GetResponseInfo());
            };
            
            // Raised when an ad fails to load into the banner view.
            _bannerView.OnBannerAdLoadFailed += (LoadAdError error) =>
            {
                Debug.LogError("Banner view failed to load an ad with error : "
                               + error);
            };
            // Raised when the ad is estimated to have earned money.
            _bannerView.OnAdPaid += (AdValue adValue) =>
            {
                Debug.Log(String.Format("Banner view paid {0} {1}.",
                    adValue.Value,
                    adValue.CurrencyCode));
            };
            // Raised when an impression is recorded for an ad.
            _bannerView.OnAdImpressionRecorded += () =>
            {
                Debug.Log("Banner view recorded an impression.");
            };
            // Raised when a click is recorded for an ad.
            _bannerView.OnAdClicked += () =>
            {
                Debug.Log("Banner view was clicked.");
            };
            // Raised when an ad opened full screen content.
            _bannerView.OnAdFullScreenContentOpened += () =>
            {
                Debug.Log("Banner view full screen content opened.");
            };
            // Raised when the ad closed full screen content.
            _bannerView.OnAdFullScreenContentClosed += () =>
            {
                Debug.Log("Banner view full screen content closed.");
            };
        }
    
        #endregion
    
        #region Interstitial Ads
    
        /// <summary>
        /// 전면 광고 준비(Load) Method
        /// </summary>
        public void LoadInterstitialAd()
        {
            if (_interstitialAd != null)
            {
                _interstitialAd.Destroy();
                _interstitialAd = null;
            }
            
            Debug.Log("Loading the interstitial ad.");
            
            var adRequest = new AdRequest();
            
            InterstitialAd.Load(_interstitialAdUnitId, adRequest, (InterstitialAd ad, LoadAdError error) =>
            {
                if (error != null || ad == null)
                {
                    Debug.LogError("interstitial ad failed to load an ad " + "with error : " + error);
                    return;
                }
                
                Debug.Log("Interstitial ad loaded with response : " + ad.GetResponseInfo());
                _interstitialAd = ad;
                RegisterInterstitialAdsEventHandlers(_interstitialAd);
            });
        }
    
        /// <summary>
        /// 전면 광고 표시 Method
        /// </summary>
        public void ShowInterstitialAd()
        {
            if (_interstitialAd != null && _interstitialAd.CanShowAd())
            {
                Debug.Log("Showing the interstitial ad.");
                _interstitialAd.Show();
            }
            else
            {
                Debug.Log("Interstitial ad is not ready yet.");
            }
        }
        
        /// <summary>
        /// 전면 광고 이벤트 수신 Method
        /// </summary>
        /// <param name="interstitialAd"></param>
        private void RegisterInterstitialAdsEventHandlers(InterstitialAd interstitialAd)
        {
            // Raised when the ad is estimated to have earned money.
            interstitialAd.OnAdPaid += (AdValue adValue) =>
            {
                Debug.Log(String.Format("Interstitial ad paid {0} {1}.",
                    adValue.Value,
                    adValue.CurrencyCode));
            };
            // Raised when an impression is recorded for an ad.
            interstitialAd.OnAdImpressionRecorded += () =>
            {
                Debug.Log("Interstitial ad recorded an impression.");
            };
            // Raised when a click is recorded for an ad.
            interstitialAd.OnAdClicked += () =>
            {
                Debug.Log("Interstitial ad was clicked.");
            };
            // Raised when an ad opened full screen content.
            interstitialAd.OnAdFullScreenContentOpened += () =>
            {
                Debug.Log("Interstitial ad full screen content opened.");
            };
            // Raised when the ad closed full screen content.
            interstitialAd.OnAdFullScreenContentClosed += () =>
            {
                Debug.Log("Interstitial ad full screen content closed.");
                
                // 전면 광고 닫히면 다시 로드
                LoadInterstitialAd();
            };
            // Raised when the ad failed to open full screen content.
            interstitialAd.OnAdFullScreenContentFailed += (AdError error) =>
            {
                Debug.LogError("Interstitial ad failed to open full screen content " +
                               "with error : " + error);
                
                // 전면 광고 로드 실패 시, 다시 로드
                LoadInterstitialAd();
            };
        }
    
        #endregion
    
        #region Rewarded Ads
    
        public void LoadRewardedAd()
        {
            if (_rewardedAd != null)
            {
                _rewardedAd.Destroy();
                _rewardedAd = null;
            }
            
            Debug.Log("Loading the rewarded ad.");
            
            var adRequest = new AdRequest();
            
            RewardedAd.Load(_rewardedAdUnitId, adRequest, (RewardedAd ad, LoadAdError error) =>
            {
                if (error != null || ad == null)
                {
                    Debug.LogError("Rewarded ad failed to load an ad " + "with error : " + error);
                    return;
                }
                
                Debug.Log("Rewarded ad loaded with response : " + ad.GetResponseInfo());
                _rewardedAd = ad;
                RegisterRewardedAdEventHandlers(_rewardedAd);
            });
        }
    
        public void ShowRewardedAd()
        {
            const string rewardMsg = "Rewarded ad rewarded the user. Type: {0}, Amount: {1}";
    
            if (_rewardedAd != null && _rewardedAd.CanShowAd())
            {
                _rewardedAd.Show((Reward reward) => 
                {
                    Debug.Log(String.Format(rewardMsg, reward.Type, reward.Amount));
                });
            }
        }
        
        private void RegisterRewardedAdEventHandlers(RewardedAd ad)
        {
            // Raised when the ad is estimated to have earned money.
            ad.OnAdPaid += (AdValue adValue) =>
            {
                Debug.Log(String.Format("Rewarded ad paid {0} {1}.",
                    adValue.Value,
                    adValue.CurrencyCode));
            };
            // Raised when an impression is recorded for an ad.
            ad.OnAdImpressionRecorded += () =>
            {
                Debug.Log("Rewarded ad recorded an impression.");
            };
            // Raised when a click is recorded for an ad.
            ad.OnAdClicked += () =>
            {
                Debug.Log("Rewarded ad was clicked.");
            };
            // Raised when an ad opened full screen content.
            ad.OnAdFullScreenContentOpened += () =>
            {
                Debug.Log("Rewarded ad full screen content opened.");
            };
            // Raised when the ad closed full screen content.
            ad.OnAdFullScreenContentClosed += () =>
            {
                Debug.Log("Rewarded ad full screen content closed.");
                
                // Reload the ad so that we can show another as soon as possible.
                LoadRewardedAd();
            };
            // Raised when the ad failed to open full screen content.
            ad.OnAdFullScreenContentFailed += (AdError error) =>
            {
                Debug.LogError("Rewarded ad failed to open full screen content " +
                               "with error : " + error);
                
                // Reload the ad so that we can show another as soon as possible.
                LoadRewardedAd();
            };
        }
    
        #endregion
    }

     

    >> QuizCardController.cs

    : 종료버튼 추가, 버그 수정

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using DG.Tweening;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    
    public struct QuizData
    {
        public int index;
        public string question;
        public string description;
        public int type;
        public int answer;
        public string firstOption;  // 원래는 string[] Options로 했었다
        public string secondOption;
        public string thirdOption;
    }
    
    // QuizCard의 위치 상태를 정의할 클래스가 반드시 구현할 Method의 목록
    public interface IQuizCardPositionState
    {
        void Transition(bool withAnimation, Action onComplete = null);
    }
    
    // QuizCard의 위치 상태 전이를 관리할 목적
    public class QuizCardPositionStateContext
    {
        private IQuizCardPositionState _currentState;
    
        public void SetState(IQuizCardPositionState state, bool withAnimation, Action onComplete = null)
        {
            if (_currentState == state) return;
            
            _currentState = state;
            _currentState.Transition(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>();
        }
    }
    
    // QuizCard가 첫 번째 위치에 나타날 상태 클래스
    public class QuizCardPositionStateFirst: QuizCardPositionState, IQuizCardPositionState
    {
        public QuizCardPositionStateFirst(QuizCardController quizCardController) : base(quizCardController) { }
        public void Transition(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();
        }
    }
    
    // QuizCard가 두 번째 위치에 나타날 상태 클래스
    public class QuizCardPositionStateSecond: QuizCardPositionState, IQuizCardPositionState
    {
        public QuizCardPositionStateSecond(QuizCardController quizCardController) : base(quizCardController) { }
        public void Transition(bool withAnimation, Action onComplete = null)
        {
            var animationDuration = (withAnimation) ? 0.2f : 0f;
            _rectTransform.DOAnchorPos(new Vector2(0f, 160f), 0);
            _rectTransform.DOScale(0.9f, animationDuration);
            _canvasGroup.DOFade(0.7f, animationDuration).OnComplete(() => onComplete?.Invoke());
            _rectTransform.SetAsFirstSibling();
        }
    }
    
    // QuizCard가 사라질 상태를 처리할 상태 클래스
    public class QuizCardPositionStateRemove: QuizCardPositionState, IQuizCardPositionState
    {
        public QuizCardPositionStateRemove(QuizCardController quizCardController) : base(quizCardController) { }
        public void Transition(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());
        }
    }
    
    // QuizCard가 뒤집어지는 상태 클래스
    public class QuizCardPositionStateFlip : QuizCardPositionState, IQuizCardPositionState
    {
        public QuizCardPositionStateFlip(QuizCardController quizCardController) : base(quizCardController) { }
    
        public void Transition(bool withAnimation, Action onComplete = null)
        {
            var animationDuration = (withAnimation) ? 0.3f : 0f;
            _rectTransform.DORotate(new Vector3(0f, 90f, 0f), animationDuration / 2)
                .OnComplete(() =>
                {
                    _rectTransform.DORotate(new Vector3(0f, 0f, 0f), animationDuration / 2)
                        .OnComplete(() => onComplete?.Invoke());
                });
        }
    }
    
    public class QuizCardPositionStateFlipNormal : QuizCardPositionState, IQuizCardPositionState
    {
        public QuizCardPositionStateFlipNormal(QuizCardController quizCardController) : base(quizCardController) { }
    
        public void Transition(bool withAnimation, Action onComplete = null)
        {
            var animationDuration = (withAnimation) ? 0.3f : 0f;
            _rectTransform.DORotate(new Vector3(0f, 90f, 0f), animationDuration / 2)
                .OnComplete(() =>
                {
                    _rectTransform.DORotate(new Vector3(0f, 0f, 0f), animationDuration / 2)
                        .OnComplete(() => onComplete?.Invoke());
                });
        }
    }
    
    public class QuizCardController : MonoBehaviour
    {
        [SerializeField] private GameObject frontPanel;
        [SerializeField] private GameObject correctBackPanel;
        [SerializeField] private GameObject incorrectBackPanel;
        
        // Front Panel
        [SerializeField] private TMP_Text questionText; // 퀴즈
        [SerializeField] private TMP_Text descriptionText; // 설명
        [SerializeField] private Button[] optionButtons;   // 보기 --> 타입을 TMP_Text로 해서 text를 직접 받아도 된다. 
        [SerializeField] private GameObject threeOptionButtons;   // 퀴즈 타입에 따른 버튼
        [SerializeField] private GameObject oxButtons;            // 퀴즈 타입에 따른 버튼
        
        // Incorrect Back Panel
        //[SerializeField] private TMP_Text heartCountText;
        
        // Timer
        [SerializeField] private MobicsTimer timer;
        
        // 애니메이션
        [SerializeField] private GameObject quizCardResultPanel;
        
        // Heart Panel
        [SerializeField] private HeartPanelController heartPanelController;
        
        public enum QuizCardPanelType { FrontPanel, CorrectBackPanel, IncorrectBackPanel }
        private enum QuizCardResultType { None, Correct, Incorrect }
        
        public delegate void QuizCardDelegate(int cardIndex);
        private event QuizCardDelegate onCompleted;
        private int _answer;
        private int _quizCardIndex;
        
        private Vector2 _correctBackPanelPosition;
        private Vector2 _incorrectBackPanelPosition;
        
        // Quiz Card 위치 상태
        private IQuizCardPositionState _positionStateFirst;
        private IQuizCardPositionState _positionStateSecond;
        private IQuizCardPositionState _positionStateRemove;
        private IQuizCardPositionState _positionStateFlip;
        private IQuizCardPositionState _positionStateFlipNormal;
        private QuizCardPositionStateContext _positionStateContext;
        
        // 애니메이션
        private Animator _animator;
    
        private void Awake()
        {
            // 숨겨진 패널의 좌표 저장
            _correctBackPanelPosition = correctBackPanel.GetComponent<RectTransform>().anchoredPosition;
            _incorrectBackPanelPosition = incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition;
            
            // 상태 관리를 위한 Context 객체 생성
            _positionStateContext = new QuizCardPositionStateContext();
            _positionStateFirst = new QuizCardPositionStateFirst(this);
            _positionStateSecond = new QuizCardPositionStateSecond(this);
            _positionStateRemove = new QuizCardPositionStateRemove(this);
            _positionStateFlip = new QuizCardPositionStateFlip(this);
            _positionStateFlipNormal = new QuizCardPositionStateFlipNormal(this);
            _positionStateContext.SetState(_positionStateRemove, false); // 카드 위치 초기화
            
            _animator = GetComponent<Animator>();
        }
    
        private void Start()
        {
            timer.OnTimeout = () =>
            {
                // TODO: 오답 연출
                ShowQuizCardResult(QuizCardResultType.Incorrect);
                //SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
            };
        }
    
        #region 구조 개선 전 코드
    
            // public void SetVisible(bool isVisible)
            // {
            //     if (isVisible)
            //     {
            //         timer.InitTimer();
            //         timer.StartTimer();
            //     }
            //     else
            //     {
            //         timer.InitTimer();
            //     }
            // }
    
        #endregion
        
        public enum QuizCardPositionType { First, Second, Remove }
        
        /// <summary>
        /// Quiz Card 위치를 지정하는 Method
        /// </summary>
        /// <param name="quizCardPositionType">Quiz Card 위치</param>
        /// <param name="withAnimation">애니메이션 여부</param>
        /// <param name="onComplete">위치 지정 후 실행할 동작</param>
        public void SetQuizCardPosition(QuizCardPositionType quizCardPositionType,
            bool withAnimation, Action onComplete = null)
        {
            switch (quizCardPositionType)
            {
                case QuizCardPositionType.First:
                    _positionStateContext.SetState(_positionStateFirst, withAnimation, () =>
                    {
                        timer.InitTimer();
                        timer.StartTimer();
                        onComplete?.Invoke();
                    });
                    break;
                case QuizCardPositionType.Second:
                    _positionStateContext.SetState(_positionStateSecond, withAnimation, () =>
                    {
                        timer.InitTimer();
                        onComplete?.Invoke();
                    });
                    break;
                case QuizCardPositionType.Remove:
                    _positionStateContext.SetState(_positionStateRemove, withAnimation, onComplete);
                    break;
            }
        }
    
        public void SetQuiz(QuizData quizData, QuizCardDelegate onCompleted)
        {
            // 1. 퀴즈
            // 2. 설명
            // 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
            // 4. 정답
            // 5. 보기 (1, 2, 3)
            
            // 퀴즈 카드 인덱스 할당
            _quizCardIndex = quizData.index;
            
            // front Panel 표시
            SetQuizCardPanelActive(QuizCardPanelType.FrontPanel, false);
            
            // 퀴즈 데이터 표현
            questionText.text = quizData.question;
            _answer = quizData.answer;
            descriptionText.text = quizData.description;
    
            if (quizData.type == 0)         // 3지선다 퀴즈
            {
                threeOptionButtons.SetActive(true);
                oxButtons.SetActive(false);
                
                var firstButtonText = optionButtons[0].GetComponentInChildren<TMP_Text>();
                firstButtonText.text = quizData.firstOption;
                var secondButtonText = optionButtons[1].GetComponentInChildren<TMP_Text>();
                secondButtonText.text = quizData.secondOption;
                var thirdButtonText = optionButtons[2].GetComponentInChildren<TMP_Text>();
                thirdButtonText.text = quizData.thirdOption;
            }
            else if (quizData.type == 1)    // OX 퀴즈
            {
                threeOptionButtons.SetActive(false);
                oxButtons.SetActive(true);
            }
            
            this.onCompleted = onCompleted;
            
            heartPanelController.InitHeartCount(GameManager.Instance.heartCount);
            
            // Quiz Card Result Panel
            ShowQuizCardResult(QuizCardResultType.None);
        }
    
        /// <summary>
        /// 퀴즈의 정답을 선택하기 위한 버튼
        /// </summary>
        /// <param name="buttonIndex"></param>
        public void OnClickOptionButton(int buttonIndex)
        {
            // Timer 일시 정지
            timer.PauseTimer();
            
            if (buttonIndex == _answer) // 정답
            {
                Debug.Log("정답!");
                // TODO: 정답 연출
                
                ShowQuizCardResult(QuizCardResultType.Correct);
                //SetQuizCardPanelActive(QuizCardPanelType.CorrectBackPanel);
            }
            else                        // 오답
            {
                Debug.Log("오답!");
                // TODO: 오답 연출
                
                ShowQuizCardResult(QuizCardResultType.Incorrect);
                //SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
            }
        }
    
        public void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType)
        {
            ShowQuizCardResult(QuizCardResultType.None);
            SetQuizCardPanelActive(quizCardPanelType, true);
        }
    
        private void ShowQuizCardResult(QuizCardResultType quizCardResultType)
        {
            switch (quizCardResultType)
            {
                case QuizCardResultType.Correct:
                    quizCardResultPanel.SetActive(true);
                    _animator.SetTrigger("correct");
                    break;
                case QuizCardResultType.Incorrect:
                    quizCardResultPanel.SetActive(true);
                    _animator.SetTrigger("incorrect");
                    break;
                case QuizCardResultType.None:
                    quizCardResultPanel.SetActive(false);
                    break;
            }
        }
    
        public void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType, bool withAnimation)
        {
            switch (quizCardPanelType)
            {
                case QuizCardPanelType.FrontPanel:
                    correctBackPanel.SetActive(false);
                    incorrectBackPanel.SetActive(false);
    
                    _positionStateContext.SetState(_positionStateFlipNormal, withAnimation, () =>
                    {
                        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, withAnimation, () =>
                    {
                        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, withAnimation, () =>
                    {
                        incorrectBackPanel.SetActive(true);
                        correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
                        incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
                    });
                    break;
            }
        }
        
        public void OnClickExitButton()
        {
            GameManager.Instance.QuitGame();
        }
    
        #region Correct Back Panel
    
        /// <summary>
        /// 다음 버튼 이벤트
        /// </summary>
        public void OnClickNextQuizButton()
        {
            onCompleted?.Invoke(_quizCardIndex);
        }
    
        #endregion
    
        #region Incorrect Back Panel
        
        /// <summary>
        /// 다시 도전 버튼 이벤트
        /// </summary>
        public void OnClickRetryQuizButton()
        {
            if (GameManager.Instance.heartCount > 0)
            {
                GameManager.Instance.heartCount--;
                heartPanelController.RemoveHeart(() =>
                {
                    SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
                    
                    // 타이머 초기화 및 시작
                    timer.InitTimer();
                    timer.StartTimer();
                });
            }
            else
            {
                // 하트가 부족해서 Retry 불가
                heartPanelController.EmptyHeart();
            }
        }
    
        #endregion
    }

     

    >> GamePanelController.cs

    : 함수명, 매개변수명 수정

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class GamePanelController : MonoBehaviour
    {
        private GameObject _firstQuizCardObject;
        private GameObject _secondQuizCardObject;
    
        private List<QuizData> _quizDataList;
        
        // private int _lastGeneratedQuizIndex; // 구조 개선 전 코드
        private int _lastStageIndex;
    
        private Queue<QuizCardController> _quizCardQueue = new();
        
        /// <summary>
        /// 새로운 퀴즈 카드 추가하는 함수
        /// </summary>
        /// <param name="quizData">퀴즈 데이터</param>
        /// <param name="isInit">초기화 여부</param>
        public void AddQuizCardObject(QuizData? quizData, bool isInit = false) // Queue를 이용한 새로운 로직
        {
            QuizCardController tempQuizCardController = null; // 뺄 Quiz Card
            
            // 1. First 영역의 카드 제거
            void RemoveFirstQuizCard(Action onCompleted = null)
            {
                tempQuizCardController = _quizCardQueue.Dequeue();
                tempQuizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.Remove,
                    true, onCompleted);
            }
            
            // 2. Second 영역의 카드를 First 영역으로 이동
            void SecondQuizCardToFirst(Action onCompleted = null)
            {
                var firstQuizCardController = _quizCardQueue.Peek();
                firstQuizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.First,
                    true, onCompleted);
            }
            
            // 3. 새로운 퀴즈 카드를 Second 영역에 생성
            void AddNewQuizCard(Action onCompleted = 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,
                        true, onCompleted);
                }
            }
            
            // 애니메이션 처리
            if (_quizCardQueue.Count > 0)
            {
                if (isInit)
                {
                    SecondQuizCardToFirst();
                    AddNewQuizCard();
                }
                else
                {
                    RemoveFirstQuizCard(() =>
                        SecondQuizCardToFirst(() =>
                            AddNewQuizCard(() =>
                            {
                                if (tempQuizCardController != null)
                                    ObjectPool.Instance.ReturnObject(tempQuizCardController.gameObject);
                            })));
                }
            }
            else
            {
                AddNewQuizCard();
            }
        }
        
        private void Start()
        {
            _lastStageIndex = UserInformations.LastStageIndex;
            InitQuizCards(_lastStageIndex);
        }
    
        private void InitQuizCards(int stageIndex)
        {
            _quizDataList = QuizDataController.LoadQuizData(stageIndex);
            
            AddQuizCardObject(_quizDataList[0], true);
            AddQuizCardObject(_quizDataList[1], true);
    
            #region 구조 개선 전 코드
    
            // _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);
            
            // 마지막으로 생성된 Quiz Index
            // _lastGeneratedQuizIndex = 1;
    
            #endregion
        }
    
        private void OnCompletedQuiz(int cardIndex)
        {
            if (cardIndex < _quizDataList.Count - 2)
            {
                AddQuizCardObject(_quizDataList[cardIndex + 2]);
            }
            else
            {
                AddQuizCardObject(null);
    
                if (cardIndex == _quizDataList.Count - 1)
                {
                    // TODO: 스테이지 클리어 연출
                    _lastStageIndex++;
                    
                    // TODO: 스테이지 클리어 연출 후, 새로운 스테이지 시작
                    if (_lastStageIndex < Constants.MAX_STAGE_COUNT) // 임시 코드
                        InitQuizCards(_lastStageIndex);
                }
            }
    
            #region 구조 개선 전 코드
    
            // if (cardIndex >= Constants.MAX_QUIZ_COUNT - 1)
            // {
            //     if (_lastStageIndex >= Constants.MAX_STAGE_COUNT - 1)
            //     {
            //         // TODO: 올 클리어 연출
            //         
            //         GameManager.Instance.QuitGame();
            //     }
            //     else
            //     {
            //         // TODO: 스테이지 클리어 연출
            //         InitQuizCards(++_lastStageIndex);
            //         return;
            //     }
            // }
            // else
            // {
            //      //ChangeQuizCard();
            //      if (_quizDataList.Count > cardIndex + 1)
            //          AddQuizCardObject(_quizDataList[cardIndex + 1]);
            //      else
            //          AddQuizCardObject(null);
            // }
    
            #endregion
        }
    
        #region 구조 개선 전 코드
    
        // 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(); // 같은 depth에서 마지막으로 이동 --> 카드가 앞으로 배치됨
        //         
        //         quizCardObject.GetComponent<QuizCardController>().SetVisible(true);
        //     }
        //     else if (index == 1)
        //     {
        //         quizCardTransform.anchoredPosition = new Vector2(0, 160);
        //         quizCardTransform.localScale = Vector3.one * 0.9f;
        //         quizCardTransform.SetAsFirstSibling(); // 같은 depth에서 처음으로 이동
        //
        //         quizCardObject.GetComponent<QuizCardController>().SetVisible(false);
        //     }
        // }
    
        // private void ChangeQuizCard()
        // {
        //     if (_lastGeneratedQuizIndex >= Constants.MAX_QUIZ_COUNT) return;
        //     
        //     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);
        //     SetQuizCardPosition(_secondQuizCardObject, 1);
        //     
        //     ObjectPool.Instance.ReturnObject(temp);
        // }
        
        #endregion
    }

    C# 단기 교육 보강

    16일차

    장염 이슈로 추후 작성 예정...