목차
틱택토 게임 만들기
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. 폰트 다운로드
※ 눈누
눈누
상업용 무료 한글 폰트 사이트
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
└ 접근 제한자
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 49일차 (0) | 2025.02.06 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 48일차 (0) | 2025.02.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 46일차 (0) | 2025.02.03 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 45일차 (0) | 2025.01.24 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 44일차 (0) | 2025.01.23 |