본문 바로가기
Development/C#

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

by Mobics 2025. 2. 17.

 

목차


    퀴즈 게임 만들기

    25.02.17

    타이머 만들기

    >> Head Cap, Tail Cap 생성

    : Image로 생성

    --> Anchor는 Alt + Shift

     

    >> Timer의 Width, Height 조정

     

    >> Fill Image 수정

    Clockwise : Image를 시계 방향으로 차오르게 할 지(체크), 반시계 방향으로 차오르게 할 지(체크 해제)

     

    >> Mobics Timer.cs에 HeadCap, TailCap 바인딩

     

    >> Timer Prefab화

    : 이후 Hierarchy에서 Timer 제거

     

    >> GamePanel의 GamePanelController를 다시 활성화

     

    >> Quiz Card Prefab에 Timer 추가

    --> Anchor는 Alt + Shift

     

    >> Timer의 색 변경

    • Fill Image, Head Cap, Tail Cap, Time Text : (44, 55, 89, 255)
    • Background Image (242, 242, 242, 255)

     

    >> Timer에 Time Text 바인딩

     

    ※ Prefab이 아닌 Hierarchy에서 수정한 값을 Prefab에 개별적으로 적용하기

     

    활동

    • 타이머를 Quiz Card에 추가
    • 10초 동안 퀴즈를 못 풀면 오답 처리
    • 현재 진행 중인 퀴즈의 타이머만 동작하게 구현
    • 타이머가 끝나기 전에 퀴즈를 풀었을 때 타이머 처리

     

    >> 구현

    : 대부분 코드로 구현, QuizCardController.cs에 Timer 바인딩

     

    Heart 구현

    : Game Panel의 자식으로, 빈 게임 오브젝트를 생성

    --> 이름을 HeartPanel로 수정하고 Width 수정하고 HeartPanelController.cs 추가

     

     

    >> Heart Image, Heart Count 생성

    : Heart Panel의 자식으로, Heart Image는 Image로, Heart Count Text (TMP)는 Text로 생성

    --> Anchor는 Alt + Shift

    --> 'Heart Count'를 'Heart Count Text (TMP)'로 이름 변경

     

    └ 하트 감소 구현

    : Heart Image의 자식으로 Image를 'Heart Remove Image'라는 이름으로 생성

     

    >> HeartPanelController.cs에 바인딩

    : Heart Remove Image, Heart Count Text 바인딩

     

    >> 하트 수의 자릿수에 따라 텍스트 위치 조정

    : 하트 수가 1자리 수일 때와 2자리 수일 때가 Text의 간격이 다르기 때문에 이를 조정

    --> HeartPanel의 Width를 글자 수에 따라 변경

    --> HeartPanel의 Width가 한 자리 수일 때는 130, 두 자리 수 일 때는 160이 적당하다

    --> HeartCountText의 Width가 한 자리 수일 때는 31 정도, 두 자리 수 일 때는 61 정도가 적당하다

    --> 코드로 이를 조절

     

    ※ 'Content Size Filter' 라는 Component를 추가해서 조정하는 방법도 있음

    : Heart Count Text에 추가 --> 이 방법 사용

     

    >> Test용 버튼 추가

    : 'Canvas'의 자식으로 빈 게임 오브젝트로 Test Buttons 추가 및 Vertical Layout Group 추가

    --> PosX, PosY, Width, Height는 임의로 조정한 것

     

    >> Test Buttons의 자식으로 Button 추가

    : Width, Height 조정, Text 수정

    --> Button 3개 전부 동일하게 적용

    --> Text는 각각 'Remove Heart', 'Add Heart', 'Empty Heart'

     

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

    --> 나머지 버튼에도 각각 'AddHeart(int)', 'EmptyHeart()' 바인딩

    --> AddHeart(int)의 int값은 상관없지만 일단 5로 설정

     

    └ 하트 증가 구현

    : 하트 감소 코드와 거의 동일하기 때문에 하트 감소 코드를 따로 빼서 ChangeTextAnimation()으로 만듦

     

    ※ DOTween.Sequence()

    : 여러 복잡한 애니메이션을 순차적으로 적용

     

    └ 하트가 없을 때 구현

    : DOPunchPosition을 이용하여 하트 수가 흔들리는 Animation 추가

     

    └ Heart Sound 추가

    ※ Asset 추가

    https://assetstore.unity.com/packages/audio/sound-fx/free-casual-game-sfx-pack-54116

     

    FREE Casual Game SFX Pack | 음향 효과음 | Unity Asset Store

    Layer in the sounds of FREE Casual Game SFX Pack from Dustyroom for your next project. Browse all audio options on the Unity Asset Store.

    assetstore.unity.com

     

    >> 각 동작에 맞는 효과음 바인딩

     

    >> 유저의 설정값에 따라 효과음을 재생하도록 구현

     

    >> Heart Panel Prefab화 후, Hierarchy에서 제거

    : TestButtons에 있는 Button들의 OnClick()이 전부 해제됐을 것, 테스트할 때 사용하려면 다시 연결해주어야 한다.

     

    활동

    : Heart Panel을 Quiz Card에 적용하기

     

    └ 활동 해보기

    1. Quiz Card Prefab에 Incorrect Back Panel의 자식으로 HeartPanel 추가

    2. QuizCardController.cs에 HeartPanel 바인딩

    3. 배경이랑 같은 흰색이라 잘 안 보이기 때문에 Color 조정

    --> 'Heart Image', 'Heart Remove Image'를 (242, 242, 242, 255)

    --> 'Heart Count Text'를 (0, 0, 0, 255)

     

    최종 코드

    >> 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;
        }
    
        public void PauseTimer()
        {
            _isPaused = true;
        }
    
        public void InitTimer()
        {
            CurrentTime = 0;
            fillImage.fillAmount = 1;
            timeText.text = totalTime.ToString("F0");
            headCapImage.gameObject.SetActive(true);
            tailCapImage.gameObject.SetActive(true);
            _isPaused = true;
        }
    }

     

    >> 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에서 마지막으로 이동 --> 카드가 앞으로 배치됨
                
                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);
        }
    }

     

    >> 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;
        
        // Timer
        [SerializeField] private MobicsTimer timer;
        
        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, 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)
        {
            // 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--;
                heartCountText.text = GameManager.Instance.heartCount.ToString(); // Incorrect Back Panel에 heartCount 표시
                SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
                
                // 타이머 초기화 및 시작
                timer.InitTimer();
                timer.StartTimer();
            }
            else
            {
                // 하트가 부족해서 Retry 불가
                // TODO: 하트 부족 알림 구현
            }
        }
    
        #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(10);
        }
    
        /// <summary>
        /// Heart Panel에 하트 수 초기화
        /// </summary>
        /// <param name="heartCount">하트 수</param>
        public void InitHeartCount(int heartCount)
        {
            _heartCount = heartCount;
            heartCountText.text = _heartCount.ToString();
        }
    
        private void ChangeTextAnimation(bool isAdd)
        {
            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(() =>
                {
                    
                });
            });
        }
    
        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()
        {
            // 효과음 재생
            if (UserInformations.IsPlaySFX)
                _audioSource.PlayOneShot(heartRemoveAudioClip);
            
            // 하트 초기화
            heartRemoveImageObject.SetActive(true);
            heartRemoveImageObject.transform.localScale = Vector3.zero;
            heartRemoveImageObject.GetComponent<Image>().color = Color.white;
            
            // 하트 사라지는 연출
            heartRemoveImageObject.transform.DOScale(3f, 1f);
            heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
    
            // 하트 개수 텍스트가 감소되는 연출
            DOVirtual.DelayedCall(1f, () =>
            {
                ChangeTextAnimation(false);
            });
        }
    }

     

    >> UserInfromations.cs

    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); }
        }
        
        // 효과음 재생 여부
        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); }
        }
    }

     


    C# 단기 교육 보강

    9일차

    Hanoi Tower 복잡한 코드 수정

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    namespace Hanoi_Tower
    {
        public class GameManager : MonoBehaviour
        {
            [SerializeField] private GameObject donutPrefab;
            
            public enum HanoiLevel { LV1 = 3, LV2 = 4, LV3 = 5 } // 생성되는 Donut 개수
            public HanoiLevel hanoiLevel;
            
            public DonutBar[] donutBars;
    
            public bool isSelected = false;
            
            // Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
            public List<GameObject> leftBar = new();
            public List<GameObject> centerBar = new();
            public List<GameObject> rightBar = new();
            
            public GameObject selectedDonut;
    
            IEnumerator Start()
            {
                for (int i = (int)hanoiLevel; i >= 1; i--)
                {
                    // 생성한 Donut을 LeftBar에 Push (index 0 == left)
                    donutBars[0].PushDonut(CreateDonut(donutPrefab, i));
                    
                    yield return new WaitForSeconds(1f);
                }
            }
    
            private GameObject CreateDonut(GameObject prefab, int i)
            {
                GameObject obj = Instantiate(prefab); // 도넛 생성
                // 도넛 생성 위치, 회전 설정
                // Board의 X의 Scale이 10이라서 (int)DonutBar.BarType.LEFT에 0.1f를 곱할 필요가 없다.
                obj.transform.SetPositionAndRotation
                    (new Vector3((int)DonutBar.BarType.LEFT, 3.5f, 0f), Quaternion.identity);
                obj.name = "Donut_" + i; // 도넛 이름 설정
                obj.GetComponent<Donut>().donutNumber = i; // 도넛에 번호 부여
                obj.transform.localScale = Vector3.one * (i * 0.3f + 1f); // 도넛 크기 설정
                
                return obj;
            }
        }
    }

     

    >> DonutBar.cs

    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using Hanoi_Tower; // namespace를 사용하기 위해 선언
    
    public class DonutBar : MonoBehaviour
    {
        // position.x 값인데, enum은 int이므로 10배 크게 작성
        public enum BarType { LEFT = -3, CENTER = 0, RIGHT = 3 }
        public BarType barType;
    
        public Stack<GameObject> stack = new();
    
        // Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
        public GameManager gameManager;
    
        void OnMouseDown()
        {
            if (!gameManager.isSelected) // 도넛을 가져올 기둥을 선택
            {
                gameManager.selectedDonut = PopDonut();
            }
            else // 도넛을 넣을 기둥 선택
            {
                PushDonut(gameManager.selectedDonut);
            }
        }
    
        private bool CheckDonutNumber(GameObject pushDonut)
        {
            bool result = true; // 처음에 도넛을 Push해야하기 때문에 Default 값을 true로 설정
    
            if (stack.Count > 0)
            {
                int pushNumber = pushDonut.GetComponent<Donut>().donutNumber;
                int peekNumber = stack.Peek().GetComponent<Donut>().donutNumber;
                
                result = pushNumber < peekNumber; // 도넛이 하노이 로직에 맞는지 확인
    
                if (!result)
                    Debug.Log($"놓으려는 도넛은 {pushNumber}이고, 해당 기둥의 도넛은 {peekNumber}입니다.");
            }
    
            return result;
        }
    
        public void PushDonut(GameObject pushDonut)
        {
            if (!CheckDonutNumber(pushDonut)) return; // 도넛 넘버가 하노이 로직에 맞지 않음
    
            gameManager.isSelected = false;
            gameManager.selectedDonut = null;
            
            stack.Push(pushDonut);
            pushDonut.transform.SetPositionAndRotation(new Vector3((int)barType, 3.5f, 0f), Quaternion.identity);
    
            switch (barType)
            {
                case BarType.LEFT:
                    gameManager.leftBar = stack.ToList();
                    break;
                case BarType.CENTER:
                    gameManager.centerBar = stack.ToList();
                    break;
                case BarType.RIGHT:
                    gameManager.rightBar = stack.ToList();
                    break;
            }
        }
    
        public GameObject PopDonut()
        {
            GameObject obj = null;
            if (stack.Count > 0)
            {
                obj = stack.Pop();
                gameManager.isSelected = true;
    
                switch (barType)
                {
                    case BarType.LEFT:
                        gameManager.leftBar = stack.ToList();
                        break;
                    case BarType.CENTER:
                        gameManager.centerBar = stack.ToList();
                        break;
                    case BarType.RIGHT:
                        gameManager.rightBar = stack.ToList();
                        break;
                }
            }
            
            return obj;
        }
    }

     

    Hanoi Tower에 재귀함수 적용

    : Button을 누르면 Debug.Log()로 정답을 알려주도록 구현

     

    >> Button 추가 및 OnClick()에 GameManager 바인딩

     

    >> 코드로 구현

    : GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    namespace Hanoi_Tower
    {
        public class GameManager : MonoBehaviour
        {
            [SerializeField] private GameObject donutPrefab;
            
            public enum HanoiLevel { LV1 = 3, LV2 = 4, LV3 = 5 } // 생성되는 Donut 개수
            public HanoiLevel hanoiLevel;
            
            public DonutBar[] donutBars;
    
            public bool isSelected = false;
            
            // Unity Inspector에서 보기 위해 억지로 하드코딩 --> 실제로는 쓰지 말자
            public List<GameObject> leftBar = new();
            public List<GameObject> centerBar = new();
            public List<GameObject> rightBar = new();
            
            public GameObject selectedDonut;
    
            IEnumerator Start()
            {
                for (int i = (int)hanoiLevel; i >= 1; i--)
                {
                    // 생성한 Donut을 LeftBar에 Push (index 0 == left)
                    donutBars[0].PushDonut(CreateDonut(donutPrefab, i));
                    
                    yield return new WaitForSeconds(1f);
                }
            }
    
            private GameObject CreateDonut(GameObject prefab, int i)
            {
                GameObject obj = Instantiate(prefab); // 도넛 생성
                // 도넛 생성 위치, 회전 설정
                // Board의 X의 Scale이 10이라서 (int)DonutBar.BarType.LEFT에 0.1f를 곱할 필요가 없다.
                obj.transform.SetPositionAndRotation
                    (new Vector3((int)DonutBar.BarType.LEFT, 3.5f, 0f), Quaternion.identity);
                obj.name = "Donut_" + i; // 도넛 이름 설정
                obj.GetComponent<Donut>().donutNumber = i; // 도넛에 번호 부여
                obj.transform.localScale = Vector3.one * (i * 0.3f + 1f); // 도넛 크기 설정
                
                return obj;
            }
    
            public void OnShowAnswer()
            {
                ShowAnswer((int)hanoiLevel, 0, 1, 2);
            }
            
            /// <summary>
            /// 재귀함수를 이용하여 정답을 알려주는 함수
            /// </summary>
            /// <param name="count">원반 개수</param>
            /// <param name="from">시작 기둥</param>
            /// <param name="temp">임시 기둥</param>
            /// <param name="to">목표 기둥</param>
            private void ShowAnswer(int count, int from, int temp, int to)
            {
                if (count == 0) return;
    
                if (count == 1)
                    Debug.Log($"{count}번 도넛을 {from}에서 {to}로 이동");
                else
                {
                    ShowAnswer(count - 1, from, to, temp);
                    Debug.Log($"{count}번 도넛을 {from}에서 {to}로 이동");
                    ShowAnswer(count - 1, temp, from, to);
                }
            }
        }
    }

     

    Lotto

    : Swap, Shuffle 등을 활용

     

    >> 빈 게임 오브젝트로 'Lotto Creator'를 만들고, LottoCreator.cs 추가'

     

    >> 화면 비율 조정

     

    >> Image 생성 후 위치 조정

    : 저는 Vertical Layout Group를 사용

     

    >> Width, Height 조정

     

    >> 마지막 공은 Bonus 공이니 따로 색을 바꿔 표시

     

    >> 공에 전부 Text 추가

     

    --> Text를 복붙

    : 첫 Text를 복붙하고 각각 Image의 자식 오브젝트로 옮긴 뒤, PosX를 0으로 바꾸면 사진과 같이 자기 자리를 찾아감

     

    >> 이름을 Lotto Ball로 바꾸고 LottoBall.cs 추가

     

    >> Lotto Ball들을 LottoCreator.cs에 바인딩

     

    >> 버튼 생성

    : 위치와 크기 바꿔주고 OnClick() 함수 바인딩

     

    └ 코드

    >> LottoCreator.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Random = UnityEngine.Random;
    
    public class LottoCreator : MonoBehaviour
    {
        // Swap 자리 바꾸기
        public int[] numbers = new int[45]; // 45자리의 숫자 배열 생성
        public LottoBall[] lottoBalls; // 7개의 로또볼
        
        private int _shakeCount = 1000; // 섞는 횟수
    
        private void Start()
        {
            for (int i = 0; i < 45; i++) // numbers의 개수는 45개로 고정이기 때문에 numbers.Length로 할 필요 없다.
                numbers[i] = i + 1; // 로또번호를 1부터 45까지 등록
        }
    
        public void OnCreateLotto()
        {
            for (int i = 0; i < _shakeCount; i++) // 셔플 기능
            {
                int ranInt1 = Random.Range(0, numbers.Length);
                int ranInt2 = Random.Range(0, numbers.Length);
                
                //var temp = numbers[ranInt1];
                //numbers[ranInt1] = numbers[ranInt2];
                //numbers[ranInt2] = temp;
                
                (numbers[ranInt1], numbers[ranInt2]) = (numbers[ranInt2], numbers[ranInt1]);
            }
            
            // 섞은 숫자 배열의 앞 7개를 오름차순 정렬
            int[] sortArray = new int[7];
            for (int i = 0; i < 7; i++)
                sortArray[i] = numbers[i];
            
            Array.Sort(sortArray);
    
            for (int i = 0; i < lottoBalls.Length; i++) // 로또볼에 적용
                lottoBalls[i].textNumber.text = sortArray[i].ToString();
    
            StartCoroutine(ShowBall());
        }
    
        IEnumerator ShowBall()
        {
            foreach (var ball in lottoBalls)
            {
                ball.gameObject.SetActive(true);
                yield return new WaitForSeconds(0.5f);
            }
        }
    
        private void Swap()
        {
            int ranInt1 = Random.Range(0, numbers.Length);
            int ranInt2 = Random.Range(0, numbers.Length);
            int temp = 0;
    
            temp = numbers[ranInt1];
            numbers[ranInt1] = numbers[ranInt2];
            numbers[ranInt2] = temp;
        }
    
        private void Shuffle()
        {
            for (int i = 0; i < _shakeCount; i++)
            {
                Swap();
            }
        }
    }

     

    >> LottoBalls.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    
    public class LottoBall : MonoBehaviour
    {
        public TMP_Text textNumber;
    
        private bool _isScale;
    
        private void Start()
        {
            textNumber = transform.GetChild(0).GetComponent<TMP_Text>();
            
            transform.localScale = Vector3.zero;
            gameObject.SetActive(false);
        }
    
        private void Update()
        {
            if (!_isScale)
            {
                transform.localScale += Vector3.one * (Time.deltaTime * 2f);
    
                if (transform.localScale.x >= 1f)
                {
                    _isScale = true;
                    transform.localScale = Vector3.one;
                }
            }
        }
    }