본문 바로가기
Development/C#

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

by Mobics 2025. 2. 19.

 

목차


    퀴즈 게임 만들기

    25.02.19

    코드로 Quiz Card의 상태 패턴 구현

    : Quiz Card Animation 구현 완료, Timer 버그 수정

    --> 다음 퀴즈로 넘어갈 때, 앞의 카드가 내려가고 뒤의 카드가 앞으로 나오면서 새 카드가 뒤에 등장하는 Animation

     

    활동

    >> 카드 뒤집기를 상태 패턴으로 구현

    : 다시 도전한 뒤에 버그 발생

    --> 'QuizCardPositionStateContext'의 'SetState' 함수에서 아래 코드 부분이 문제를 일으킴

    if (_currentState == state) return;

    --> 'QuizCardPositionStateFlipNormal' 상태를 추가하여 해결

     

    >> Game Scene 전체 구현하기 --> 게임 플레이 영상과 동일한 형태로 게임을 구현하기

    • Button 이미지 적용
    • 정답/오답 연출 구현
    • 폰트 교체 --> 에스코어드림 폰트
    • 스테이지 시작 구현

     

    ※ 아직 미구현 된 Game Scene

    - 레벨(스테이지) 팝업

    - 정답과 오답 시, 애니메이션

    - 오답 시, 몇 문제 남았는지 표시

    - 앞의 퀴즈 카드에 명암을 넣어서 입체적으로 표현

     

    └ 활동 해보기

    >> Button 이미지 적용

    1. QuizCard Prefab의 O Button의 Source Image 변경 및 Width와 Height 조정

    --> X Button도 마찬가지로 설정

     

    2. Button의 Text를 제거하고 Image 추가하고 Color 조정

    : 이름은 'O Image', Color는 (44, 55, 89, 255)

    --> X Button에도 마찬가지로 추가

     

    3. OX Buttons의 Width와 Height 조정

     

    4. Options의 Width와 Height 조정 및 Horizontal Layout Group의 Chile Alignment 변경

     

    5. First Option Button ~ Third Option Button의 Source Image 변경 및 Width와 Height 조정

    --> 버튼 세개 다 마찬가지로 설정

     

    6. Three Option Buttons의 Width와 Height 조정

     

    >> 정답/오답 연출 구현

    구상 (정답)

    1. Front Panel에 Correct Marker 배치 및 SetActive(false)

    2. 정답 클릭 시, Timer, Question Text를 비활성화하고 Correct Marker를 활성화

    3. 클릭한 정답의 Option Button의 Color와 그 자식의 Text 또는 Image의 Color 변경

    4. Game Panel의 Color를 변경하고 배경에 'pang_effect' 가 나왔다가 사라지도록 Animation 추가

    5. Card의 잔상이 커지면서 흐려지는 Animation 추가

    6. Text가 앞에서부터 나타나도록 구현 --> Coroutine 활용?

    7. 이후 Description 화면으로 Flip하며 전환될 때, Game Panel의 Color가 서서히 변하도록 구현

     

    구상 (오답)

    1. Front Panel에 Incorrect Marker 배치 및 SetActive(false)

    --> Anchor는 Alt + Shift

     

    2. 정답 클릭 시, Timer, Question Text를 비활성화하고 Incorrect Marker를 활성화

    3. 클릭한 오답의 Option Button의 Color와 그 자식의 Text 또는 Image의 Color 변경

    4. Game Panel의 Color가 변경되도록 구현 --> 빠르게 바뀌는건지 그냥 바뀌는건지 모르겠다

    5. Card가 흔들리는 Animation 추가

    6. Text가 앞에서부터 나타나도록 구현 --> Coroutine 활용?

    7. 이후 Retry 화면으로 Flip하며 전환될 때, Game Panel의 Color가 서서히 변하도록 구현

     

    최종 코드

    >> 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<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 onComplete = null)
            {
                tempQuizCardController = _quizCardQueue.Dequeue();
                tempQuizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.Remove,
                    true, onComplete);
            }
            
            // 2. Second 영역의 카드를 First 영역으로 이동
            void SecondQuizCardToFirst(Action onComplete = null)
            {
                var firstQuizCardController = _quizCardQueue.Peek();
                firstQuizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.First,
                    true, onComplete);
            }
            
            // 3. 새로운 퀴즈 카드를 Second 영역에 생성
            void AddNewQuizCard(Action onComplete = null)
            {
                if (quizData.HasValue)
                {
                    var quizCardObject = ObjectPool.Instance.GetObject();
                    var quizCardController = quizCardObject.GetComponent<QuizCardController>();
                    quizCardController.SetQuiz(quizData.Value, OnCompletedQuiz);
                    _quizCardQueue.Enqueue(quizCardController);
                    quizCardController.SetQuizCardPosition(QuizCardController.QuizCardPositionType.Second,
                        true, onComplete);
                }
            }
            
            // 애니메이션 처리
            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;
            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
    }

     

    >> 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;
        
        // 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;
        
        // Quiz Card 위치 상태
        private IQuizCardPositionState _positionStateFirst;
        private IQuizCardPositionState _positionStateSecond;
        private IQuizCardPositionState _positionStateRemove;
        private IQuizCardPositionState _positionStateFlip;
        private IQuizCardPositionState _positionStateFlipNormal;
        private QuizCardPositionStateContext _positionStateContext;
    
        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); // 카드 위치 초기화
        }
    
        private void Start()
        {
            timer.OnTimeout = () =>
            {
                // TODO: 오답 연출
                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 퀴즈
            {
                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, bool withAnimation = true)
        {
            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()
        {
            
        }
    
        #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
    }

     

    >> MobicsTimer.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class MobicsTimer : 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;                             // 현재 Pause 상태인지 체크
    
        public delegate void MobicsTimerDelegate();
        public MobicsTimerDelegate 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 = (totalTime - CurrentTime) / totalTime;
                    headCapImage.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, fillImage.fillAmount * 360));
                    
                    var timeTextTime = totalTime - CurrentTime;
                    timeText.text = timeTextTime.ToString("F0"); // "F0" : 소수점을 표현하지 않고 정수로 표현
                }
            }
        }
    
        public void StartTimer()
        {
            _isPaused = false;
            headCapImage.gameObject.SetActive(true);
            tailCapImage.gameObject.SetActive(true);
        }
    
        public void PauseTimer()
        {
            _isPaused = true;
        }
    
        public void InitTimer()
        {
            CurrentTime = 0;
            fillImage.fillAmount = 1;
            timeText.text = totalTime.ToString("F0");
            headCapImage.gameObject.SetActive(false);
            tailCapImage.gameObject.SetActive(false);
            _isPaused = true;
        }
    }

     


    C# 단기 교육 보강

    11일차

    게임 수학 실습 - 벡터(Vector)와 스칼라(Scalar)

    : Plane의 Scale 변경

     

    >> Turret이 Player를 감지하도록 구현

    : Player는 Cube로 생성

    --> Player의 Use Gravity를 체크 해제하기

    ※ Vector3.Magnitude() vs Distance() vs SqrMagnitude()

    • Magnitude() vs Distance() 는 비슷하지만 굳이 따지면 Distance()가 더 연산이 가볍다
    • SqrMagnitude()는 루트를 씌우지 않은 값이기 때문에 정확한 크기를 구하는 것이 아니라 단순히 크기의 비교를 위해서라면 SqrMagnitude()를 사용하는 것이 연산이 가볍다.

     

    행렬 (Matrix)

     

    >> 회전 행렬 (Rotation Matrix)

     

    게임 수학 실습 - 행렬 (Matrix)

    >> 빈 게임 오브젝트로 'Board Manager' 생성 후, BoardMatrix.cs 추가 및 바인딩

    : Tile Prefab은 Package로 보내주셔서 다운 받아 사용함

     

    >> MainCamera를 원하는 위치로 옮기기

    1. Play 상태로 Scene View에서 원하는 위치로 옮긴 뒤, Main Camera를 선택하고 Ctrl + Shift + F 로 Camera 옮기기

    2. 그 위치의 MainCamera의 Transform 값 복사

     

    3. Play를 종료한 뒤, MainCamera의 Transform에 값 붙여넣기

     

    >> Button으로 Turret 변경하기

    1. Button 5개 생성

     

    2. Turret Prefab 바인딩

     

    3. 각 Button의 OnClick()에 함수 바인딩

    : index는 0부터 4까지 순차적으로 적용

     

    최종 코드

    >> TurretRotation.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class TurretRotation : MonoBehaviour
    {
        public Transform target;
        
        public float rotationSpeed = 1f;
        private float _theta;
    
        public float radius = 10f;
        public float angle = 120f;
    
        public void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Player"))
            {
                target = other.transform;
            }
        }
    
        private void Update()
        {
            Rotation();
            SetTarget();
        }
    
        private void SetTarget()
        {
            if (target == null) return;
            
            // 터렛과 타겟과의 거리
            float distance = Vector3.Distance(transform.position, target.position);
            if (distance <= radius)
            {
                Vector3 targetDir = (target.position - transform.position).normalized;
                // 터렛의 정면과 타겟과의 각도
                float targetAngle = Vector3.Angle(transform.forward, targetDir);
    
                if (targetAngle <= angle / 2) // 터렛의 공격 각도 범위 내에 있는지
                {
                    // 공격 가능 대상일 때 초록색 선
                    Debug.DrawRay(transform.position, targetDir * radius, Color.green);
                    
                    transform.LookAt(targetDir);
                }
                else
                {
                    // 공격 불가능 대상일 때 빨간색 선
                    Debug.DrawRay(transform.position, targetDir * radius, Color.red);
                }
            }
        }
    
        private void Rotation()
        {
            //transform.Rotate(Vector3.up, Time.deltaTime * rotationSpeed);
            
            _theta += Time.deltaTime * rotationSpeed;
            transform.rotation = Quaternion.Euler(Vector3.up * (45f * Mathf.Sin(_theta)));
            
            Vector3 vecA = new Vector3(0, 3, 5);
            Vector3 vecB = new Vector3(7, 2, 1);
            
            // 벡터의 크기
            float result1 = Vector3.Magnitude(vecB - vecA);
            float result2 = Vector3.Distance(vecB, vecA);
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(transform.position, radius);
    
            Vector3 leftBoundary = Quaternion.Euler(0, -angle / 2, 0) * transform.forward * radius;
            Vector3 rightBoundary = Quaternion.Euler(0, angle / 2, 0) * transform.forward * radius;
    
            Gizmos.color = Color.blue;
            Gizmos.DrawLine(transform.position, transform.position + leftBoundary);
            Gizmos.DrawLine(transform.position, transform.position + rightBoundary);
        }
    }

     

    >> BoardMatrix.cs

    using System;
    using UnityEngine;
    
    public class BoardMatrix : MonoBehaviour
    {
        public GameObject tilePrefab;
    
        public Vector2 boardSize = new Vector2(5, 5);
        
        public int[,] tileArray;
        
        public GameObject turretPrefab;
        public GameObject[] turrets;
    
        private void Start()
        {
            tileArray = new int[(int)boardSize.x, (int)boardSize.y];
            for (int x = 0; x < boardSize.x; x++)
            {
                for (int z = 0; z < boardSize.y; z++)
                {
                    GameObject tileObj = Instantiate(tilePrefab);
                    tileObj.transform.position = new Vector3(x, 0, z);
    
                    // tileArray[x, z] = 1;
                }
            }
        }
    
        private void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    
                if (Physics.Raycast(ray, out RaycastHit hit))
                {
                    int x = Mathf.RoundToInt(hit.collider.transform.position.x);
                    int z = Mathf.RoundToInt(hit.collider.transform.position.z);
    
                    if (tileArray[x, z] == 0)
                    {
                        GameObject turretObj = Instantiate(turretPrefab);
                        turretObj.transform.position = new Vector3(x, 0, z);
    
                        tileArray[x, z] = 1;
                    }
                }
            }
        }
    
        public void OnChangeTurret(int index)
        {
            turretPrefab = turrets[index];
        }
    }