목차
※ 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으로 돌아오면 연결이 끊어진 상태로 돌아오면서 문제가 생김 --> 게임을 처음 플레이하면 괜찮겠지만 반복 플레이하면 문제가 발생
>> 해결방법
- 'DontDestroyOnLoad'를 없애서 GameManager가 없어지도록 --> GameScene에서 선택한 것과 정보들을 MainScene으로 전달해야해서, 없앤다면 Scene의 Data를 어떻게 전달할지 고민해야 한다.
- 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() 바인딩
버그 해결
버그 종류
- 중복 체크 방지
- ConfirmPanel에서 Block을 클릭해도 체크되지 않도록
- Confirm을 눌렀을 때, ConfirmPanel의 Animation이 실행되고 Scene이 전환되도록
- 경우에 따라 첫 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 만들어보기
나의 첫 구상
- 중앙이 비었으면 무조건 중앙을 체크
- Block을 체크해서 Player가 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있으면 체크
- Block을 체크해서 AI가 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있으면 체크
- 앞선 세 경우가 아니라면 남은 칸 중에 체크
>> 코드로 구현
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);
}
}
└ 추가 보완점
- 한 줄 중에서 두 칸이 이어져있고 한 칸이 비어있는 상황에서 None이 아님을 체크하는 아름다운(?) 방법
- 남은 칸을 체크할 때 랜덤으로 체크하도록
※ 사진과 같은 상황에서 이기려면 (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에서 무료 에셋 추가
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
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;
}
}
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 51일차 (0) | 2025.02.10 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 50일차 (0) | 2025.02.07 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 48일차 (0) | 2025.02.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 47일차 (1) | 2025.02.04 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 46일차 (0) | 2025.02.03 |