TIL

[멋쟁이사자처럼 부트캠프 TIL회고] 67일차 : 틱택토

Cadi 2025. 2. 4. 17:14

 

 

 

오늘 배운 것

1. 배운 테크닉? 들

2. 틱택토 코드들 

 

 

알아두면 좋을 사실들

 

1. OnMouseUpAsButton()

우리가 클릭 했을 때 , Down이나 Up만 사용하면 중간에 잘못 클릭했을 때 취소가 안된다. 

OnMouseUpAsButton()은 클릭한 콜라이더 내에서 다시 Up을 했을 때만 호출되는 함수이다. 

우리가 평상시에 사용하는 핸드폰의 클릭과 같다고 보면 된다.

 

 

 

2. 삼항 연산자의 사용

// _board[row, col] = playerType;
// blockController.PlaceMarker( playerType == PlayerType.PlayerA ? Block.MarkerType.O : Block.MarkerType.X, row, col);
// return true;

 

삼항 연산자로 다음턴 넘기는 테크닉

 

 

3. 화면 비율 

화면 비율에 관한 팁

화면 비율마다 카메라 사이즈를 다르게 설정해 주어야 한다. 그래야 모든 UI나 Sprtie가 화면 안에 들어가게 된다.

이때 예를 들어 위 아래 합쳐서 10칸을 보이게 하고 싶다 ! 하면 예시처럼

Ex) 800 X 480 의 가로형 화면의 경우 화면 비율이 1.667

위 아래 열칸일 보이게 하려면 10 / x = 1.667

즉 x = 2.99가 되어야 함. 

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

코드로 보자면 

 

[SerializeField] private float widthUnit = 6f;

private Camera _camera;



private void Start()
{
    _camera = GetComponent<Camera>();
  
    _camera.orthographicSize = widthUnit / _camera.aspect / 2;
}

이런 식으로 설정해주면 된다. 

 

틱택토 코드들

1. Block

가장 작은 부분인 Blcok부터 할 것이다.

우리는 델리게이트를 사용해서 커플링 문제가 일어나지 않게 할 것이다.

using UnityEngine;
using UnityEngine.Timeline;

namespace Game
{
    public class Block : MonoBehaviour
    {
        [SerializeField] private Sprite oSprite;
        [SerializeField] private Sprite xSprite;
        [SerializeField] private SpriteRenderer MarkerspriteRenderer;


            public delegate void OnBlockClicked(int index);

            // 여기 앞에다 이벤트 단 것과 달지 않은 것의 차이 ?
            public event OnBlockClicked onBlockClicked;

        private int _blockIndex;

        /// <summary>
        /// Block 초기화 함수
        /// </summary>
        /// <param name="blockIndex">Block 인덱스</param>
        /// <param name="onBlockClicked">Blcok 터치 이벤트</param>
        public void InitMarker(int blockIndex, OnBlockClicked onBlockClicked)
        {
            _blockIndex = blockIndex;
            SetMarker(MarkerType.None);
            this.onBlockClicked = onBlockClicked;
        }

        public enum MarkerType
        {
            None,
            O,
            X
        };


        /// <summary>
        /// d어떤 마커를 표시할지 전달하는 함수
        /// </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);
        }
    }
}

 

 

사용할 이미지들을 저장해준다.

 

우리는 BlcokController라는 함수에서 정보를 받아와 세팅할 것이기 때문에 Init 함수에서

매개 변수로 index와 델리게이트를 받아준다.

그리고 처음에는 마커를 null로, 인덱스를 받아온 인덱스로, 델리게이트도 받아온 델리게이트로 설정한다.

이렇게 되면 Block에서 onBloClicked를 실행할 때 BlockController에 있는 함수가 실행되게 된다. 

 

SetMarker는 간단하게 markerType을 받아서 이미지를 변경하는 함수이다.

 

OnMouseUpAsButton은 위에서 설명했다. 

 

2. BlockController

 

using UnityEngine;

namespace Game
{
    public class BlockController : MonoBehaviour
    {
        [SerializeField] private Block[] blocks;
        
