본문 바로가기
Development/C#

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

by Mobics 2025. 2. 18.

 

목차


    ※ GetComponent나 FindObjectsOfType 과 같은 방식은 많이 사용되는 것을 지양한다


    퀴즈 게임 만들기

    25.02.18

    Heart Panel을 Quiz Card에 적용하기

    >> Heart Count Text 삭제

     

    >> Heart Image, Heart Remove Image, Heart Count Text의 Color 변경

    : (242, 68, 149, 255)

     

    >> QuizCardController.cs에 Heart Panel 바인딩

     

    ※ Action

    : Delegate와 달리 별도의 선언 없이 함수를 매개변수로 전달 가능하다

     

    >> TestButtons 삭제

     

    현재 있는 버그

    • Stage를 넘어가면 QuizCard가 하나씩 계속 쌓이는 버그
    • IncorrectBackPanel에서 다시 도전을 광클하면 생명이 여러 개 깎이는 버그

     

    Quiz Card를 띄우는 새로운 로직

    1. 앞에 있는 Card를 제거
    2. 뒤에 있는 Card를 앞으로 옮기기
    3. 새로운 Card를 뒤에 만들기

    --> Queue를 활용

     

    ※ Stage의 마지막 문제를 풀 때는 뒤에 보이는 QuizCard가 없도록

     

    활동

    : Quiz Card 애니메이션 구현

    • 카드가 회전하며 뒤집히는 애니메이션
    • 다음 퀴즈로 넘어갈 때, 앞의 카드는 내려가고 뒤의 카드가 앞으로 나타나며 새로운 뒤의 카드가 등장하는 애니메이션

    ※ 카드 뒤집기는 180도를 돌리는 것이 아니라, 90도만 돌린 뒤 다시 역방향으로 다시 90도를 돌리면 시각적으로는 카드를 뒤집은 것 처럼 보이면서 글자 등이 뒤집어지지 않게 연출이 가능하다.

     

    StatePattern를 사용하여 QuizCard의 위치 상태 구현

    >> QuizCard에 Canvas Group을 추가

    : 자식들의 Alpha값을 한번에 설정 가능

     

    최종 코드

    >> 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)
        {
            _currentState = state;
            _currentState.Transition(false); // false는 내가 임의로 넣은 것
        }
    }
    
    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)
        {
            
        }
    }
    
    // QuizCard가 사라질 상태를 처리할 상태 클래스
    public class QuizCardPositionStateRemove: QuizCardPositionState, IQuizCardPositionState
    {
        public QuizCardPositionStateRemove(QuizCardController quizCardController) : base(quizCardController) { }
        public void Transition(bool withAnimation, Action onComplete = null)
        {
            
        }
    }
    
    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;
        
        // Heart Panel
        [SerializeField] private HeartPanelController heartPanelController;
        
        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;
        }
    
        private void Start()
        {
            timer.OnTimeout = () =>
            {
                // TODO: 오답 연출
                SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
            };
        }
    
        public void SetVisible(bool isVisible)
        {
            if (isVisible)
            {
                timer.InitTimer();
                timer.StartTimer();
            }
            else
            {
                timer.InitTimer();
            }
        }
    
        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);
            
            // 퀴즈 데이터 표현
            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;
            
            heartPanelController.InitHeartCount(GameManager.Instance.heartCount);
        }
    
        public void OnClickOptionButton(int buttonIndex)
        {
            // Timer 일시 정지
            timer.PauseTimer();
            
            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--;
                heartPanelController.RemoveHeart(() =>
                {
                    SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
                    
                    // 타이머 초기화 및 시작
                    timer.InitTimer();
                    timer.StartTimer();
                });
            }
            else
            {
                // 하트가 부족해서 Retry 불가
                heartPanelController.EmptyHeart();
            }
        }
    
        #endregion
    }

     

    >> HeartPanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using DG.Tweening;
    using TMPro;
    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.SetActive(false);
            InitHeartCount(GameManager.Instance.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, 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(() =>
                {
                    DOVirtual.DelayedCall(0.5f, () => onComplete?.Invoke());
                });
            });
        }
    
        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.5f); // 연결된 동작들이 0.5f마다 동작하도록
            }
        }
    
        public void EmptyHeart()
        {
            // 효과음 재생
            if (UserInformations.IsPlaySFX)
                _audioSource.PlayOneShot(heartEmptyAudioClip);
            
            // 주먹으로 친 것처럼 흔들리는 애니메이션
            GetComponent<RectTransform>().DOPunchPosition(new Vector3(20f, 0, 0), 1f, 7);
        }
    
        public void RemoveHeart(Action onComplete = null)
        {
            // 효과음 재생
            if (UserInformations.IsPlaySFX)
                _audioSource.PlayOneShot(heartRemoveAudioClip);
            
            // 하트 초기화
            heartRemoveImageObject.SetActive(true);
            heartRemoveImageObject.transform.localScale = Vector3.zero;
            heartRemoveImageObject.GetComponent<Image>().color = new Color32(242, 68, 149, 255);
            
            // 하트 사라지는 연출
            heartRemoveImageObject.transform.DOScale(3f, 1f);
            heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
    
            // 하트 개수 텍스트가 감소되는 연출
            DOVirtual.DelayedCall(1f, () =>
            {
                ChangeTextAnimation(false, onComplete);
            });
        }
    }

     

    >> UserInformations.cs

    using UnityEngine;
    using UnityEditor;
    
    public static class UserInformations
    {
        [MenuItem("Window/PlayerPrefs 초기화")]
        private static void ResetPrefs() // 다른 수강생님이 만든 레지스트리 초기화 함수
        {
            PlayerPrefs.DeleteAll();
            Debug.Log("PlayerPrefs has been reset.");
        }
        
        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); }
        }
        
        // 효과음 재생 여부
        public static bool IsPlaySFX
        {
            get { return PlayerPrefs.GetInt("IsPlaySFX", 1) == 1; }
            set { PlayerPrefs.SetInt("IsPlaySFX", value ? 1 : 0); }
        }
        
        // 배경음악 재생 여부
        public static bool IsPlayBGM
        {
            get { return PlayerPrefs.GetInt("IsPlayBGM", 1) == 1; }
            set { PlayerPrefs.SetInt("IsPlayBGM", value ? 1 : 0); }
        }
    }

     

    >> Constants.cs

    public class Constants
    {
        // public const int MAX_QUIZ_COUNT = 5;           // 한 스테이지에 나오는 퀴즈의 수, 구조 개선하면서 제거
        public const int MAX_STAGE_COUNT = 3;          // 전체 스테이지 수
    }

     

    >> 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 Queue<GameObject> _quizCardObjectQueue = new();
        
        /// <summary>
        /// 새로운 퀴즈 카드 추가하는 함수
        /// </summary>
        /// <param name="quizData"></param>
        /// <param name="isInit"></param>
        public void AddQuizCardObject(QuizData? quizData, bool isInit = false) // Queue를 이용한 새로운 로직
        {
            GameObject tempObject = null; // Animation을 재생하고 삭제되도록
            
            // 1. 앞에 있는 QuizCard 제거
            if (_quizCardObjectQueue.Count > 0 && !isInit)
            {
                tempObject = _quizCardObjectQueue.Dequeue();
            }
            
            // 2. 뒤에 있는 카드 앞으로 옮기기
            if (_quizCardObjectQueue.Count > 0)
            {
                var firstQuizCardObject = _quizCardObjectQueue.Peek();
                firstQuizCardObject.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
                firstQuizCardObject.transform.localScale = Vector3.one;
                firstQuizCardObject.transform.SetAsLastSibling();
                firstQuizCardObject.GetComponent<QuizCardController>().SetVisible(true);
            }
            
            // 3. 새로운 카드 뒤에 만들기
            if (quizData.HasValue)
            {
                var quizCardObject = ObjectPool.Instance.GetObject();
                quizCardObject.GetComponent<QuizCardController>().SetQuiz(quizData.Value, OnCompletedQuiz);
                quizCardObject.GetComponent<QuizCardController>().SetVisible(false);
                quizCardObject.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, 160f);
                quizCardObject.transform.localScale = Vector3.one * 0.9f;
                quizCardObject.transform.SetAsFirstSibling();
                
                _quizCardObjectQueue.Enqueue(quizCardObject);
            }
    
            if (tempObject != null)
            {
                ObjectPool.Instance.ReturnObject(tempObject);
            }
        }
        
        private void Start()
        {
            _lastStageIndex = UserInformations.LastStageIndex;
            InitQuizCard(_lastStageIndex);
        }
    
        private void InitQuizCard(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) // 임시 코드
                        InitQuizCard(_lastStageIndex);
                }
            }
    
            #region 구조 개선 전 코드
    
            // if (cardIndex >= Constants.MAX_QUIZ_COUNT - 1)
            // {
            //     if (_lastStageIndex >= Constants.MAX_STAGE_COUNT - 1)
            //     {
            //         // TODO: 올 클리어 연출
            //         
            //         GameManager.Instance.QuitGame();
            //     }
            //     else
            //     {
            //         // TODO: 스테이지 클리어 연출
            //         InitQuizCard(++_lastStageIndex);
            //         return;
            //     }
            // }
            // ChangeQuizCard();
    
            #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
    }

     

    >> QuizDataController.cs

    using System.Collections.Generic;
    using UnityEngine;
    using System.Text.RegularExpressions; // Regex를 사용하기 위해 선언
    
    public static class QuizDataController
    {
        static string ROW_SEPARATOR = @"\r\n|\n\r|\n|\r";
        static string COL_SEPARATOR = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))";
        private static char[] TRIM_CHARS = { '\"' }; // Trim : 특정한 문자 제거
        
        public static List<QuizData> LoadQuizData(int stageIndex)
        {
            // 퀴즈를 스테이지별로 나누기 위해 불러오는 quizData 파일의 이름 형식을 고정
            var fileName = "QuizData-" + stageIndex;
            
            // Resources.Load()는 Object 타입으로 반환하기 때문에 'as'로 형변환
            TextAsset quizDataAsset = Resources.Load(fileName) as TextAsset;
            var lines = Regex.Split(quizDataAsset.text, ROW_SEPARATOR);
    
            var quizDataList = new List<QuizData>();
    
            for (var i = 1; i < lines.Length; i++)
            {
                var values = Regex.Split(lines[i], COL_SEPARATOR);
                
                QuizData quizData = new QuizData();
                quizData.index = i - 1; // 추가
                
                for (var j = 0; j < values.Length; j++)
                {
                    var value = values[j];
                    // value의 시작(TrimStart)과 끝(TrimEnd)에 " 가 있으면 잘라주고 "\\"는 ""로 바꿔준다.
                    value = value.TrimStart(TRIM_CHARS).TrimEnd(TRIM_CHARS).Replace("\\", "");
                    
                    switch (j)
                    {
                        case 0:
                            quizData.question = value;
                            break;
                        case 1:
                            quizData.description = value;
                            break;
                        case 2:
                            quizData.type = int.Parse(value); // int.Parse()를 통해 int로 변환
                            break;
                        case 3:
                            quizData.answer = int.Parse(value);
                            break;
                        case 4:
                            quizData.firstOption = value;
                            break;
                        case 5:
                            quizData.secondOption = value;
                            break;
                        case 6:
                            quizData.thirdOption = value;
                            break;
                    }
                }
                quizDataList.Add(quizData);
            }
            return quizDataList;
        }
    }

    C# 단기 교육 보강

    10일차

    게임수학

    π(파이), 라디안

     

    └ 삼각함수

     

    >> 사인 법칙

    : ASA(Angle - Side - Angle) 또는 AAS (Angle - Angle - Side)

     

    >> 코사인 법칙

    : SSS(Side - Side - Side) 또는 SAS(Side - Angle - Side)

     

    >> 탄젠트 법칙

    : 사인 법칙과 코사인 법칙으로 해결하기 어려운 경우

     

    >> 그래프

    • 0 ~ 90º : +Sinθ, +Cosθ, +Tanθ --> Sin, Cos, Tan 모두 양수
    • 90º ~ 180º : +Sinθ, -Cosθ, -Tanθ --> Sin만 양수
    • 180º ~ 270º : -Sinθ, -Cosθ, +Tanθ --> Tan만 양수
    • 270º ~ 360º : -Sinθ, +Cosθ, -Tanθ --> Cos만 양수

     

    └ 게임 수학 실습

    : 'Game Math' Scene 생성

     

    >> Point Light 생성

    : PositionY 살짝 올려줌

     

    >> LightIntensity.cs 생성

    : Point Light에 추가

     

    >> Turret 추가 및 TurretRotation.cs 생성

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class TurretRotation : MonoBehaviour
    {
        public float rotationSpeed = 60f;
        
        private void Update()
        {
            transform.Rotate(Vector3.up, Time.deltaTime * rotationSpeed);
        }
    }

     

    >> 삼각함수를 이용하여 Turret 회전

    : 360도 회전하는 게 아니라 원하는 만큼만 감시하도록 구현

     

     

    └ 벡터(Vector)와 스칼라(Scalar)

    : 속도와 속력의 차이

     

    >> 벡터의 연산

    --> 덧셈의 결과는 AB가 아니라 OC가 맞음

     

    >> 벡터의 크기

    --> AB가 아니라 OC

     

    >> 내적

    : 두 벡터의 관계

     

     

    >> 외적

    : 두 벡터의 수직 벡터