본문 바로가기
Development/C#

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

by Mobics 2025. 2. 4.

 

목차


    틱택토 게임 만들기

    25.02.04

    화면 비율

    : 480x800의 세로형 화면의 경우 화면 비율이 0.6이다.

    이 때 화면의 width를 6 unit으로 설정하기 위해서는 6 / x = 0.6 이 되어야 한다. --> 6 = 0.6x, x = 6 / 0.6

    즉, x = 10이 되어야 한다.

    카메라의 Size는 x / 2한 값을 지정하면 된다.

     

    : 800x480의 가로형 화면의 경우 화면 비율이 1.667 이다.

    이 때 화면의 width를 10 unit으로 설정하기 위해서는 10 / x = 1.667 이 되어야 한다. --> 10 = 1.667x, x = 10 / 1.667

    x = 2.99 가 되어야 한다.

    카메라의 Size는 x / 2한 값을 지정하면 된다.

     

    >> 화면 비율에 따라 자동으로 카메라의 Size를 수정해주는 코드 작성

    : CameraController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class CameraController : MonoBehaviour
    {
        [SerializeField] private float widthUnit = 6f;
        private Camera _camera;
    
        private void Start()
        {
            _camera = GetComponent<Camera>();
            _camera.orthographicSize = widthUnit / _camera.aspect / 2;
        }
    }

    --> MainCamera에 할당

     

    Game Scene

    ※ UI로 Blocks 만들기

    : Grid Layout Group을 이용하여 편하게 Block 추가 (그냥 한번 알려주신 것)

    --> Blocks에 Grid Layout Group을 추가한 뒤, 설정해주고 Image를 복붙하면 알아서 배치된다.

     

    └ Unity 세팅

    >> 자잘한 설정

    - 만들었던 Block들을 정리

    - Main Camera의 배경색과 Block의 색을 조정

    - 빈 게임 오브젝트로 Marker를 만든 후, Sprite Renderer를 추가한 뒤, Order in Layer를 1로 수정

    --> (Sprite는 다시 삭제함)

     

    >> Block Prefab화 및 배치

    --> X 간격 1.7 / Y 간격 1.8 (Block을 9개 이상으로 계속 생성해야 한다면 Instantiate를 사용했을 것)

     

    >> Block Prefab

    >> Block.cs 추가 및 바인딩

     

    >> Box Collider 2D 추가

     

    >> BlockController 생성

    : 빈 게임 오브젝트에 BlockController.cs 추가 및 바인딩

    --> 순서에 따라 이름 변경 및 순서 조정 (Blocks 배열에 들어가는 순서가 중요하기 때문)

     

    >> GameManager 생성

    : 빈 게임 오브젝트에 GameManager.cs 할당 및 바인딩

     

    >> Canvas를 추가하고 Button 추가

    : Button의 Text도 수정

    --> 잠깐 테스트를 위해 OnClick()에 GameManager를 바인딩해서 StartGame() 할당

     

    >> StartPanel이 원할 때 등장하게끔 옆에 치워두고 StartPanelController.cs 추가

     

    >> StartButton의 OnClick() 변경

    --> Button의 Text도 폰트 바꾸고 '시작'으로 변경

     

    >> Canvas에 PanelManager.cs 추가

     

    └ 코드 작성

    ※ Rider에서 Script를 편하게 생성하는 법

    : Ctrl + Tap (Script 전환 탭) --> Ctrl + 1 (Explorer 열기) --> Ctrl + N (새 Script 생성) --> Unity Script 선택

    ※ summary

     

    Delegate vs Event

     

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class GameManager : Singleton<GameManager>
    {
        [SerializeField] private BlockController blockController;
        [SerializeField] private GameObject startPanel;     // TODO: 임시 변수, 나중에 삭제 예정
    
        private enum PlayerType { None, PlayerA, PlayerB }
        private PlayerType[,] _board;
        
        private enum TurnType { PlayerA, PlayerB }
        private enum GameResult { None, Win, Lose, Draw }
    
        private void Start()
        {
            // 게임 초기화
            InitGame();
            
            
        }
    
        /// <summary>
        /// 게임 초기화 함수 
        /// </summary>
        public void InitGame()
        {
            // _board 초기화
            _board = new PlayerType[3, 3];
            
            // Block 초기화
            blockController.InitBlocks();
        }
    
        /// <summary>
        /// 게임 시작
        /// </summary>
        public void StartGame()
        {
            startPanel.SetActive(false);    // TODO: 테스트 코드, 나중에 삭제 예정
            
            SetTurn(TurnType.PlayerA);
        }
        
        /// <summary>
        /// 게임 오버 시, 호출되는 함수
        /// gameResult에 따라 결과 출력
        /// </summary>
        /// <param name="gameResult">win, lose, draw</param>
        private void EndGame(GameResult gameResult)
        {
            // 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 (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:
                    Debug.Log("Player A turn");
                    blockController.OnBlockClickedDelegate = (row, col) =>
                    {
                        if (SetNewBoardValue(PlayerType.PlayerA, row, col))
                        {
                            var gameResult = CheckGameResult();
                            if (gameResult == GameResult.None)
                                SetTurn(TurnType.PlayerB);
                            else
                                EndGame(gameResult);
                        }
                        else
                        {
                            // TODO: 이미 있는 곳을 터치 했을 때 처리
                        }
                    };
                    break;
                case TurnType.PlayerB:
                    Debug.Log("Player B turn");
                    blockController.OnBlockClickedDelegate = (row, col) =>
                    {
                        if (SetNewBoardValue(PlayerType.PlayerB, row, col))
                        {
                            var gameResult = CheckGameResult();
                            if (gameResult == GameResult.None)
                                SetTurn(TurnType.PlayerA);
                            else
                                EndGame(gameResult);
                        }
                        else
                        {
                            // TODO: 이미 있는 곳을 터치 했을 때 처리
                        }
                    };
                    //TODO: AI에게 입력 받기
                    
                    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)
                    return true;
            }
            
            // 세로로 마커가 일치하는지 확인
            for (var col = 0; col < _board.GetLength(1); col++)
            {
                if (_board[0, col] == playerType && _board[1, col] == playerType && _board[2, col] == playerType)
                    return true;
            }
            
            // 대각선으로 마커가 일치하는지 확인
            if (_board[0, 0] == playerType && _board[1, 1] == playerType && _board[2, 2] == playerType)
                return true;
            if (_board[0, 2] == playerType && _board[1, 1] == playerType && _board[2, 0] == playerType)
                return true;
            
            return false;
        }
    }

     

    >> BlockController.cs

    using UnityEngine;
    
    public class BlockController : MonoBehaviour
    {
        [SerializeField] private Block[] blocks;
        
        public delegate void OnBlockClicked(int row, int col);
        public OnBlockClicked OnBlockClickedDelegate;
    
        public void InitBlocks()
        {
            for (int i = 0; i < blocks.Length; i++)
            {
                blocks[i].InitMarker(i, blockIndex =>
                {
                    var clickedRow = blockIndex / 3;
                    var clickedCol = blockIndex % 3;
                    
                    OnBlockClickedDelegate?.Invoke(clickedRow, clickedCol);
                });
            }
        }
    
        /// <summary>
        /// 특정 Block에 Marker를 표시하는 함수
        /// </summary>
        /// <param name="markerType">마커 타입</param>
        /// <param name="row">Row</param>
        /// <param name="col">Col</param>
        public void PlaceMarker(Block.MarkerType markerType, int row, int col)
        {
            // row, col을 index로 변환
            var markerIndex = row * 3 + col;
            
            // Block에게 마커 표시
            blocks[markerIndex].SetMarker(markerType);
        }
    }

     

    >> Block.cs

    using System;
    using UnityEngine;
    
    public class Block : MonoBehaviour
    {
        [SerializeField] private Sprite oSprite;
        [SerializeField] private Sprite xSprite;
        [SerializeField] private SpriteRenderer markerSpriteRenderer;
    
        public enum MarkerType { None, O, X }
    
        public delegate void OnBlockClicked(int index); // void를 반환하고 int를 매개변수로 삼는 형태의 함수를 받는다.
        public OnBlockClicked onBlockClicked;
        
        private int _blockIndex;
    
        /// <summary>
        /// Block 초기화 함수
        /// </summary>
        /// <param name="blockIndex">Block 인덱스</param>
        /// <param name="onBlockClicked">Block 터치 이벤트</param>
        public void InitMarker(int blockIndex, OnBlockClicked onBlockClicked)
        {
            _blockIndex = blockIndex;
            SetMarker(MarkerType.None);
            this.onBlockClicked = onBlockClicked;
        }
    
        /// <summary>
        /// 어떤 마커를 표시할지 전달하는 함수
        /// </summary>
        /// <param name="markerType">마커 타입</param>
        public void SetMarker(MarkerType markerType) // 외부에서 마크 표시
        {
            switch (markerType)
            {
                case MarkerType.O:
                    markerSpriteRenderer.sprite = oSprite;
                    break;
                case MarkerType.X:
                    markerSpriteRenderer.sprite = xSprite;
                    break;
                case MarkerType.None:
                    markerSpriteRenderer.sprite = null;
                    break;
            }
        }
    
        private void OnMouseUpAsButton()
        {
            onBlockClicked?.Invoke(_blockIndex);
        }
    }

     

    >> PanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(RectTransform))]
    public class PanelController : MonoBehaviour
    {
        public bool IsShow { get; private set; }
    
        private RectTransform _rectTransform;
        private Vector2 _hideAnchorPosition;
    
        private void Start()
        {
            _rectTransform = GetComponent<RectTransform>();
            _hideAnchorPosition = _rectTransform.anchoredPosition; // 숨겨둔 StartPanel의 Anchor 위치 저장
            IsShow = false;
        }
    
        /// <summary>
        /// Panel 표시 함수
        /// </summary>
        public void Show()
        {
            _rectTransform.anchoredPosition = Vector2.zero;
            IsShow = true;
        }
        
        /// <summary>
        /// Panel 숨기기 함수
        /// </summary>
        public void Hide()
        {
            _rectTransform.anchoredPosition = _hideAnchorPosition;
            IsShow = false;
        }
    }

     

    >> StartPanelController.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class StartPanelController : PanelController
    {
        public void OnClickStartButton()
        {
            GameManager.Instance.StartGame();
            Hide();
        }
    }

     

    >> PanelManager.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class PanelManager : MonoBehaviour
    {
        [SerializeField] private PanelController startPanelController;
        
        public enum PanelType { StartPanel, WinPanel, DrawPanel, LosePanel }
    
        public void ShowPanel(PanelType panelType)
        {
            switch (panelType)
            {
                case PanelType.StartPanel:
                    break;
                case PanelType.WinPanel:
                    break;
                case PanelType.DrawPanel:
                    break;
                case PanelType.LosePanel:
                    break;
            }
        }
    }

     

    TextMashPro 폰트 설치

    : 한글 사용을 위해

     

    1. 폰트 다운로드

    ※ 눈누

    https://noonnu.cc/

     

    눈누

    상업용 무료 한글 폰트 사이트

    noonnu.cc

     

    2. Unity Project에 다운 받은 폰트 추가

    : ttp 파일로 추가

     

    3. Font Asset Creator에서 Font 생성

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


    C# 단기 교육 보강

    2일차

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

    >> MonoBehaviour

     

    └ Class

     

    >> 개체 인스턴스 (Object Instance)

     

    └ Struct

     

    >> 값 타입 / 참조 타입

    : 메모리에 저장하는 방식의 차이

     

    └ Class vs Struct

    • Class : Reference Type
    • Struct : Value Type

     

    └ 함수와 변수

    >> 함수

     

    >> 변수

     

    >> 자료형

     

    >> 상수

     

    ※ Const vs Readonly

    • 컴파일 단계 : 파일을 만들고 뽑아낼 때
    • 런타임 단계 : 실행할 때

     

    └ Collection

     

    └ 접근 제한자