        public delegate void OnBlockClickedDelegate(int row, int column);
        public  OnBlockClickedDelegate onBlockClickedDelegate;

        public void InitBlocks()
        {
            for (int i = 0; i < blocks.Length; i++)
            {
                blocks[i].InitMarker(i, blockIndex =>
                {
                    var clickedRow = blockIndex / 3;
                    var clickedColumn = blockIndex % 3;
                    
                    onBlockClickedDelegate?.Invoke(clickedRow, clickedColumn);
                } );
                
            }
                
        }

/// <summary>
/// 특정 Block에 마커 표시하는 함수
/// </summary>
/// <param name="markerType"></param>
/// <param name="row"></param>
/// <param name="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);
        }
    }
}

 

블럭들의 배열을 설정하고 ( 하나하나 넣어주었다, 나중에 코드로 할 수 있는 방법을 찾아볼 것)

 

마찬가지로 대리자를 설정해준다. 이는 GameManager에서 결국 컨트롤할 것이다.

 

InitBlocks

: 모든 블럭들의 초기화 함수, 9개의블럭들을 순회하며 인덱스를 부여하고, 그 인덱스를 바탕으로 동작하는 델리게이트 함수를 설정하는 함수이다. 

 

PlaceMarker

: 특정 블럭에 마커 표시하는 함수이다.

 

 

 

 

 

3. GameManager

[SerializeField] BlockController blockController;

[SerializeField] private GameObject startPanel; // 임시 변수, 삭제 에정

public GameObject TextPanel; //TODO: 임시 변수 삭제 예정
public TextMeshProUGUI textMesh;

private enum PlayerType
{
    None,
    PlayerA,
    PlayerB
}

public enum TurnType
{
    PlayerA,
    PlayerB
}

public enum GameResult
{
    None, // 진행중
    Win, // 승리
    Draw, //무승부
    Lose //패배
}


private PlayerType[,] _board;

변수들을 선언해주었다. 

PlayerType, TurnType, GameResult 등의 열거형이 있고,

보드의 정보를 위해 PlayerType[,] 도 선언해주었다.

 

public void StartGame()
{
    SetTurn(TurnType.PlayerA);
    startPanel.SetActive(false); //TODO: 테스트 코드, 삭제 예정
}
private void Start()
{
    InitGame();
    textMesh = TextPanel.GetComponentInChildren<TextMeshProUGUI>();
}

public void InitGame()
{
    // 보드 초기화
    _board = new PlayerType[3, 3];

    //블럭 초기화
    blockController.InitBlocks();
}

 

시작하면 InitGame()으로 보드 초기화와 블럭 초기화를 해 준다. 

 

/// <summary>
/// _board에 새로운 값을 할당하는 함수
/// </summary>
/// <param name="playerType">할당하고자 하는 플레이어 타입</param>
/// <param name="row">"row"</param>
/// <param name="col">"col"</param>
/// <returns>Ture 할당 완료, False면 이미 할당함</returns>
private bool SetNewBoardValue(PlayerType playerType, int row, int col)
{
    if (playerType == PlayerType.PlayerA)
    {
        if (_board[row, col] != PlayerType.PlayerB)
        {
            _board[row, col] = playerType;
            blockController.PlaceMarker(Block.MarkerType.O, row, col);
            return true;
        }
    } 
    if (playerType == PlayerType.PlayerB)
    {
        if (_board[row, col] != PlayerType.PlayerA)
        {
            _board[row, col] = playerType;
            blockController.PlaceMarker(Block.MarkerType.X, row, col);
            return true;
        }
    }

    return false;
    
}

 

보드에 새로운 값을 할당하는 함수이다. 

만일 비어 있다면 할당하고, 아니면 false를 반환한다. 

 

/// <summary>
/// 게임이 누구 한 쪽의 승리로 끝났는지 확인하는 함수
/// </summary>
/// <param name="playerType">플레이어 타입</param>
/// <returns>True면 끝남, False면 계속 </returns>
    private bool CheckGameWin(PlayerType playerType)
    {
        // 전체 row를 한 줄씩 체크하면서 가로로 마커가 일치하는지 확인
        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;
    }

 

