목차
틱택토 게임 만들기
25.02.05
※ Project의 Default Font 설정
: Project Settings에서 font를 검색한 뒤, 폰트 수정
Canvas
>> Canvas에 있는 PanelManager.cs에 StartPanel 바인딩
>> GameManager에 Canvas 바인딩
StartPanel 만들기
1. 빈 게임 오브젝트로 Buttons를 만들어서 StartButton을 자식으로 넣기
: PosY를 -300으로, Width와 Height를 400으로 설정 --> 이후 제대로 된 버튼 이미지를 넣으면 다시 수정될 수 있음
※ Anchor는 그대로 middle-center
2. Title Text 추가
--> Font Size 140으로 늘림
3. Button
--> Image Type을 Sliced 타입으로 사용해도 되지만, 이번에는 Simple로 해봄 (디자인 형태에 따라 적용)
--> Set Native Size를 눌러서 사이즈 때문에 모서리가 깨진 부분을 수정
>> Set Native Size를 누르면 Size가 바뀌기 때문에 다시 조정
>> Button의 Text 수정
>> StartButton을 복사하여 Button 3개 생성 및 세팅
--> 이름 : SinglePlayButton, DualPlayButton, SettingsButton
--> Buttons에 Vertical Layout Group을 추가하여 세로로 정렬
--> 각 버튼들의 Text도 수정
>> 각 Button들의 OnClick() 수정
--> DualPlayButton과 SettingsButton에도 각각 맞게 할당
ConfirmPanel 만들기
※ panel-bg 삽입
1. Panel
--> Color의 Alpha값 255로 수정
2. ConfirmPanel
3. MessageText
--> Text를 "게임을 종료하시겠습니까?" 로 수정
4. OK Button
5. Close Button
--> Anchor는 Alt + Shift로 설정
--> Text 삭제
Settings Panel 만들기
1. ConfirmPanel을 복붙하고 세팅
--> CloseButton 빼고 전부 삭제
2. Toggle 생성
: SFXToggle, BGMToggle
--> 화면상에 보이도록 PosY만 내림
--> 지금은 임시로 만든거고, 나중에 다듬을 예정
GameUIPanel 만들기
: 다른 Panel에 가리지 않도록 Hierarchy에서 가장 위에 위치
>> PlayerAPanel 만들기
: Canvas Group 추가 --> 코드로 모든 자식 Object의 Alpha값을 수정 가능하도록
>> Player1Marker 만들기
: Image 생성
--> Anchor는 Shift + Alt
>> Player1Text 만들기
--> Anchor는 Shift + Alt
--> Alignment는 중앙 정렬
--> Color는 내가 임의로 넣은 것 (0, 166, 255, 255)
>> PlayerB는 PlayerA 복붙
: Player2Marker의 Transform 조정 --> 'X' Sprite의 크기가 142x142로 'O'와 달라서 조정
※ Player2Text의 Color는 (255, 0, 94, 255)
>> GameOverButton 만들기
※ Horizontal Layout Group의 꼼수 활용 : Active 돼있는 것들을 정렬해주기 때문에 코드로 SetActive를 활용하여 UI 배치
>> GameManager에 바인딩
Main Scene 만들기
: Main Scene을 만들어서 StartPanel만 따로 배치
1. Build Settings
: Main Scene 다음 Game Scene이 되도록 배치
2. Game Scene에 있는 StartPanel을 Main Scene으로 이동
: Prefab으로 만들어서 이동 후, 좌표 조정
3. 다시 Prefab을 해제
: Main Scene으로 옮겨오기 위해 Prefab화 한 것이기 때문 --> 만든 Prefab도 삭제
4. StartPanel을 MainPanel으로 이름 변경 후 설정
: StartPanelController.cs을 삭제하고 MainPanelController.cs를 추가
5. 버튼에 각각 함수 바인딩
--> DualPlayButton과 SettingsButton도 각각 맞게 바인딩
6. Game Scene 수정
: StartPanel이 없어짐에 따라 코드 수정 및 StartPanelController.cs 삭제
최종 코드
>> GameManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : Singleton<GameManager>
{
[SerializeField] private BlockController blockController;
[SerializeField] private PanelManager panelManager;
[SerializeField] private GameUIController gameUIController;
public enum PlayerType { None, PlayerA, PlayerB }
private PlayerType[,] _board;
public 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();
// Game UI 초기화
gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
// 게임 스타트
StartGame();
}
/// <summary>
/// 게임 시작
/// </summary>
public void StartGame()
{
//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);
// 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:
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);
}
else
{
// TODO: 이미 있는 곳을 터치 했을 때 처리
}
};
break;
case TurnType.PlayerB:
gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnB);
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)
{
(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;
}
}
>> PanelManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PanelManager : MonoBehaviour
{
[SerializeField] private PanelController confirmPanelController;
[SerializeField] private PanelController settingsPanelController;
//[SerializeField] private PanelController battlePanelController;
public enum PanelType { ConfirmPanel, SettingsPanel, BattlePanel }
private PanelController _currentPanelController;
/// <summary>
/// 표시할 패널 정보 전달하는 함수
/// </summary>
/// <param name="panelType">표시할 패널</param>
public void ShowPanel(PanelType panelType)
{
switch (panelType)
{
case PanelType.ConfirmPanel:
ShowPanelController(confirmPanelController);
break;
case PanelType.SettingsPanel:
ShowPanelController(settingsPanelController);
break;
//case PanelType.BattlePanel:
// ShowPanelController(battlePanelController);
// break;
}
}
/// <summary>
/// 패널을 표시하는 함수
/// 기존 패널이 있으면 Hide하고 새로운 패널을 Show 함
/// </summary>
/// <param name="panelController">표시할 패널</param>
private void ShowPanelController(PanelController panelController)
{
if (_currentPanelController != null)
_currentPanelController.Hide();
panelController.Show(() =>
{
_currentPanelController = null;
});
_currentPanelController = panelController;
}
}
>> 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; }
public delegate void OnHide();
private OnHide _onHideDelegate;
private RectTransform _rectTransform;
private Vector2 _hideAnchorPosition;
private void Awake() // GameManager에서 Start에 InitGame()을 해주기 때문에 그 전에 Awake에서 세팅
{
_rectTransform = GetComponent<RectTransform>();
_hideAnchorPosition = _rectTransform.anchoredPosition; // 숨겨둔 StartPanel의 Anchor 위치 저장
IsShow = false;
}
/// <summary>
/// Panel 표시 함수
/// </summary>
public void Show(OnHide onHideDelegate)
{
_onHideDelegate = onHideDelegate;
_rectTransform.anchoredPosition = Vector2.zero;
IsShow = true;
}
/// <summary>
/// Panel 숨기기 함수
/// </summary>
public void Hide()
{
_rectTransform.anchoredPosition = _hideAnchorPosition;
IsShow = false;
_onHideDelegate?.Invoke();
}
}
>> StartPanelController.cs
: 마지막에 삭제됨
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StartPanelController : PanelController
{
public void OnClickSinglePlayButton()
{
GameManager.Instance.StartGame();
Hide();
}
public void OnClickDualPlayButton()
{
}
public void OnClickSettingsButton()
{
}
}
>> 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;
public void Show(string message, OnConfirmButtonClick onConfirmButtonClick, OnHide onHide)
{
messageText.text = message;
this.onConfirmButtonClick = onConfirmButtonClick;
base.Show(onHide);
}
/// <summary>
/// Confirm 버튼 클릭 시 호출되는 함수
/// </summary>
public void OnClickConfirmButton()
{
onConfirmButtonClick?.Invoke();
Hide();
}
/// <summary>
/// X 버튼 클릭 시 호출되는 함수
/// </summary>
public void OnClickCloseButton()
{
Hide();
}
}
>> SettingsPanelController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SettingsPanelController : PanelController
{
/// <summary>
/// SFX On/Off시 호출되는 함수
/// </summary>
/// <param name="value">On/Off 값</param>
public void OnSFXToggleValueChanged(bool value)
{
}
/// <summary>
/// BGM On/Off시 호출되는 함수
/// </summary>
/// <param name="value">On/Off 값</param>
public void OnBGMToggleValueChanged(bool value)
{
}
/// <summary>
/// X 버튼 클릭 시 호출되는 함수
/// </summary>
public void OnClickCloseButton()
{
Hide();
}
}
>> 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);
}
public void SetBlockColor(GameManager.PlayerType playerType, (int row, int col)[] blockPositions)
{
if (playerType == GameManager.PlayerType.None) return;
foreach (var blockPosition in blockPositions)
{
var blockIndex = blockPosition.row * 3 + blockPosition.col;
Color32 markerColor;
if (playerType == GameManager.PlayerType.PlayerA)
markerColor = new Color32(0, 166, 255, 255);
else if (playerType == GameManager.PlayerType.PlayerB)
markerColor = new Color32(255, 0, 94, 255);
else
markerColor = Color.black;
blocks[blockIndex].SetColor(markerColor);
}
}
}
>> Blocks.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를 매개변수로 삼는 형태의 함수를 받는다.
private OnBlockClicked _onBlockClicked;
private int _blockIndex;
private SpriteRenderer _spriteRenderer;
private Color _defaultColor;
private void Awake()
{
_spriteRenderer = GetComponent<SpriteRenderer>();
_defaultColor = _spriteRenderer.color;
}
/// <summary>
/// 블럭의 색상을 변경하는 함수
/// </summary>
/// <param name="color">색상</param>
public void SetColor(Color color)
{
_spriteRenderer.color = color;
}
/// <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;
SetColor(_defaultColor);
}
/// <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);
}
}
>> 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()
{
// TODO: 게임오버 버튼을 눌렀을 때 구현
}
}
>> MainPanelController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MainPanelController : MonoBehaviour
{
public void OnClickSinglePlayButton()
{
SceneManager.LoadScene("Game");
}
public void OnClickDualPlayButton()
{
SceneManager.LoadScene("Game");
}
public void OnClickSettingsButton()
{
}
}
>> BattlePanelController.cs
: GameUIController.cs 처럼 게임 중에 상단에 누구 Turn인지 표시되도록 만들어보려다가 실패한 것
※ 아이디어
- 플레이어1 턴이면 '플레이어1' Text와 'O' Sprite의 색이 파란색으로 변하고 플레이어2는 기존의 색으로 돌아감
- 플레이어2 턴이면 '플레이어2' Text와 'X' Sprite의 색이 빨간색으로 변하고 플레이어1은 기존의 색으로 돌아감
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class BattlePanelController : PanelController
{
[SerializeField] private Image player1Marker;
[SerializeField] private Image player2Marker;
private Color _defaultColor;
public void SetTurnColor(GameManager.TurnType turnType)
{
_defaultColor = player1Marker.color;
if (turnType == GameManager.TurnType.PlayerA)
{
player1Marker.color = new Color32(0, 166, 255, 255);
player2Marker.color = _defaultColor;
}
else if (turnType == GameManager.TurnType.PlayerB)
{
player2Marker.color = new Color32(255, 0, 94, 255);
player1Marker.color = _defaultColor;
}
}
}
C# 단기 교육 보강
3일차
Unity C# 프로그래밍 기초 문법
>> 형 변환
※ boxing과 unboxing
- boxing : 기본형 타입 데이터를 참조형(ref) 타입 데이터로 변환하는 과정
- unboxing : 참조형(ref) 타입 데이터를 기본형 타입 데이터로 변환하는 과정
--> 내부적으로 엄청난 오버헤드가 발생하므로 지양해야 한다.
ex) 정수 변수 i를 boxing하고 개체 o에 할당
int i = 123;
// The following line boxes i.
object o = i;
ex) o 개체를 unboxing하고 정수 변수 i에 할당
o = 123;
i = (int)o; // unboxing
https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/types/boxing-and-unboxing
boxing 및 unboxing - C#
C# 프로그래밍의 boxing 및 unboxing에 대해 알아봅니다. 코드 예제를 살펴보고 사용 가능한 추가 리소스를 확인합니다.
learn.microsoft.com
└ Generic
└ Overload
└ 개체 지향 프로그래밍의 특징
- 추상화 (확장성)
- 상속 (다양성, 유연성)
- 다형성 (다양성)
- 캡슐화 (은닉성)
└ 상속 (Inheritance)
: 정의된 클래스를 물려받아, 확장 / 변형해서 새로운 클래스를 구현하는 방법
>> 1개만 상속 가능하다. (다중 상속 불가능)
※ Unity는 MonoBehavior를 상속 받아야하는데, 그럼 어떡하는가?
: 최상위 Object가 MonoBehavior를 상속받으면 자식 Object는 자연스레 이어 받는다.
>> Unity에서는 생성자를 거의 안 쓴다.
사실 생성자를 쓰려고 해도 에러난다. --> Awake()와 Start()를 활용
※ 생성자는 부모 Object가 먼저 호출되고 그 다음 자식 Object가 호출된다.
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 50일차 (0) | 2025.02.07 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 49일차 (0) | 2025.02.06 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 47일차 (1) | 2025.02.04 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 46일차 (0) | 2025.02.03 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 45일차 (0) | 2025.01.24 |