본문 바로가기
Development/C#

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

by Mobics 2025. 2. 20.

 

목차


    ※ 운영체제의 신기능을 유심하게 살펴볼 필요가 있다.


    퀴즈 게임 만들기

    25.02.20

    지난 시간에 이은 활동

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

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

    ※ 아직 미구현 된 Game Scene

    - 레벨(스테이지) 팝업

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

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

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

     

    └ Button 이미지 적용

    : 강사님과 함께 적용

     

    ※ Sprite로 버튼을 눌렀을 때 Image 설정하는 방법

     

    ※ 자주 쓰는 Color를 저장하는 방법

     

    >> Sprite Editor

    : Sprite가 늘어나도 안 깨지도록 설정

     

    >> Width와 Height 계산

    : 'FirstOptionButton'에 넣은 Source Image의 'Set Native Size'가 800x146이고 Button 사이의 간격을 20으로 둔다면 'Three Option Buttons'의 Height는 (146*3) + (20*2) = 478이 된다.

    --> Options도 마찬가지로 변경

    --> Options의 PosY랑 Spacing은 임의로 설정한 것

     

    : O,X Button은 2개기 때문에 (478/2) = 229로 설정

     

    >> Option Buttons 3개의 Text와 퀴즈 내용의 Text 색 변경

    : 저장해둔 컬러 사용 --> (44, 55, 89, 255)

     

    >> 이후 나머지 Button에도 적용하자

     

    └ 폰트 교체

    : 에스코어드림 폰트 --> 4(Regular), 5(Medium) 사용

    https://s-core.co.kr/company/font/

     

    에스코어

    에스코어는 디지털 혁신을 위한 고급 프로페셔널 서비스를 제공합니다. 매니지먼트 컨설팅과 소프트웨어 테크놀로지 서비스 오퍼링을 살펴보세요.

    s-core.co.kr

     

    >> Font Asset 생성

    32-126,44032-55203,12593-12643,8200-9900

     

    >> Text에 적용

     

    └ 정답/오답 연출 구현

    : DOTween을 사용하지 않고 Unity에 있는 Animation 만으로 구현해봄

     

    >> Front Panel의 자식으로 빈 게임 오브젝트로 'Result Panel' 생성

    --> 원래는 Correct와 Incorrect를 따로 구현하려고 해서, Horizontal Layout Group 을 추가하고 Spacing을 200 줬었다

     

    >> Result Panel의 자식으로 빈 게임 오브젝트로 'Result Panel Correct' 생성

    : 원래는 Result Panel Correct와 Result Panel Incorrect로 분리해서 구현하려 했으나, 하나에 전부 구현하게 됨

    --> 이미 Animation을 만든 후라서 이를 수정하면 만든 Animation이 전부 깨지게 되므로 놔둠 (아직 구현하지 않았다면 Result Panel Correct도 필요없으니 삭제하고 Result Panel에 만들자)

     

    >> Result Panel Correct 구현

    - Pang Image

     

    - Circle Background Image

    --> Set Native Size

     

    - Circle Stroke Image

    --> Set Native Size

     

    - Point Image 3개

    --> Set Native Size

     

    - Marker Correct / Marker Incorrect

    : Marker Correct의 이름이 Marker (Animation 깨짐 때문에 변경 못함)

     

     

    - Text Correct / Text Incorrect

     

    >> Quiz Card에 Animator 추가하여 Animation 구현

    : Project에 Animator Controller를 생성하여 바인딩

     

    >> Anim 생성 및 Animation 구현

    : 'QuizCardCorrect' 라는 이름으로 Clip 생성

    • 방법 1. 원하는 프레임에서 원하는 값으로 설정한 뒤, 'Add Key'로 추가
    • 방법 2. 녹화 버튼을 눌러서 시작 프레임에서 원하는 값과 끝 프레임에서 원하는 값을 모두 설정하고 녹화 중지

     

    >> Animation 설정

    - Circle Background Image : [0 ~ 60프레임] Color의 Alpha 값을 0 ~ 1로 변경

    - Circle Stroke Image : [0 ~ 60프레임] Fill Amount 값을 0 ~ 1로 변경

    - Marker의 Stroke : [60 ~ 75프레임] Fill Amount 값을 0 ~ 1로 변경

    - Marker의 Stroke (1) : [75 ~ 90프레임] Fill Amount 값을 0 ~ 1로 변경

    - Pang Image : [0 ~ 30프레임] Color의 Alpha 값을 0 ~ 40으로 변경 / [0 ~ 170프레임] Rotation.Z 값을 0 ~ 360으로 변경

    - Point Image 3개 : 셋 다 0프레임에서 Image의 Active를 false

    └ Point Image : [100프레임] Image의 Active를 true

    └ Point Image (1) : [110프레임] Image의 Active를 true

    └ Point Image (2) : [90프레임] Image의 Active를 true

    - Text : [45 ~ 60프레임] Color의 Alpha 값을 0 ~ 1로 변경, Scale.x와 Scale.y를 3 ~ 1로 변경

     

    ※ Animation의 지속 시간을 늘리기 위해 170프레임에서 'Add Key' 해주기

     

    >> 완성된 모습

     

    >> Incorrect Animation 만들기

    : Animation Clip 추가 후, Correct로 만든 Animation 전부 복사 붙여넣기

     

    >> Animation 설정

    - Quiz Card : [0 ~ 18프레임] Rotation.Z 값을 10 ~ -10으로 변경 / [18 ~ 35프레임] Rotation.Z 값을 -10 ~ 0으로 변경

    - Circle Background Image : [0 ~ 60프레임] Color의 Alpha 값을 0 ~ 1로 변경 --> Color를 저장해둔 분홍색으로 변경

    - Circle Stroke Image : [0 ~ 60프레임] Fill Amount 값을 0 ~ 1로 변경

    - Marker Incorrect의 Stroke : [60 ~ 75프레임] Fill Amount 값을 0 ~ 1로 변경

    - Marker Incorrect의 Stroke (1) : [75 ~ 90프레임] Fill Amount 값을 0 ~ 1로 변경

    - Point Image 3개 : 셋 다 0프레임에서 Image의 Active를 false

    └ Point Image : [100프레임] Image의 Active를 true

    └ Point Image (1) : [110프레임] Image의 Active를 true

    └ Point Image (2) : [90프레임] Image의 Active를 true

    - Text Incorrect : [45 ~ 60프레임] Color의 Alpha 값을 0 ~ 1로 변경, Scale.x와 Scale.y를 3 ~ 1로 변경

     

    --> Correct 대비 변경 사항

    : Quiz Card 추가, Circle Background Image의 Color 변경, Marker Incorrect의 Stroke로 변경

     

    ※ Quiz Card의 Rotation 변화를 Curves로 더 다채롭게 바꾸기

     

    >> 완성된 모습

     

    >> Animator 설정

    : New State를 만들어서 사진과 같이 세팅

     

    >> Parameter 생성

    : Trigger 타입으로 'correct'와 'incorrect' 생성 후 Conditions에 각각 맞게 추가

    --> (Idle -> QuizCardIncorrect)에는 incorrect 추가

     

    >> 코드로 작성

    : QuizCardController.cs에 Animation 부분 추가

     

    >> Quiz Card Controller에 바인딩

     

    >> Animation이 끝나고 함수 호출하도록

    : 170프레임에 Event 추가

    --> Incorrect에도 마찬가지로 적용

     

    ※ 이후 세세한 디테일은 스스로 해보자

    : 현재 Flip이 되지않는 버그가 있음

     

    Main Scene

    >> Menu Button들의 Image 변경 및 자식의 Text 삭제

    • Play Button
    • Shop Button
    • Stage Button
    • Leaderboard Button
    • Settings Button

    --> 나머지 버튼들도 동일한 방식으로 추가

     

    >> Menu Buttons의 위치 조정

     

    >> Remove Ads Button 추가

     

    >> Popup Panel 구현

    1. 'Shop Panel' 생성 --> 저장해둔 색으로 변경

     

    2. 'Shop Panel'의 자식으로 'Panel Background' 생성

    : Panel을 생성하여 Top값 조정 및 Source Image를 'None'으로 바꾸고 Color의 Alpha값을 255로 수정

     

    3. 'Shop Panel'의 자식으로 'Panel Top Image' 생성

    : Image로 생성

    ※ Image Size가 안 맞다면, Image Type을 Sliced로 바꾸기 전에 Simple 상태에서 Set Native Size를 누르고 Sliced로 변경

    --> Anchor는 Alt + Shift

     

    >> 'modal_popup_bg'를 Sprite Editor로 수정

     

    4. 'Shop Panel'의 자식으로 'Close Button' 생성

    : Button으로 생성

    --> Anchor는 Alt + Shift

     

    5. 'Shop Panel'의 자식으로 'Title Text (TMP)' 생성

    : Text로 생성 --> Color는 저장해 둔 색으로 변경

    --> Anchor는 Alt + Shift

     

    >> 지금까지 만든 모습

     

    앞으로 구현해야하는 모바일 기능

    • Admob --> 구글에서 제공해주는 SDK 이용
    • GPGS(Google Play Game Services) : Leaderboard, 업적 등 --> 구글에서 제공해주는 SDK 이용
    • In-App 결제 --> Unity에서 제공해주는 SDK 이용

    --> Native 개발을 알면 전부 직접 만들 수 있고 Admob을 제외하면 직접 만들어 쓰는게 제일 좋지만, 어렵다.

     

    최종 코드

    >> 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 퀴즈
            {
                oxButtons.SetActive(true);
                threeOptionButtons.SetActive(false);
            }
            
            this.onCompleted = onCompleted;
            
            heartPanelController.InitHeartCount(GameManager.Instance.heartCount);
        }
    
        /// <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()
        {
            
        }
    
        #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
    }

     


    C# 단기 교육 보강

    12일차

    Unity C# 프로그래밍 중급 문법

    Event와 Unity Event

    • Event : 외부 클래스에서 호출하지 못하도록 한다. --> 안정적이다.

    ※ 코드로만 작성해야한다.

     

    • Unity Event

    : Inspector 상에서 바인딩하여 사용 가능하다

    - UnityEvent 추가 - AddListener(함수명);

    - UnityEvent 삭제 - RemoveListener(함수명); --> 전체 삭제 : RemoveAllListener();

     

    Delegate

    : 함수를 참조하여 실행하는 대리자

    - 장점

    • 여러 함수를 엮어서(체인) 쓸 수 있다. --> 여러 명에서 협업할 때 각자 함수를 유연하게 넣었다 뺄 수 있다.
    • 모듈화된 코드를 작성할 수 있다.

     

    익명 함수와 Lambda

    : 가상의 함수를 만들어서 사용하는 방법

     

    >> 'Game Math' Scene에서 Turret 생성 Button에 대해 Delegate 활용

    : BoardMatrix.cs

    using System;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class BoardMatrix : MonoBehaviour
    {
        public GameObject tilePrefab;
    
        public Vector2 boardSize = new Vector2(5, 5);
        
        public int[,] tileArray;
        
        public GameObject turretPrefab;
        public GameObject[] turrets;
    
        public Button[] buttons; // 버튼 5개 받기
    
        private void Awake()
        {
            // 버튼 5개 세팅
            buttons[0].onClick.AddListener(() => OnChangeTurret(0));
            buttons[1].onClick.AddListener(() => OnChangeTurret(1));
            buttons[2].onClick.AddListener(() => OnChangeTurret(2));
            buttons[3].onClick.AddListener(() => OnChangeTurret(3));
            buttons[4].onClick.AddListener(() => OnChangeTurret(4));
        }
    
        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];
        }
    }

     

    Closure 이슈

    : 외부 함수의 변수를 내부 함수가 참조할 때 발생

    --> 캐싱