본문 바로가기
Development/C#

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

by Mobics 2025. 2. 14.

 

목차


    ※ Object들을 만들 때부터 이름을 잘 정해놓아야 한다.

    : 어지러운 Object들을 보면 만든 나도 구분을 못하고, 혼자 만드는 게 아니라 같이 만든다면 더욱 문제가 된다.


    퀴즈 게임 만들기

    25.02.14

    카드 기능 구현

    : 'Quiz Card' Prefab 수정

    >> 'Buttons' 의 이름을 'Three Option Buttons'로 바꾸고 부모 오브젝트로 Options 생성

    --> Options로 위치를 제어하기 위해 Three Option Buttons는 초기화

    --> Anchor는 Alt + Shift

     

    --> Options는 빈 게임 오브젝트로 생성

     

    >> OX Buttons

    : Three Options Button 복붙, 버튼 하나 제거 (맨 밑에 있는 버튼 제거) --> Button의 Text를 O, X로 수정

     

    >> Quiz Card의 QuizCardController.cs에 바인딩

     

    >> Button들 전부 선택하여 OnClick() 함수 바인딩

    --> Button의 Index를 각각 설정 (위에서부터 0, 1, 2 / 0, 1)

    --> 이름 변경 (First Option Button, Second Option Button, Third Option Button / O Button, X Button)

     

    └ Front Panel

    : 빈 게임 오브젝트로 만들어서 Text와 Options를 자식 오브젝트로 넣기

     

    └ Correct Back Panel

    : 빈 게임 오브젝트로 만들고 오른쪽으로 치워두기 (Left, Right 조정)

    --> 이름 : Correct Back Panel

     

    >> Description Text 추가

     

    >> 빈 게임 오브젝트로 Buttons 추가

    --> Anchor는 Alt + Shift

     

    >> Buttons의 자식으로 Button 추가 (Next Quiz Button, Exit Button)

    : Button의 Text 수정

    --> 그 다음 NextQuizButton을 복사하여 Exit Button 생성 (Exit Button의 Text는 "종료")

     

    >> 각 버튼에 맞게 OnClick() 함수 바인딩

    --> Exit Button은 OnClickExitButton() 바인딩

    └ Incorrect Back Panel

    : Correct Back Panel을 복붙하고 위치 조정

     

    >> 복사한 Next Quiz Button의 이름을 Retry Quiz Button으로 수정

    : Text도 수정 ("다시 도전" 으로 수정) / Retry Quiz Button의 OnClick()에 바운딩된 함수도 수정해줘야 한다.

     

    >> Text로 Heart Count Text (TMP) 생성

     

    >> QuizCardController.cs에 각각 바인딩

     

    HeartCount 구현

    >> GameScene에 빈 게임 오브젝트로 GameManager 추가

     

    >> Quiz Card Controller.cs 에 Heart Count Text 바인딩

     

    >> UserInformations.cs 생성

    : PlayerPrefs를 활용하여 HeartCount에 대한 정보를 저장 --> static property를 활용

     

    >> 줄어든 HeartCount 다시 늘리기

    1. 레지스트리 편집기 열기 --> 실행을 열어서 (윈도우 + R) 'regedit' 입력

    2. 경로 : 컴퓨터\HKEY_CURRENT_USER\SOFTWARE\Unity\UnityEditor\(Company Name)\(Product Name)

    3. HeartCount 값 조정

     

    ※ 혹시 경로에 설정한 Company Name이 없다면

    : Company Name을 설정만 하고 Play를 한 적이 없어서 정보가 없는 것 --> 한번 Play하고 다시 가보자

    >> 기존 값은 'DefaultCompany'에 저장되어 있을 것

     

    └ Company Name, Product Name 설정

    : Build Settings -> PlayerSettings

    --> Company Name, Product Name 수정

     

    ※ 업데이트 할 때마다 늘어나는 버전 코드....? --> 알아보자

     

    ※ App Life Cycle

    출처 : https://developer.apple.com/documentation/uikit

    • Not Running : 실행되지 않거나 종료된 상태
    • InActive : 앱이 Foreground 상태로 돌아가지만, 이벤트는 받지 않는 상태, 앱의 상태 전환 과정에서 잠깐 머무는 단계
    • Active : 일반적으로 앱이 돌아가는 상태 (이벤트를 받는 단계)
    • Background : 앱이 Suspended 상태 (유예 상태)로 진입되기 전 거치는 상태 --> 이 경우 앱이 종료되었다고 봐야하며 음악, 통화 앱 같은 경우는 Background에 머물고 보통 다른 앱들은 바로 Suspended 상태로 넘어간다.
    • Suspended : 앱이 Background 상태에 있지만, 아무 코드도 실행하지 않는 상태, 시스템이 임의로 Background 상태의 앱을 Suspended 상태로 만든다. (리소스 해제) --> 일반적으로 Not Running과 동일한 상태.

    --> 주요 작업은 Active와 Background 상태에서 수행된다.

     

    ※ OnApplicationPause(bool pauseStatus) 함수

     

    스테이지 클리어 정보 저장

    : UserInformations.cs에서 PlayerPrefs를 활용하여 LastStageIndex에 대한 정보를 저장 --> static property를 활용

    --> QuizCardController에서 클리어한 quizCardIndex도 저장

     

    ※ PlayerPrefs

    :

     

    QuizData 수정

    : QuizData-0.csv 파일을 퀴즈5까지 줄이고 복붙하여 QuizData-0부터 QuizData-4까지 총 5개의 QuizData 만듦

    --> 이후 OX퀴즈는 보기가 필요 없으므로 제거

     

    >> Stage 구분을 위해 QuizData의 Question을 수정

    --> 다른 QuizData파일도 각각 맞게 수정

     

    원형 타이머 제작

    >> Image는 Figma를 통해 제작

     

    >> Game panel의 GamePanelController.cs 를 비활성화

    : GameScene에서 확인하기 위해

     

    최종 코드

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : Singleton<GameManager>
    {
        [HideInInspector] public int heartCount;
    
        private void Start()
        {
            heartCount = UserInformations.HeartCount;
        }
    
        public void StartGame()
        {
            SceneManager.LoadScene("Game");
        }
    
        public void QuitGame()
        {
            SceneManager.LoadScene("Main");
        }
    
        public void AllClearStage()
        {
            // TODO: 전체 스테이지 클리어 처리
            
            QuitGame();
        }
        
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
    
        private void OnApplicationQuit() // 게임의 정상적인 종료 시, 호출
        {
            Debug.Log("OnApplicationQuit!");
            UserInformations.HeartCount = heartCount;
        }
    }

     

    >> GamePanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class GamePanelController : MonoBehaviour
    {
        private GameObject _firstQuizCardObject;
        private GameObject _secondQuizCardObject;
    
        private List<QuizData> _quizDataList;
        
        private int _lastGeneratedQuizIndex;
        private int _lastStageIndex;
        
        private void Start()
        {
            _lastStageIndex = UserInformations.LastStageIndex;
            InitQuizCard(_lastStageIndex);
        }
    
        private void InitQuizCard(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);
            
            // 마지막으로 생성된 Quiz Index
            _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();
                }
                else
                {
                    // TODO: 스테이지 클리어 연출
                    InitQuizCard(++_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(); // 같은 depth에서 마지막으로 이동 --> 카드가 앞으로 배치됨
            }
            else if (index == 1)
            {
                quizCardTransform.anchoredPosition = new Vector2(0, 160);
                quizCardTransform.localScale = Vector3.one * 0.9f;
                quizCardTransform.SetAsFirstSibling(); // 같은 depth에서 처음으로 이동
            }
        }
    
        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);
        }
    }

     

    >> QuizCardController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    
    public struct QuizData
    {
        public string question;
        public string description;
        public int type;
        public int answer;
        public string firstOption;  // 원래는 string[] Options로 했었다
        public string secondOption;
        public string thirdOption;
    }
    
    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;
        
        private enum QuizCardPanelType { FrontPanel, CorrectBackPanel, IncorrectBackPanel }
        
        public delegate void QuizCardDelegate(int cardIndex);
        private event QuizCardDelegate onCompleted;
        private int _answer;
        private int _quizCardIndex;
        
        private Vector2 _correctBackPanelPosition;
        private Vector2 _incorrectBackPanelPosition;
    
        private void Awake()
        {
            // 숨겨진 패널의 좌표 저장
            _correctBackPanelPosition = correctBackPanel.GetComponent<RectTransform>().anchoredPosition;
            _incorrectBackPanelPosition = incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition;
        }
    
        public void SetQuiz(QuizData quizData, int quizCardIndex, QuizCardDelegate onCompleted)
        {
            // 1. 퀴즈
            // 2. 설명
            // 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
            // 4. 정답
            // 5. 보기 (1, 2, 3)
            
            // 퀴즈 카드 인덱스 할당
            _quizCardIndex = quizCardIndex;
            
            // front Panel 표시
            SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
            
            // 퀴즈 데이터 표현
            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 퀴즈
            {
                oxButtons.SetActive(true);
                threeOptionButtons.SetActive(false);
            }
            
            this.onCompleted = onCompleted;
            
            // Incorrect Back Panel
            heartCountText.text = GameManager.Instance.heartCount.ToString();
        }
    
        public void OnClickOptionButton(int buttonIndex)
        {
            if (buttonIndex == _answer) // 정답
            {
                Debug.Log("정답!");
                // TODO: 정답 연출
                
                SetQuizCardPanelActive(QuizCardPanelType.CorrectBackPanel);
            }
            else                        // 오답
            {
                Debug.Log("오답!");
                // TODO: 오답 연출
                
                SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
            }
        }
    
        private void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType)
        {
            switch (quizCardPanelType)
            {
                case QuizCardPanelType.FrontPanel:
                    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);
                    correctBackPanel.SetActive(true);
                    incorrectBackPanel.SetActive(false);
                    
                    correctBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
                    incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
                    break;
                case QuizCardPanelType.IncorrectBackPanel:
                    frontPanel.SetActive(false);
                    correctBackPanel.SetActive(false);
                    incorrectBackPanel.SetActive(true);
                    
                    correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
                    incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
                    break;
            }
        }
        
        public void OnClickExitButton()
        {
            
        }
    
        #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--;
                heartCountText.text = GameManager.Instance.heartCount.ToString(); // Incorrect Back Panel에 heartCount 표시
                SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
            }
            else
            {
                // 하트가 부족해서 Retry 불가
                // TODO: 하트 부족 알림 구현
            }
        }
    
        #endregion
    }

     

    >> UserInformation.cs

    : PlayerPrefs를 활용하여 HeartCount과 LastStageIndex에 대한 정보를 저장 --> static property를 활용

    using UnityEngine;
    
    public static class UserInformations
    {
        private const string HEART_COUNT = "HeartCount"; // string key 값 저장
        private const string LAST_STAGE_INDEX = "LastStageIndex";
        
        // 하트 수
        public static int HeartCount
        {
            get
            {
                // "HeartCount"라는 이름의 정보를 Int로 가져오는데, Default 값은 5 --> 최초로 게임이 시작되면 저장된 값이 없기 때문
                return PlayerPrefs.GetInt(HEART_COUNT, 5);
            }
            set
            {
                // value 값으로 PlayerPrefs에 저장
                PlayerPrefs.SetInt(HEART_COUNT, value);
            }
        }
        
        // 스테이지 클리어 정보
        public static int LastStageIndex
        {
            get
            {
                return PlayerPrefs.GetInt(LAST_STAGE_INDEX, 0);
            }
            set
            {
                PlayerPrefs.GetInt(LAST_STAGE_INDEX, value);
            }
        }
    }

     

    >> Constants.cs

    : 공통적인 상수 관리

    public class Constants
    {
        public const int MAX_QUIZ_COUNT = 5;           // 한 스테이지에 나오는 퀴즈의 수
        public const int MAX_STAGE_COUNT = 3;          // 전체 스테이지 수
    }

     

    >> MobicsTimer.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class MobicsTimer : MonoBehaviour
    {
        [Serializable]
        public class FillSettings
        {
            public Color color;
        }
        public FillSettings fillSettings;
    
        [Serializable]
        public class BackgroundSettings
        {
            public Color color;
        }
        public BackgroundSettings backgroundSettings;
    
        [SerializeField] private Image fillImage;
        [SerializeField] private float totalTime;
        
        public float CurrentTime { get; private set; }      // 현재 시간 저장
        private bool _isPaused;                             // 현재 Pause 상태인지 체크
    
        private void Update()
        {
            if (!_isPaused)
            {
                CurrentTime += Time.deltaTime;
                fillImage.fillAmount = CurrentTime / totalTime;
            }
        }
    
        public void StartTimer()
        {
            _isPaused = false;
        }
    
        public void PauseTimer()
        {
            _isPaused = true;
        }
    
        public void ResetTimer()
        {
            CurrentTime = 0;
            fillImage.fillAmount = 1;
        }
    }