본문 바로가기
Development/C#

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

by Mobics 2025. 2. 6.

 

목차


    ※ Rider에서 TODO를 확인하는 법


    틱택토 게임 만들기

    25.02.06

    개선 항목

    1. PanelController의 구동 방식이 Panel들을 Hierarchy에 안 보이게 배치해두고 필요할 때 보이도록 옮기는 것이었는데, Scene이 분리되면서 문제가 생김

    >> 해결방법

    : Prefab화 해서 필요할 때 Instantiate하는 방식으로 변경

     

    >> PanelManager.cs 삭제

    : 그에 따라 에러나는 코드 및 불필요한 코드 수정

     

    >> Panel들 Prefab화

    : ConfirmPanel과 SettingsPanel을 Prefab화 한 다음 Hierarchy에서 삭제, 그리고 다시 세팅

    --> ConfirmPanel

    --> SettingsPanel

     

    2. GameManager가 GameScene에서 MainScene으로 이동하면 SerializeField로 연결돼있던 것들이 끊어지게 되고 나중에 다시 GameScene으로 돌아오면 연결이 끊어진 상태로 돌아오면서 문제가 생김 --> 게임을 처음 플레이하면 괜찮겠지만 반복 플레이하면 문제가 발생

    >> 해결방법

    1. 'DontDestroyOnLoad'를 없애서 GameManager가 없어지도록 --> GameScene에서 선택한 것과 정보들을 MainScene으로 전달해야해서, 없앤다면 Scene의 Data를 어떻게 전달할지 고민해야 한다.
    2. GameManager를 개선 --> 이 방법을 채택

    >> GameManager.cs 개선

    : SerializeField를 제거하고 FindObjectOfType함수 활용 --> 그럼 왜 애초에 안 썼는가? 비용이 비싸서

    • GameScene에 있던 GameManager를 MainScene으로 옮김
    • 코드 수정

    >> GameManager에 Prefab 바인딩

    : 수정 전과 후의 차이가 무엇인가?

    --> Hierarchy에 있는 것들은 해당 Scene이 Load 됐을 때 메모리 상에 만들어지고 Scene이 바뀌면 삭제된다. 하지만 Prefab은 Project에 있는 파일이기 때문에 Scene이 바뀌어도 따로 삭제되지 않는다.

     

    GameOver 시, 게임 재시작 구현

    >> GameOverButton 바인딩

     

    SettingsPanel 만들기

    >> Close Button의 OnClick() 바인딩

     

    >> DOTween을 활용하여 SettingsPanel 띄우기

    - SettingsPanel에 Canvas Group을 추가하여 자식 오브젝트들을 한꺼번에 조정

    - SettingsPanelController.cs에 Panel 바인딩

     

    >> 배경이 덜 어두워지는 것 같아서 Alpha값 수정

     

    ConfirmPanel 만들기

    >> 배경이 덜 어두워지는 것 같아서 Alpha값 수정

     

    >> ConfirmPanelController.cs에 Panel 바인딩

     

    >> CloseButton의 OnClick() 바인딩

     

    버그 해결

    버그 종류

    1. 중복 체크 방지
    2. ConfirmPanel에서 Block을 클릭해도 체크되지 않도록
    3. Confirm을 눌렀을 때, ConfirmPanel의 Animation이 실행되고 Scene이 전환되도록
    4. 경우에 따라 첫 Scene의 OnSceneLoaded가 호출이 안 되는 경우를 해결 --> Destroy 후에는 OnSceneLoaded가 할당되지 않도록

    >> ConfirmPanel Animation 버그의 여러 해결 방법

    1. Hide()에 delegate를 매개변수로 넣어줘서 해결 (강사님 방법)

     

    2. Invoke에 Delay 넣기

    public void OnClickConfirmButton()
    {
        Invoke(nameof(InvokeConfirmButton), 1f);
        Hide();
    }
    
    private void InvokeConfirmButton()
    {
        _onConfirmButtonClick?.Invoke();
    }

     

    3. Task를 사용하여 Delay 넣기

    public async void OnClickConfirmButton()
    {
        Hide();
        await Task.Delay(500);
        onConfirmButtonClick?.Invoke();
    }

     

    4. bool을 활용하여 처리

    private bool _closePanel = false;
    
    public void OnClickConfirmButton()
    {
        _closePanel = true;
        Hide();
    }
    
    void OnDestroy()
    {
        if (_closePanel)
        {
           _onConfirmButtonClick?.Invoke();
        }
    }

     

    5. DOTween 기능을 활용 (AI가 알려준 방법)

    using DG.Tweening;
    
    public void OnClickConfirmButton()
    {
        DOVirtual.DelayedCall(1f, () =>
        {
            _onConfirmButtonClick?.Invoke();
            Hide();
        });
    }

     

    최종 코드

    >> Singleton.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public abstract class Singleton<T> : MonoBehaviour where T : Component
    {
        private static T _instance;
    
        public static T Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = FindObjectOfType<T>();
                    if (_instance == null)
                    {
                        GameObject obj = new GameObject();
                        obj.name = typeof(T).Name;
                        _instance = obj.AddComponent<T>();
                    }
                }
                return _instance;
            }
        }
    
        private void Awake()
        {
            if (_instance == null)
            {
                _instance = this as T;
                DontDestroyOnLoad(gameObject);
                // 경우에 따라 첫 Scene의 OnSceneLoaded가 호출이 안 되는 경우를 해결
                OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
                
                // Scene 전환 시, 호출되는 Action Method 할당
                SceneManager.sceneLoaded += OnSceneLoaded;
            }
            else
            {
                Destroy(gameObject);
            }
        }
    
        // Destroy 후에는 OnSceneLoaded가 할당하지 않도록
        private void OnDestroy()
        {
            SceneManager.sceneLoaded -= OnSceneLoaded;
        }
    
        protected abstract void OnSceneLoaded(Scene scene, LoadSceneMode mode);
    }

     

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : Singleton<GameManager>
    {
        [SerializeField] private GameObject settingsPanel;
        [SerializeField] private GameObject confirmPanel;
        
        private BlockController _blockController;
        private GameUIController _gameUIController;
        private Canvas _canvas;
    
        public enum PlayerType { None, PlayerA, PlayerB }
        private PlayerType[,] _board;
        
        public enum TurnType { PlayerA, PlayerB }
        private enum GameResult { None, Win, Lose, Draw }
        
        public enum GameType { SinglePlayer, DualPlayer }
    
        public void ChangeToGameScene(GameType gameType)
        {
            SceneManager.LoadScene("Game");
        }
    
        public void ChangeToMainScene()
        {
            SceneManager.LoadScene("Main");
        }
    
        public void OpenSettingsPanel()
        {
            if (_canvas != null)
            {
                var settingsPanelObject = Instantiate(settingsPanel, _canvas.transform);
                settingsPanelObject.GetComponent<PanelController>().Show();
            }
        }
    
        public void OpenConfirmPanel(string message, ConfirmPanelController.OnConfirmButtonClick onConfirmButtonClick)
        {
            if (_canvas != null)
            {
                var confirmPanelObject = Instantiate(confirmPanel, _canvas.transform);
                confirmPanelObject.GetComponent<ConfirmPanelController>()
                    .Show(message, onConfirmButtonClick);
            }
        }
    
        /// <summary>
        /// 게임 시작
        /// </summary>
        private void StartGame()
        {
            // _board 초기화
            _board = new PlayerType[3, 3];
            
            // Block 초기화
            _blockController.InitBlocks();
            
            // Game UI 초기화
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
            
            //panelManager.ShowPanel(PanelManager.PanelType.BattlePanel);
            
            // 턴 시작
            SetTurn(TurnType.PlayerA);
        }
        
        /// <summary>
        /// 게임 오버 시, 호출되는 함수
        /// gameResult에 따라 결과 출력
        /// </summary>
        /// <param name="gameResult">win, lose, draw</param>
        private void EndGame(GameResult gameResult)
        {
            // 게임오버 표시
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
            _blockController.OnBlockClickedDelegate = null; // 게임이 끝났을 때 board를 Click해도 바뀌지 않도록
            
            // TODO: 나중에 구현!
            switch (gameResult)
            {
                case GameResult.Win:
                    break;
                case GameResult.Lose:
                    break;
                case GameResult.Draw:
                    break;
            }
        }
    
        /// <summary>
        /// _board에 새로운 값을 할당하는 함수
        /// </summary>
        /// <param name="playerType">할당하고자 하는 플레이어 타입</param>
        /// <param name="row">Row</param>
        /// <param name="col">Col</param>
        /// <returns>False : 할당할 수 없음, True : 할당이 완료됨</returns>
        private bool SetNewBoardValue(PlayerType playerType, int row, int col)
        {
            if (_board[row, col] != PlayerType.None) return false; // 중복 체크 방지
            
            if (playerType == PlayerType.PlayerA)
            {
                _board[row, col] = playerType;
                _blockController.PlaceMarker(Block.MarkerType.O, row, col);
                return true;
            }
            else if (playerType == PlayerType.PlayerB)
            {
                _board[row, col] = playerType;
                _blockController.PlaceMarker(Block.MarkerType.X, row, col);
                return true;
            }
            return false;
        }
    
        private void SetTurn(TurnType turnType)
        {
            switch (turnType)
            {
                case TurnType.PlayerA:
                    _gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnA);
                    _blockController.OnBlockClickedDelegate = (row, col) =>
                    {
                        if (SetNewBoardValue(PlayerType.PlayerA, row, col))
                        {
                            var gameResult = CheckGameResult();
                            if (gameResult == GameResult.None)
                                SetTurn(TurnType.PlayerB);
                            else
                                EndGame(gameResult);
                        }
                    };
                    break;
                case TurnType.PlayerB:
                    _gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnB);
                    
                    // TODO: 계산된 row, col 값
    
                    var result = AIController.FindNextMove(_board);
                    
                    if (SetNewBoardValue(PlayerType.PlayerB, result.row, result.col))
                    {
                        var gameResult = CheckGameResult();
                        if (gameResult == GameResult.None)
                            SetTurn(TurnType.PlayerA);
                        else
                            EndGame(gameResult);
                    }
                    
                    break;
            }
        }
    
        /// <summary>
        /// 게임 결과 확인 함수
        /// </summary>
        /// <returns>플레이어 기준 게임 결과</returns>
        private GameResult CheckGameResult()
        {
            if (CheckGameWin(PlayerType.PlayerA)) return GameResult.Win;
            if (CheckGameWin(PlayerType.PlayerB)) return GameResult.Lose;
            if (IsAllBlocksPlaced()) return GameResult.Draw;
            
            return GameResult.None;
        }
        
        // 모든 마커가 보드에 배치 되었는지 확인하는 함수
        private bool IsAllBlocksPlaced()
        {
            for (var row = 0; row < _board.GetLength(0); row++)
            {
                for (var col = 0; col < _board.GetLength(1); col++)
                {
                    if (_board[row, col] == PlayerType.None)
                        return false;
                }
            }
            return true;
        }
        
        // 게임의 승패를 판단하는 함수
        private bool CheckGameWin(PlayerType playerType)
        {
            // 가로로 마커가 일치하는지 확인 
            for (var row = 0; row < _board.GetLength(0); row++)
            {
                if (_board[row, 0] == playerType && _board[row, 1] == playerType && _board[row, 2] == playerType)
                {
                    (int, int)[] blocks = { (row, 0), (row, 1), (row, 2) };
                    _blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 세로로 마커가 일치하는지 확인
            for (var col = 0; col < _board.GetLength(1); col++)
            {
                if (_board[0, col] == playerType && _board[1, col] == playerType && _board[2, col] == playerType)
                {
                    (int, int)[] blocks = { (0, col), (1, col), (2, col) };
                    _blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 대각선으로 마커가 일치하는지 확인
            if (_board[0, 0] == playerType && _board[1, 1] == playerType && _board[2, 2] == playerType)
            {
                (int, int)[] blocks = { (0, 0), (1, 1), (2, 2) };
                _blockController.SetBlockColor(playerType, blocks);
                return true;
            }
    
            if (_board[0, 2] == playerType && _board[1, 1] == playerType && _board[2, 0] == playerType)
            {
                (int, int)[] blocks = { (0, 2), (1, 1), (2, 0) };
                _blockController.SetBlockColor(playerType, blocks);
                return true;
            }
            
            return false;
        }
    
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            if (scene.name == "Game")
            {
                _blockController = GameObject.FindObjectOfType<BlockController>();
                _gameUIController = GameObject.FindObjectOfType<GameUIController>();
    
                // 게임 시작 --> GameScene에 들어왔을 때만 실행
                StartGame();
            }
    
            // Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
            _canvas = GameObject.FindObjectOfType<Canvas>();
        }
    }

     

    >> PanelManager.cs 삭제

     

    >> PanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using DG.Tweening; // DOTween 사용
    
    [RequireComponent(typeof(CanvasGroup))]
    public class PanelController : MonoBehaviour
    {
        [SerializeField] private RectTransform panelRectTransform;      // 팝업창
    
        private CanvasGroup _backgroundCanvasGroup;                     // 뒤의 배경
    
        public delegate void PanelControllerHideDelegate();
        
        private void Awake()
        {
            _backgroundCanvasGroup = GetComponent<CanvasGroup>();
        }
    
        /// <summary>
        /// Panel 표시 함수
        /// </summary>
        public void Show()
        {
            _backgroundCanvasGroup.alpha = 0;
            panelRectTransform.localScale = Vector3.zero;
    
            _backgroundCanvasGroup.DOFade(1, 0.2f).SetEase(Ease.Linear);
            panelRectTransform.DOScale(1, 0.2f).SetEase(Ease.OutBack);
        }
        
        /// <summary>
        /// Panel 숨기기 함수
        /// </summary>
        public void Hide(PanelControllerHideDelegate hideDelegate = null) // default 값을 null로 설정
        {
            _backgroundCanvasGroup.alpha = 1;
            panelRectTransform.localScale = Vector3.one;
    
            _backgroundCanvasGroup.DOFade(0, 0.2f).SetEase(Ease.Linear);
            panelRectTransform.DOScale(0, 0.2f)
                .SetEase(Ease.InBack).OnComplete(() =>
                {
                    hideDelegate?.Invoke();
                    Destroy(gameObject);
                });
        }
    }

     

    >> MainPanelController.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class MainPanelController : MonoBehaviour
    {
        public void OnClickSinglePlayButton()
        {
            GameManager.Instance.ChangeToGameScene(GameManager.GameType.SinglePlayer);
        }
        
        public void OnClickDualPlayButton()
        {
            GameManager.Instance.ChangeToGameScene(GameManager.GameType.DualPlayer);
        }
        
        public void OnClickSettingsButton()
        {
            GameManager.Instance.OpenSettingsPanel();
        }
    }

     

    >> ConfirmPanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    
    public class ConfirmPanelController : PanelController
    {
        [SerializeField] private TMP_Text messageText;
    
        public delegate void OnConfirmButtonClick();
        private OnConfirmButtonClick _onConfirmButtonClick;
    
        // Confirm Button에 띄울 message도 받아서 사용하기 위해 함수 오버로딩을 활용
        public void Show(string message, OnConfirmButtonClick onConfirmButtonClick)
        {
            messageText.text = message;
            _onConfirmButtonClick = onConfirmButtonClick;
            base.Show(); // Show Animation 재생
        }
    
        /// <summary>
        /// Confirm 버튼 클릭 시 호출되는 함수
        /// </summary>
        public void OnClickConfirmButton()
        {
            Hide(() => _onConfirmButtonClick?.Invoke());
        }
        
        /// <summary>
        /// X 버튼 클릭 시 호출되는 함수
        /// </summary>
        public void OnClickCloseButton()
        {
            Hide();
        }
    }

     

    >> PopupPanelController.cs

    using System.Collections;
    using System.Collections.Generic;
    using System;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    using DG.Tweening;
    using UnityEngine.SceneManagement; // DOTween을 사용하기 위해 선언
    
    [RequireComponent(typeof(CanvasGroup))]
    public class PopupPanelController : Singleton<PopupPanelController>
    {
        [SerializeField] private TMP_Text contentText;
        [SerializeField] private Button confirmButton;
        [SerializeField] private TMP_Text confirmButtonText;
    
        [SerializeField] private RectTransform panelRectTransform;
    
        private CanvasGroup _canvasGroup;
    
        private void Start()
        {
            _canvasGroup = GetComponent<CanvasGroup>();
            Hide(true);
        }
    
        public void Show(string content, string confirmButtonText, bool animation, Action confirmAction)
        {
            gameObject.SetActive(true);
    
            // animation을 위한 초기화
            _canvasGroup.alpha = 0;
            panelRectTransform.localScale = Vector3.zero;
    
            if (animation)
            {
                // DOTween 사용
                panelRectTransform.DOScale(1f, 0.2f);
                _canvasGroup.DOFade(1f, 0.2f).SetEase(Ease.OutBack);
                // SetEase() : animation을 빠르게 시작했다가 점차 느려지도록, / OutBack : 효과
            }
            else
            {
                panelRectTransform.localScale = Vector3.one;
                _canvasGroup.alpha = 1f;
            }
            
            contentText.text = content;
            this.confirmButtonText.text = confirmButtonText;
            confirmButton.onClick.AddListener(() =>
            {
                confirmAction();
                Hide(true);
            });
        }
    
        public void Hide(bool animation)
        {
            if (animation)
            {
                // OnComplete() : DOScale() 함수가 끝나고 난 뒤 실행
                panelRectTransform.DOScale(0f, 0.2f).OnComplete(() =>
                {
                    contentText.text = "";
                    confirmButtonText.text = "";
                    confirmButton.onClick.RemoveAllListeners();
    
                    gameObject.SetActive(false);
                });
                _canvasGroup.DOFade(0f, 0.2f).SetEase(Ease.InBack);
            }
            else
            {
                contentText.text = "";
                confirmButtonText.text = "";
                confirmButton.onClick.RemoveAllListeners();
    
                gameObject.SetActive(false);
            }
        }
    
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
    }

     

    >> GameUIController.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class GameUIController : MonoBehaviour
    {
        [SerializeField] private CanvasGroup canvasGroupA;
        [SerializeField] private CanvasGroup canvasGroupB;
        [SerializeField] private Button gameOverButton;
        
        public enum GameUIMode { Init, TurnA, TurnB, GameOver }
    
        private const float DisableAlpha = 0.5f;
        private const float EnableAlpha = 1f;
    
        public void SetGameUIMode(GameUIMode gameUIMode)
        {
            switch (gameUIMode)
            {
                case GameUIMode.Init:
                    canvasGroupA.gameObject.SetActive(true);
                    canvasGroupB.gameObject.SetActive(true);
                    gameOverButton.gameObject.SetActive(false);
    
                    canvasGroupA.alpha = DisableAlpha;
                    canvasGroupB.alpha = DisableAlpha;
                    break;
                case GameUIMode.TurnA:
                    canvasGroupA.gameObject.SetActive(true);
                    canvasGroupB.gameObject.SetActive(true);
                    gameOverButton.gameObject.SetActive(false);
    
                    canvasGroupA.alpha = EnableAlpha;
                    canvasGroupB.alpha = DisableAlpha;
                    break;
                case GameUIMode.TurnB:
                    canvasGroupA.gameObject.SetActive(true);
                    canvasGroupB.gameObject.SetActive(true);
                    gameOverButton.gameObject.SetActive(false);
    
                    canvasGroupA.alpha = DisableAlpha;
                    canvasGroupB.alpha = EnableAlpha;
                    break;
                case GameUIMode.GameOver:
                    canvasGroupA.gameObject.SetActive(false);
                    canvasGroupB.gameObject.SetActive(false);
                    gameOverButton.gameObject.SetActive(true);
                    break;
            }
        }
    
        public void OnClickGameOverButton()
        {
            GameManager.Instance.OpenConfirmPanel("게임을 종료하시겠습니까?", () =>
            {
                GameManager.Instance.ChangeToMainScene();
            });
        }
    }

     

    스스로 AI 만들어보기

    나의 첫 구상

    1. 중앙이 비었으면 무조건 중앙을 체크
    2. Block을 체크해서 Player가 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있으면 체크
    3. Block을 체크해서 AI가 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있으면 체크
    4. 앞선 세 경우가 아니라면 남은 칸 중에 체크

    >> 코드로 구현

    public static class AIController
    {
        private static GameManager.PlayerType[,] _board;
        
        public static (int row, int col) FindNextMove(GameManager.PlayerType[,] board)
        {
            // TODO: board의 내용을 보고 다음 수를 계산 후 반환
            // 1. 중앙이 비었으면 무조건 중앙을 체크
            if (board[1, 1] == GameManager.PlayerType.None) { return (1, 1); }
            
            // 2. player가 두 칸이 이어져있고 남은 한 칸이 비어있으면 체크
            // 가로 체크
            for (var row = 0; row < board.GetLength(0); row++)
            {
                if (board[row, 0] == GameManager.PlayerType.PlayerA &&
                    board[row, 1] == GameManager.PlayerType.PlayerA &&
                    board[row, 2] == GameManager.PlayerType.None)
                {
                    return (row, 2);
                }
                else if (board[row, 0] == GameManager.PlayerType.None &&
                         board[row, 1] == GameManager.PlayerType.PlayerA &&
                         board[row, 2] == GameManager.PlayerType.PlayerA)
                {
                    return (row, 0);
                }
                else if (board[row, 0] == GameManager.PlayerType.PlayerA &&
                         board[row, 1] == GameManager.PlayerType.None &&
                         board[row, 2] == GameManager.PlayerType.PlayerA)
                {
                    return (row, 1);
                }
            }
            
            // 세로 체크
            for (var col = 0; col < board.GetLength(1); col++)
            {
                if (board[0, col] == GameManager.PlayerType.PlayerA &&
                    board[1, col] == GameManager.PlayerType.PlayerA &&
                    board[2, col] == GameManager.PlayerType.None)
                {
                    return (2, col);
                }
                else if (board[0, col] == GameManager.PlayerType.None &&
                         board[1, col] == GameManager.PlayerType.PlayerA &&
                         board[2, col] == GameManager.PlayerType.PlayerA)
                {
                    return (0, col);
                }
                else if (board[0, col] == GameManager.PlayerType.PlayerA &&
                         board[1, col] == GameManager.PlayerType.None &&
                         board[2, col] == GameManager.PlayerType.PlayerA)
                {
                    return (1, col);
                }
            }
            
            // 대각선 체크
            if (board[0, 0] == GameManager.PlayerType.PlayerA &&
                board[1, 1] == GameManager.PlayerType.PlayerA &&
                board[2, 2] == GameManager.PlayerType.None)
            {
                return (2, 2);
            }
            else if (board[0, 0] == GameManager.PlayerType.None &&
                     board[1, 1] == GameManager.PlayerType.PlayerA &&
                     board[2, 2] == GameManager.PlayerType.PlayerA)
            {
                return (0, 0);
            }
            
            if (board[0, 2] == GameManager.PlayerType.PlayerA &&
                board[1, 1] == GameManager.PlayerType.PlayerA &&
                board[2, 0] == GameManager.PlayerType.None)
            {
                return (2, 0);
            }
            else if (board[0, 2] == GameManager.PlayerType.None &&
                     board[1, 1] == GameManager.PlayerType.PlayerA &&
                     board[2, 0] == GameManager.PlayerType.PlayerA)
            {
                return (0, 2);
            }
            
            // 3. AI가 두 칸이 이어져있고 한 칸이 비어있으면 체크해서 승리
            // 가로 체크
            for (var row = 0; row < board.GetLength(0); row++)
            {
                if (board[row, 0] == GameManager.PlayerType.PlayerB &&
                    board[row, 1] == GameManager.PlayerType.PlayerB &&
                    board[row, 2] == GameManager.PlayerType.None)
                {
                    return (row, 2);
                }
                else if (board[row, 0] == GameManager.PlayerType.None &&
                         board[row, 1] == GameManager.PlayerType.PlayerB &&
                         board[row, 2] == GameManager.PlayerType.PlayerB)
                {
                    return (row, 0);
                }
                else if (board[row, 0] == GameManager.PlayerType.PlayerB &&
                         board[row, 1] == GameManager.PlayerType.None &&
                         board[row, 2] == GameManager.PlayerType.PlayerB)
                {
                    return (row, 1);
                }
            }
            
            // 세로 체크
            for (var col = 0; col < board.GetLength(1); col++)
            {
                if (board[0, col] == GameManager.PlayerType.PlayerB &&
                    board[1, col] == GameManager.PlayerType.PlayerB &&
                    board[2, col] == GameManager.PlayerType.None)
                {
                    return (2, col);
                }
                else if (board[0, col] == GameManager.PlayerType.None &&
                         board[1, col] == GameManager.PlayerType.PlayerB &&
                         board[2, col] == GameManager.PlayerType.PlayerB)
                {
                    return (0, col);
                }
                else if (board[0, col] == GameManager.PlayerType.PlayerB &&
                         board[1, col] == GameManager.PlayerType.None &&
                         board[2, col] == GameManager.PlayerType.PlayerB)
                {
                    return (1, col);
                }
            }
            
            // 대각선 체크
            if (board[0, 0] == GameManager.PlayerType.PlayerB &&
                board[1, 1] == GameManager.PlayerType.PlayerB &&
                board[2, 2] == GameManager.PlayerType.None)
            {
                return (2, 2);
            }
            else if (board[0, 0] == GameManager.PlayerType.None &&
                     board[1, 1] == GameManager.PlayerType.PlayerB &&
                     board[2, 2] == GameManager.PlayerType.PlayerB)
            {
                return (0, 0);
            }
            
            if (board[0, 2] == GameManager.PlayerType.PlayerB &&
                board[1, 1] == GameManager.PlayerType.PlayerB &&
                board[2, 0] == GameManager.PlayerType.None)
            {
                return (2, 0);
            }
            else if (board[0, 2] == GameManager.PlayerType.None &&
                     board[1, 1] == GameManager.PlayerType.PlayerB &&
                     board[2, 0] == GameManager.PlayerType.PlayerB)
            {
                return (0, 2);
            }
            
            // 4. 앞선 세 경우가 아니라면 남은 칸 중에 랜덤으로 체크 --> 일단 지금은 랜덤 없이 테스트
            for (var row = 0; row < board.GetLength(1); row++)
            {
                for (var col = 0; col < board.GetLength(0); col++)
                {
                    if (board[row, col] == GameManager.PlayerType.None)
                    {
                        return (row, col);
                    }
                }
            }
            
            return (0, 0);
        }
    
        private static bool CheckTwoMarker(GameManager.PlayerType[,] board, GameManager.PlayerType playerType)
        {
            _board = board;
            
            // 가로 체크
            for (var row = 0; row < board.GetLength(0); row++)
            {
                if (board[row, 0] == playerType && board[row, 1] == playerType)
                {
                    
                }
            }
    
            return false;
        }
    }

     

    보완

    : Block을 체크해서 Player든 AI든 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있으면 체크

    --> None이 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있는 상황이 생기면 체크하는 문제가 발생하여 추가 보완

     

    >> 최종 코드 AIController.cs

    public static class AIController
    {
        private static GameManager.PlayerType[,] _board;
        
        public static (int row, int col) FindNextMove(GameManager.PlayerType[,] board)
        {
            // TODO: board의 내용을 보고 다음 수를 계산 후 반환
            // 1. 중앙이 비었으면 무조건 중앙을 체크
            if (board[1, 1] == GameManager.PlayerType.None) { return (1, 1); }
            
            // 2. 블록이 두 칸이 이어져있고 남은 한 칸이 비어있으면 체크 --> Player와 AI는 상관 없지만 None은 피해야 함
            // 가로 체크 (None을 피하는 더 아름다운 방법이 없을까..?)
            for (var row = 0; row < board.GetLength(0); row++)
            {
                if (board[row, 0] == board[row, 1] && board[row, 2] == GameManager.PlayerType.None
                                                   && board[row, 0] != GameManager.PlayerType.None)
                {
                    return (row, 2);
                }
                else if (board[row, 1] == board[row, 2] && board[row, 0] == GameManager.PlayerType.None 
                                                        && board[row, 1] != GameManager.PlayerType.None)
                {
                    return (row, 0);
                }
                else if (board[row, 0] == board[row, 2] && board[row, 1] == GameManager.PlayerType.None
                                                        && board[row, 0] != GameManager.PlayerType.None)
                {
                    return (row, 1);
                }
            }
            
            // 세로 체크
            for (var col = 0; col < board.GetLength(1); col++)
            {
                if (board[0, col] == board[1, col] && board[2, col] == GameManager.PlayerType.None 
                                                   && board[0, col] != GameManager.PlayerType.None)
                {
                    return (2, col);
                }
                else if (board[1, col] == board[2, col] && board[0, col] == GameManager.PlayerType.None
                                                        && board[1, col] != GameManager.PlayerType.None)
                {
                    return (0, col);
                }
                else if (board[0, col] == board[2, col] && board[1, col] == GameManager.PlayerType.None
                                                        && board[0, col] != GameManager.PlayerType.None)
                {
                    return (1, col);
                }
            }
            
            // 대각선 체크
            if (board[0, 0] == board[1, 1] && board[2, 2] == GameManager.PlayerType.None)
            {
                return (2, 2);
            }
            else if (board[1, 1] == board[2, 2] && board[0, 0] == GameManager.PlayerType.None)
            {
                return (0, 0);
            }
            
            if (board[0, 2] == board[1, 1] && board[2, 0] == GameManager.PlayerType.None)
            {
                return (2, 0);
            }
            else if (board[1, 1] == board[2, 0] && board[0, 2] == GameManager.PlayerType.None)
            {
                return (0, 2);
            }
            
            // 3. 앞선 두 경우가 아니라면 남은 칸 중에 랜덤으로 체크 --> 일단 지금은 랜덤 없이 테스트
            for (var row = 0; row < board.GetLength(1); row++)
            {
                for (var col = 0; col < board.GetLength(0); col++)
                {
                    if (board[row, col] == GameManager.PlayerType.None)
                    {
                        return (row, col);
                    }
                }
            }
            
            return (0, 0);
        }
    }

     

    └ 추가 보완점

    1. 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있는 상황에서 None이 아님을 체크하는 아름다운(?) 방법
    2. 남은 칸을 체크할 때 랜덤으로 체크하도록

    ※ 사진과 같은 상황에서 이기려면 (2, 2)를 체크해야 함에도 AI는 (0, 0)을 체크하는 상황 발생

    --> 어떤 알고리즘으로 막을 것인가?

    : 나중에 남은 칸을 랜덤으로 체크하도록 구현한다고 하더라도 이 상황에서는 무조건 (2, 2)를 막아야 함


    C# 단기 교육 보강

    4일차

    Unity C# 프로그래밍 기초 문법

    └ 가상화 (Virtual) vs 추상화 (Abstract)

    • Virtual : 함수를 선택적으로 재정의 할 수 있는 방법
    • Abstract : 함수를 필수적으로 재정의 해야 하는 방법

    >> Virtual

    : 'Base.' 을 사용하여 부모의 함수를 사용 가능하다.

     

    >> Abstract

    : 무조건 있어야하기 때문에 실수를 줄일 수 있다. --> class도 abstract class여야 한다.

    ※ 'Base.'를 사용할 수 없다.

     

    Project 시작

    : Project를 Universal 3D로 다시 만들고 GitHub도 재연결

    ※ Package Export & Import

     

    ※ Unity Asset Store에서 무료 에셋 추가

    https://assetstore.unity.com/packages/3d/characters/creatures/lowpoly-fantasy-monsters-pack-ver1-0-demo-98393#description

     

    LowPoly Fantasy Monsters Pack Ver1.0_DEMO | 3D 생물 | Unity Asset Store

    Elevate your workflow with the LowPoly Fantasy Monsters Pack Ver1.0_DEMO asset from TS WORK. Find this & other 생물 options on the Unity Asset Store.

    assetstore.unity.com

    https://assetstore.unity.com/packages/3d/props/weapons/sci-fi-turrets-cannon-69615

     

    Sci-fi Turrets (cannon) | 3D 무기 | Unity Asset Store

    Elevate your workflow with the Sci-fi Turrets (cannon) asset from Vertex Studio. Find this & other 무기 options on the Unity Asset Store.

    assetstore.unity.com

    https://assetstore.unity.com/packages/3d/props/weapons/weapons-pack-bullets-302702

     

    Weapons Pack - Bullets | 3D 무기 | Unity Asset Store

    Elevate your workflow with the Weapons Pack - Bullets asset from PolyOne Studio. Find this & other 무기 options on the Unity Asset Store.

    assetstore.unity.com

    https://assetstore.unity.com/packages/3d/props/guns/stylized-m4-assault-rifle-with-scope-complete-kit-with-gunshot-v-178197?srsltid=AfmBOoqP9uQ4iyADhN5eG-w-afuCLZus1nVs3cmrEqdBfjAfNuhqR85g

     

    Stylized M4 Assault Rifle with Scope Complete Kit with Gunshot VFX and Sound - Hand Painted AR Machine Gun Automatic Rifle | 3D

    Elevate your workflow with the Stylized M4 Assault Rifle with Scope Complete Kit with Gunshot VFX and Sound - Hand Painted AR Machine Gun Automatic Rifle asset from BigRookGames. Find this & other 총기 options on the Unity Asset Store.

    assetstore.unity.com

     

    └ Unity

    >> Scene 생성 --> 이름 : Monster Wave

     

    >> Plane 생성 --> Scale (5, 5 ,5)

     

    >> 받은 Asset Prefab 추가

    : LowPoly Fantasy Monsters Pack & Sci-fi turrets

     

    >> Import하면서 깨진 텍스쳐 해결하기

     

    >> Monster 크기 조정 및 Prefab에 적용

    --> 이후 적용된 Prefab들을 복사하여 '03. Prefabs' 폴더로 붙여넣기

    --> Hierarchy에 있는 Monster들은 삭제

     

    >> SpawnManager 추가

    : 빈 게임 오브젝트로 SpawnManager 만들어서 SpawnManager.cs 추가하고 Prefab들 바인딩

     

    >> Monster Prefab에 각각 Script 추가

     

    >> SpawnManager 위치 변경

     

    ※ Play 중에 Game View의 시점을 Scene View와 똑같이 만드는 법

    Scene의 원하는 위치에서 'Main Camera'를 선택 후, Ctrl + Shift + F

     

    └ Script

    SpawnManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Random = UnityEngine.Random;
    
    public class SpawnManager : MonoBehaviour
    {
        // 생성하려는 프리팹
        public GameObject[] spawnPrefabs;
        
        // 생성하려는 주기
        public float maxTime = 3f;
        public float timer = 0f;
        
        // 생성 기능
        private void Update()
        {
            // 타이머
            timer += Time.deltaTime;
            
            if (timer >= maxTime)
            {
                timer = 0f;
                
                // 랜덤하게 몬스터 생성
                int randomIndex = Random.Range(0, 3);
                GameObject monsterObj = Instantiate(spawnPrefabs[randomIndex]);
                monsterObj.transform.SetParent(this.transform); // 계층구조 변경
                
                // 생성한 몬스터의 위치를 SpawnManager의 위치로 적용
                monsterObj.transform.position = this.transform.position;
            }
        }
    }

     

    MonsterMovement.cs

    using UnityEngine;
    
    public class MonsterMovement : MonoBehaviour
    {
        public float speed = 1f;
    
        private void Update()
        {
            this.transform.Translate(Vector3.forward * (Time.deltaTime * speed));
        }
    }

     

    Monster.cs

    using System;
    using UnityEngine;
    
    public abstract class Monster : MonoBehaviour
    {
        protected float speed;
        protected float hp;
    
        protected abstract void Init();
    
        private void Start()
        {
            Init();
        }
    
        private void Update()
        {
            Move();
        }
        
        private void OnMouseDown()
        {
            Hit(1);
        }
    
        protected virtual void Hit(float damage)
        {
            hp -= damage;
    
            if (hp <= 0)
            {
                this.GetComponent<Animator>().SetBool("dead", true);
            }
        }
    
        protected void Move()
        {
            this.transform.Translate(Vector3.forward * (Time.deltaTime * speed));
        }
    }

     

    Goblin.cs

    using UnityEngine;
    
    public class Goblin : Monster
    {
        protected override void Init()
        {
            speed = 3f;
            hp = 1f;
        }
    }

     

    HobGoblin.cs

    using UnityEngine;
    
    public class HobGoblin : Monster
    {
        protected override void Init()
        {
            speed = 2f;
            hp = 2f;
        }
    }

     

    Troll.cs

    using UnityEngine;
    
    public class Troll : Monster
    {
        protected override void Init()
        {
            speed = 1f;
            hp = 3f;
        }
    }