빙고를 했는지 체크하는 함수이다.

True면 빙고, False면 계속한다.

 

/// <summary>
///  모든 마커가 보드에 배치 되었는지 확인하는 함수
/// </summary>
/// <returns>모두 배치</returns>
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;
}

 

무승부를 판별하는 함수이다. 

 

/// <summary>
/// CheckGameWin와 ISAllBlocksPlaced를 확인해 게임 결과를 반환
/// </summary>
/// <returns>게임 결과 반환 (win,los,draw,None) none이면 계속</returns>
    private GameResult GetGameResult()
    {
        if (CheckGameWin(PlayerType.PlayerA))
        {
            return GameResult.Win;
        }

        if (CheckGameWin(PlayerType.PlayerB))
        {
            return GameResult.Lose;
        }

        if (ISAllBlocksPlaced())
        {
            return GameResult.Draw;
        }

        return GameResult.None;
    }

 

위 두 함수를 사용해 게임이 끝났는지, 무승부인지, 계속 하는지를 판별하는 함수이다. 

 

public void SetTurn(TurnType turnType)
{
    switch (turnType)
    {
        case TurnType.PlayerA:
            Debug.Log(" Player A Turn");
            textMesh.text = "Player A turn";

            blockController.onBlockClickedDelegate = (row, col) =>
            {
                if (SetNewBoardValue(PlayerType.PlayerA, row, col))
                {
                    var gameResult = GetGameResult();
                    if (gameResult == GameResult.None)
                    {
                        SetTurn(TurnType.PlayerB);

                    }
                    else
                    {
                        EndGame(gameResult);
                    }
                   
                } 
            };
            break;


        case TurnType.PlayerB:
            //TODO: 플레이어에게 입력받기
            Debug.Log(" Player B Turn");
            textMesh.text = "Player B turn";
            blockController.onBlockClickedDelegate = (row, col) =>
            {
                if (SetNewBoardValue(PlayerType.PlayerB, row, col))
                {
                    var gameResult = GetGameResult();
                    if (gameResult == GameResult.None)
                    {
                        SetTurn(TurnType.PlayerA);
                       
                    }
                    else
                    {
                        EndGame(gameResult);
                    }
                }
            };

            break;
    }
}

 

턴을 하나씩 넘기는 함수이다. 

A의 턴이 온다면,  onClickDelegate를 보드 값을 플레이어 A로 할당하는 함수를 시도하고, 만일 성공한다면 게임결과를 검사해 다음 턴으로 넘길지, 혹은 게임을 종료할지 판별한다.

 

/// <summary>
/// 게임 오버시 호출되는 함수
/// gameResult에 따라 결과 출력
/// gameReuslt는
/// </summary>
/// <param name="gameResult"></param>
    public void EndGame(GameResult gameResult)
    {
        //TODO: 나중에 구현 !

        switch (gameResult)
        {
            case GameResult.Win:
                textMesh.text = "You win!";
                break;
            case GameResult.Draw:
                textMesh.text = "Draw!";
                break;
            case GameResult.Lose:
                textMesh.text = "You lose!";
                break;
        }
        blockController.onBlockClickedDelegate = null;
    }

게임이 끝났을 때 호출되는 함수이다.

더 이상 눌리지 않게 하기 위해 onBlockClickedDelegate를 null로 바꿔준다. 

 

 


마지막에 몰랐던 개념을 알았다. 

If 문 안에 함수가 들어가면 ,그 함수가 먼저 실행된 후 반환값에 따라 조건을 평가한다. 는 것이다. 

그래서

                if (SetNewBoardValue(PlayerType.PlayerA, row, col))

이 코드가 계속 실행은 되지만 true가 나올 때까지 턴이 넘어가지 않다가 넘어가게 된다. 

 

 

오늘의 목표

1. 코딩 테스트 스택, 큐 2문제

2. 유니티 에디터 관련 공부