목차
Tic Tac Toe 서버 만들기
25.03.05
Unity로 프로그램 설치
>> SocketIOUnity
: 아래 링크에서 Installation을 참고하여 설치
https://github.com/itisnajim/SocketIOUnity
GitHub - itisnajim/SocketIOUnity: A Wrapper for socket.io-client-csharp to work with Unity.
A Wrapper for socket.io-client-csharp to work with Unity. - itisnajim/SocketIOUnity
github.com
>> 설치 과정
>> 설치 Git 링크
https://github.com/itisnajim/SocketIOUnity.git
>> ParrelSync
: 아래 링크에서 Installation을 참고하여 설치
https://github.com/VeriorPies/ParrelSync
GitHub - VeriorPies/ParrelSync: (Unity3D) Test multiplayer without building
(Unity3D) Test multiplayer without building. Contribute to VeriorPies/ParrelSync development by creating an account on GitHub.
github.com
>> 설치 과정
>> 설치 Git 링크
https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync
>> ParrelSync로 Clone 만들기
--> 추가 생성된 Clone은 종료
회원가입 만들기
>> 'Main Panel'의 자식으로 Signup Panel 생성
: UI-Panel로 생성
--> Color는 Main Panel의 Color를 가져옴
>> 'Signup Panel'의 자식으로 Title Text 생성
: UI-Text로 생성
--> Anchor는 Alt + Shift
>> 'Signup Panel'의 자식으로 'Username Input Field' 생성
: UI-Input Field로 생성
>> 빈 게임 오브젝트로 'InputFields'를 만들어서 Vertical Layout Group 추가
: 이후 Username InputField를 자식으로 넣기
--> Anchor는 Alt + Shift
>> 'Username Input Field'를 복사하여 여러 'Input Field' 생성
: PlaceHolder의 Text Input은 각자에 맞게 수정
- Nickname Input Field
- Password Input Field --> Content Type을 Password로 설정
- Confirm Password Input Field --> Content Type을 Password로 설정
--> InputFields의 모든 Text들의 Alignment를 Middle로 세팅
>> 'Signup Panel'의 자식으로 Confirm Button 생성
: Source Image인 Button-bg를 Slice한 후, 설정
--> Button의 Text도 설정
>> 'Signup Panel'의 자식으로 빈 게임 오브젝트로 'Buttons'를 만들어서 Vertical Layout Group 추가
: 이후 'Confirm Button'을 자식으로 넣기
>> 'Confirm Button'을 복사하여 'Cancel Button' 만들기
: Button의 Text Input을 "취소"로 변경
>> SignupPanelController.cs 생성
: 추가 및 바인딩
--> 각 Button에 맞게 OnClick() 바인딩
>> Signup Panel Prefab화
로그인 만들기
>> Hierarchy에 있는 'Signup Panel'을 다시 Prefab 해제한 다음 'Signin Panel'로 변경
>> Input Fields의 Height 수정 및 'Nickname Input Field', 'Confirm Password Input Field' 삭제
>> Button들 이름 및 Text 변경
>> 'Signin Panel'의 'SignupPanelController.cs' 삭제, 'SigninPanelController.cs', 'Constants.cs' 생성
: 추가 및 바인딩
>> 각 Button의 OnClick()에 함수 바인딩
>> Signin Panel Prefab화 후 Hierarchy 에서 삭제
구조 개선
>> Panel Open 구현
: GameManager.cs에 바인딩
>> NetworkManager.cs 생성
: Signin, Signup, 점수 추가 및 불러오기, 회원 정보 불러오기 등등 구현할 예정
점수 불러오기
: 별도로 ID, Password를 넘기지 않아도 불러올 수 있는 이유는 Cookie값을 보내기 때문
--> 코드로 구현할 때도 마찬가지로 Cookie값을 저장했다가 불러오는 방식으로 구현
>> 'Main Panel'의 'Buttons'의 자식으로 임시로 Score Button 생성
: 'SettingsButton'을 복붙하여 생성, Text만 "점수 보기"로 수정
--> Buttons의 Height 수정
>> 'Score Button'의 OnClick()에 함수 바인딩
: 'MainPanel'은 기존에 이미 바인딩 되어 있을 것, 함수만 바꿔주자
활동
: Tic Tac Toe 자동 로그인 만들기
- 점수 불러오기 기능과 로그인 기능을 이용해 자동 로그인 기능을 만들어 보세요.
--> 한번 로그인을 하면 자동으로 로그인이 되지만, 아직 안한 상태라면 로그인 창이 뜨도록
※ 자동 로그인과 점수 불러오기의 상관관계 (다른 수강생님 답변)
: 세션 아이디로 점수 불러오기가 성공하면 로그인 되어 있는거고, 해당 세션 아이디가 없거나 서버의 것과 일치하지 않으면 로그인이 안 되어있는 것으로 생각
최종 코드
>> 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;
[SerializeField] private GameObject signinPanel;
[SerializeField] private GameObject signupPanel;
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 }
private GameType _gameType;
private void Start()
{
// 로그인
OpenSigninPanel();
}
public void ChangeToGameScene(GameType gameType)
{
_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);
}
}
public void OpenSigninPanel()
{
if (_canvas != null)
{
var signinPanelObject = Instantiate(signinPanel, _canvas.transform);
}
}
public void OpenSignupPanel()
{
if (_canvas != null)
{
var signupPanelObject = Instantiate(signupPanel, _canvas.transform);
}
}
/// <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);
if (_gameType == GameType.SinglePlayer)
{
var result = MinimaxAIController.GetBestMove(_board);
if (result.HasValue)
{
if (SetNewBoardValue(PlayerType.PlayerB, result.Value.row, result.Value.col))
{
var gameResult = CheckGameResult();
if (gameResult == GameResult.None)
SetTurn(TurnType.PlayerA);
else
EndGame(gameResult);
}
}
else
{
EndGame(GameResult.Win);
}
}
else if (_gameType == GameType.DualPlayer)
{
_blockController.OnBlockClickedDelegate = (row, col) =>
{
if (SetNewBoardValue(PlayerType.PlayerB, row, 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 (MinimaxAIController.IsAllBlocksPlaced(_board)) return GameResult.Draw;
return GameResult.None;
}
// 게임의 승패를 판단하는 함수
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>();
}
}
>> SignupPanelController.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
public struct SignupData
{
public string username;
public string nickname;
public string password;
}
public class SignupPanelController : MonoBehaviour
{
[SerializeField] private TMP_InputField usernameInputField;
[SerializeField] private TMP_InputField nicknameInputField;
[SerializeField] private TMP_InputField passwordInputField;
[SerializeField] private TMP_InputField confirmPasswordInputField;
public void OnClickConfirmButton()
{
var username = usernameInputField.text;
var nickname = nicknameInputField.text;
var password = passwordInputField.text;
var confirmPassword = confirmPasswordInputField.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(nickname) ||
string.IsNullOrEmpty(password) || string.IsNullOrEmpty(confirmPassword))
{
// 입력값이 비어있음을 알리는 팝업창 표시
GameManager.Instance.OpenConfirmPanel("입력 내용이 누락되었습니다.", () =>
{
});
return;
}
if (password.Equals(confirmPassword))
{
SignupData signupData = new SignupData();
signupData.username = username;
signupData.nickname = nickname;
signupData.password = password;
// 서버로 SignupData 전달하면서 회원가입 진행
StartCoroutine(NetworkManager.Instance.Signup(signupData, () =>
{
Destroy(gameObject);
}, () =>
{
usernameInputField.text = "";
nicknameInputField.text = "";
passwordInputField.text = "";
confirmPasswordInputField.text = "";
}));
}
else
{
GameManager.Instance.OpenConfirmPanel("비밀번호가 서로 다릅니다.", () =>
{
passwordInputField.text = "";
confirmPasswordInputField.text = "";
});
}
}
public void OnClickCancelButton()
{
Destroy(gameObject);
}
}
>> SigninPanelController.cs
using TMPro;
using UnityEngine;
public struct SigninData
{
public string username;
public string password;
}
public struct SigninResult
{
public int result;
}
public struct ScoreResult
{
public string id;
public string username;
public string nickname;
public int score;
}
public class SigninPanelController : MonoBehaviour
{
[SerializeField] private TMP_InputField usernameInputField;
[SerializeField] private TMP_InputField passwordInputField;
public void OnClickSigninButton()
{
string username = usernameInputField.text;
string password = passwordInputField.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
// TODO: 누락된 값 입력 요청 팝업 표시
return;
}
var signinData = new SigninData();
signinData.username = username;
signinData.password = password;
StartCoroutine(NetworkManager.Instance.Signin(signinData, () =>
{
Destroy(gameObject);
}, result =>
{
if (result == 0)
{
usernameInputField.text = "";
}
else if (result == 1)
{
passwordInputField.text = "";
}
}));
}
public void OnClickSignupButton()
{
GameManager.Instance.OpenSignupPanel();
}
}
>> Constants.cs
: NetworkManager.cs에서만 사용하기 때문에 사실 이렇게 따로 선언할 필요는 없지만, 그럼에도 상수값들을 한 곳에 모아두면 용이하기 때문에 이렇게 처리함
public class Constants
{
public const string ServerURL = "http://localhost:3000";
}
>> NetworkManager.cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Networking;
public class NetworkManager : Singleton<NetworkManager>
{
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
public IEnumerator Signup(SignupData signupData, Action success, Action failure)
{
string jsonString = JsonUtility.ToJson(signupData);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
// () 안의 객체가 {}를 벗어나면 Disposing되는 문법
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
{
www.uploadHandler = new UploadHandlerRaw(bodyRaw);
www.downloadHandler = new DownloadHandlerBuffer();
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
Debug.Log("Error : " + www.error);
if (www.responseCode == 409) // 중복 사용자
{
// TODO: 중복 사용자 생성 팝업 표시
Debug.Log("중복 사용자");
GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다.", () =>
{
failure?.Invoke();
});
}
}
else
{
var result = www.downloadHandler.text;
Debug.Log("Result : " + result);
// 회원가입 성공 팝업 표시
GameManager.Instance.OpenConfirmPanel("회원가입이 완료 되었습니다.", () =>
{
success?.Invoke();
});
}
}
}
public IEnumerator Signin(SigninData signinData, Action success, Action<int> failure)
{
string jsonString = JsonUtility.ToJson(signinData);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/signin", UnityWebRequest.kHttpVerbPOST))
{
www.uploadHandler = new UploadHandlerRaw(bodyRaw);
www.downloadHandler = new DownloadHandlerBuffer();
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
}
else
{
// 점수 불러오기 기능을 구현하기 위해 cookie 값 저장 (수강생님 코드 참고)
var cookie = www.GetResponseHeader("set-cookie");
if (!string.IsNullOrEmpty(cookie))
{
//int lastIndex = cookie.LastIndexOf(";");
//string sid = cookie.Substring(0, lastIndex);
//PlayerPrefs.SetString("sid", sid);
int lastIndex = cookie.IndexOf('='); // 첫 번째 '='의 위치 찾기
if (lastIndex != -1 && lastIndex + 1 < cookie.Length) // '='이 존재하고, 뒤에 값이 있을 경우
{
string sid = cookie.Substring(lastIndex + 1).Split(';')[0]; // '=' 다음부터 ';' 이전까지 추출
Debug.Log("Session ID : " + sid);
PlayerPrefs.SetString("sid", sid);
}
else
{
// 올바른 쿠키 형식이 아님
}
}
else
{
Debug.Log("쿠키가 비어있습니다.");
}
var resultString = www.downloadHandler.text;
var result = JsonUtility.FromJson<SigninResult>(resultString);
if (result.result == 0)
{
// username이 유효하지 않음
GameManager.Instance.OpenConfirmPanel("Username이 유효하지 않습니다.", () =>
{
failure?.Invoke(0);
});
}
else if (result.result == 1)
{
// password가 유효하지 않음
GameManager.Instance.OpenConfirmPanel("Password가 유효하지 않습니다.", () =>
{
failure?.Invoke(1);
});
}
else if (result.result == 2)
{
// 성공
GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () =>
{
success?.Invoke();
});
}
}
}
}
public IEnumerator GetScore(Action<ScoreResult> success, Action failure)
{
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/score", UnityWebRequest.kHttpVerbGET))
{
www.downloadHandler = new DownloadHandlerBuffer();
string sid = PlayerPrefs.GetString("sid", "");
if (!string.IsNullOrEmpty(sid))
{
www.SetRequestHeader("Cookie", sid);
}
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
if (www.responseCode == 403)
{
Debug.Log("로그인이 필요합니다.");
}
failure?.Invoke();
}
else
{
var result = www.downloadHandler.text;
var userScore = JsonUtility.FromJson<ScoreResult>(result);
Debug.Log(userScore.score);
success?.Invoke(userScore);
}
}
}
}
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 69일차 (0) | 2025.03.07 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 68일차 (0) | 2025.03.06 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 66일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 65일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차 (0) | 2025.02.27 |