목차
Tic Tac Toe 서버 만들기
25.03.06
지난 시간에 놓친 활동하기
: Tic Tac Toe 자동 로그인 만들기
- 점수 불러오기 기능과 로그인 기능을 이용해 자동 로그인 기능을 만들어 보세요.
--> 한번 로그인을 하면 자동으로 로그인이 되지만, 아직 안한 상태라면 로그인 창이 뜨도록
25.03.12
>> GameManager.cs 의 Start()에서 NetworkManager의 GetScore()를 호출하여 구현
: 자동 로그인에 실패했을 때 OpenSigninPanel()을 호출하여 다시 로그인 할 수 있도록 구현
--> 아래의 최종 코드도 수정해놓음
>> 'NavigationPanel'의 Button들의 Source Image 수정
: 기존의 Source Image가 깨져있어서 'Dark Theme UI' Asset에서 다운 받은 Sprite로 수정
--> 크기는 그대로
>> 'MainPanel'의 'Buttons'의 Height 수정
: 버튼이 4개인데 Height가 맞지 않아서 크기에 맞게 수정
--> 버튼의 Height 100 x 4개 + Spacing 20 x 3 = 460
활동
: TicTacToe 스코어 등록 및 리더보드 만들기
- 싱글 플레이에서 게임에 승리하면 유저에게 10점 부여하세요 --> 현재 싱글 플레이에는 Minimax 알고리즘이 적용되어 절대 승리할 수 없기 때문에 2인 플레이에서 승리하면 유저에게 10점 부여하는 식으로 확인 가능
- 전체 유저를 대상으로 랭킹을 보여주는 리더보드를 만드세요
- 리더보드는 Scroll View를 이용해 만드세요
- Scroll View에는 유저 닉네임 + 점수를 표시하세요
- 서버에서 유저 랭킹을 보여주는 기능을 만드세요
- 리더보드를 실행했을 때 자신의 점수를 바로 볼 수 있게 만들어주세요
※ Hint : 처음 로그인 했을 때 Cookie가 전달되고 Session 파일이 존재하면 Cookie가 전달되지 않을 것이기 때문에 로그인 했을 때 받은 Cookie 값을 저장해놓고 점수 저장과 불러오기를 구현하면 된다.
※ UnityWebRequest에서도 Cookie가 사용된다. 정확히는 게임 세션 동안 Cookie를 캐싱한다.
--> UnityWebRequest.ClearCookieCache(); 를 사용하면 캐싱된 쿠키를 제거할 수 있다.
└ 리더보드 만들기
>> 코드 작성
- NetworkManager.cs
- SigninPanelController.cs
- app.js
- leaderboard.js
25.03.13
>> LeaderboardPanel 만들기
1. Prefab에 있는 'ConfirmPanel'을 복붙하여 LeaderboardPanel로 이름 변경
2. 'MessageText'와 'OKButton'을 삭제
3. 'Panel'의 자식으로 Scroll View 추가
4. Scroll View의 Anchor 조정 및 Scroll Rect의 Horizontal 체크 해제
: CloseButton과 안 겹치도록 Top 조정
--> Anchor는 Alt + Shift
5. 'Content'의 자식으로 'ScoreCell' 생성
: UI-Panel로 만들어서 Source Image 넣고 Color도 Alpha 값을 255로 조정
--> Anchor, Left, Right, PosY, Height도 조정했지만 이후에 Content의 Vertical Layout Group에서 전부 조정해줄 예정
--> PosY를 0으로 조정
6. 'ScoreCell'의 자식으로 'NicknameText'와 'ScoreText' 생성
: UI-Text로 만듦
- NicknameText : Anchor, PosX, Width, Font, Color, Alignment 조정 (Text Input은 상관없음)
- ScoreText : Anchor, PosX, Font, Font Style, Color, Alignment 조정 (Text Input은 상관없음)
--> 둘다 Anchor는 Alt + Shift
7. 'ScoreCellController.cs' 생성
: 코드 작성
8. 'ScoreCell'에 'ScoreCellController.cs' 추가 및 바인딩
9. ScoreCell을 Prefab화
: 이후 Hierarchy에서 삭제
10. 'Content'에 'Content Size Fitter', 'Vertical Layout Group' 추가
: Test를 위해 ScoreCell Prefab을 'Content'의 자식으로 2개 추가
--> Content Size Fitter의 Vertical Fit을 Preferred Size로 설정
--> Vertical Layout Group 설정
- Control Child Size의 Width를 체크
- Padding의 Left를 30, Right를 30, Bottom을 50으로 설정
- Child Alignment를 Middle Center로 설정
- Spacing을 20으로 설정
--> 테스트 후 ScoreCell 삭제
--> Scroll View의 Color도 테스트용이었기 때문에 (255, 255, 255, 100)으로 원상복귀
11. 'Scrollbar Vertical'의 PosX, Source Image, Color 수정
: Scrollbar_bg를 Slice, Color는 (216, 216, 216, 255)
12. 'Handle'의 Source Image, Color 수정
: Color는 (140, 140, 140, 255)
13. 'LeaderboardPanelController.cs' 생성
: 코드 작성
14. 'LeaderboardPanel'에 'LeaderboardPanelController.cs' 추가 및 바인딩
15. 'CloseButton'의 OnClick()에 함수 바인딩
16. 'LeaderboardPanel'을 Prefab화 후, Hierarchy에서 삭제
17. MainPanel의 Buttons의 자식으로 'SettingsButton'을 복붙하여 'LeaderboardButton' 생성
: Buttons의 Height를 580으로 조정하고 Text를 '리더보드'로 수정
18. 'GameManager.cs', 'MainPanelController.cs'에 코드 작성
: 'LeaderboardButton'의 OnClick()에 함수 바인딩, GameManager.cs에 'LeaderboardPanel' Prefab 바인딩
※ NullReferenceException 에러가 발생
: GameManager.cs의 OpenLeaderboardPanel()에서 GetLeaderboard()할 때, ranks.scores가 null이 나오는 게 문제
--> Postman으로 테스트했을 때는 문제 없이 Score가 출력됨
25.03.14
해결 방법
: ChatGPT를 통해 해결했는데, 결과적으로 GetLeaderboard()의 'JsonUtility.FromJson<Scores>(result);'가 문제였다. 즉, JsonUtility의 문제였고 이를 대신하여 Newtonsoft.Json을 사용하니 해결되었다. (NetworkManager.cs에서 코드 수정)
--> Newtonsoft.Json 라이브러리는 JSON 키와 C# 필드명이 다르거나, 구조가 복잡할 때 더 유연하게 동작한다.
>> 리더보드 창의 부자연스러운 부분 수정
: 옆에 튀어나온 부분이나 Scrollbar가 아래로 더 튀어나온 것 수정하고 Panel의 크기를 늘림
--> Scroll View의 Bottom, Source Image와 Color의 Alpha값 수정 (Alpha 값은 0)
--> (왼)수정 전, (오)수정 후
└ 게임 승리 시, 점수 획득 구현
: 싱글 플레이로 AI와 대결하면 절대 승리할 수 없고 무승부가 최대기 때문에, 2인 플레이에서 승리하면 점수를 10점 얻도록 구현
>> NetworkManager.cs에 AddScore() 코드 작성
: JsonUtility.ToJson()은 int 값을 JSON으로 변환할 수 없으며, 객체만 변환 가능하다. --> 따라서 따로 구조체를 만들어야 한다.
>> GameLogic.cs의 ProcessMove()에서 GameResult가 Win일 때 AddScore()를 호출
: ProcessMove()를 가지고 있는 BasePlayerState가 MonoBehaviour를 상속받지 않기 때문에 StartCoroutine을 호출하면 에러가 발생
--> CoroutineHelper.cs를 Singleton 패턴으로 생성하여 StartCoroutine을 대신 실행해주는 함수를 구현
>> users.js 에서 addscore 관련 코드 수정
: ChatGPT를 통해 분석해보니, 서버의 /addscore API가 score를 덮어쓰기(set) 방식으로 저장하고 있었다. 따라서 기존 점수에서 10점을 추가하는 방식으로 구현하려면 $inc 연산자를 사용하여야 한다.
※ 70일차 글을 참고하여 멀티플레이를 위해 클라우드 서버에 연결해야한다!
채팅 서버 만들기
>> www 파일에 코드 추가
var game = require('../game'); // 추가
/**
* 게임 서버 실행
*/
game(server); // 추가
>> game.js 생성 및 코드 작성
>> 'Main Panel'의 자식으로 'Chatting Panel' 생성
: Color는 임의로 정한 색
--> 이름에 t 하나 빼먹음..
>> Chating Panel의 자식으로 Scroll View 생성
--> Anchor는 Alt + Shift
>> Content
>> Content의 자식으로 Text 추가
>> Chating Panel의 자식으로 Input Field 생성
--> Anchor는 Alt + Shift
>> Placeholder와 Text의 Alignment를 Middle로 설정
최종 코드
>> 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);
}
}
}
public IEnumerator GetLeaderboard(Action<Scores> success, Action failure)
{
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/leaderboard", 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 scores = JsonUtility.FromJson<Scores>(result);
success?.Invoke(scores);
}
}
}
}
>> 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 struct ScoreInfo
{
public string username;
public string nickname;
public int score;
}
public struct Scores
{
public ScoreInfo[] scores;
}
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();
}
}
>> 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 GameUIController _gameUIController;
private Canvas _canvas;
private Constants.GameType _gameType;
private GameLogic _gameLogic;
private void Start()
{
// 로그인
StartCoroutine(NetworkManager.Instance.GetScore((userInfo) =>
{
Debug.Log("자동 로그인 성공" + "\n아이디 : " + userInfo.username + "\n로그인 닉네임 : " + userInfo.nickname);
OpenConfirmPanel(userInfo.nickname + "님 로그인에 성공하였습니다.", () => { });
}, () =>
{
Debug.Log("자동 로그인 실패");
OpenSigninPanel();
}));
}
public void ChangeToGameScene(Constants.GameType gameType)
{
_gameType = gameType;
SceneManager.LoadScene("Game");
}
public void ChangeToMainScene()
{
_gameLogic?.Dispose();
_gameLogic = null;
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);
}
}
public void OpenGameOverPanel()
{
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
}
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (scene.name == "Game")
{
// Scene에 배치된 Object 찾기 (BlockController, GameUIController)
var blockController = GameObject.FindObjectOfType<BlockController>();
_gameUIController = GameObject.FindObjectOfType<GameUIController>();
// BlockController 초기화
blockController.InitBlocks();
// Game UI 초기화
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
// Game Logic 객체 생성 --> 생성자 호출
if (_gameLogic != null) _gameLogic.Dispose();
_gameLogic = new GameLogic(blockController, _gameType);
}
// Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
_canvas = GameObject.FindObjectOfType<Canvas>();
}
private void OnApplicationQuit()
{
_gameLogic?.Dispose();
_gameLogic = null;
}
}
>> app.js
>> leaderboard.js
>> www
>> game.js
└ 추가 최종 코드
>> GameManager.cs
: 자동 로그인, 리더보드
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : Singleton<GameManager>
{
[SerializeField] private GameObject leaderboardPanel;
[SerializeField] private GameObject settingsPanel;
[SerializeField] private GameObject confirmPanel;
[SerializeField] private GameObject signinPanel;
[SerializeField] private GameObject signupPanel;
private GameUIController _gameUIController;
private Canvas _canvas;
private Constants.GameType _gameType;
private GameLogic _gameLogic;
private void Start()
{
// 로그인
StartCoroutine(NetworkManager.Instance.GetScore((userInfo) =>
{
Debug.Log("자동 로그인 성공" + "\n아이디 : " + userInfo.username + "\n로그인 닉네임 : " + userInfo.nickname);
OpenConfirmPanel(userInfo.nickname + "님 로그인에 성공하였습니다.", () => { });
}, () =>
{
Debug.Log("자동 로그인 실패");
OpenSigninPanel();
}));
}
public void ChangeToGameScene(Constants.GameType gameType)
{
_gameType = gameType;
SceneManager.LoadScene("Game");
}
public void ChangeToMainScene()
{
_gameLogic?.Dispose();
_gameLogic = null;
SceneManager.LoadScene("Main");
}
public void OpenLeaderboardPanel()
{
if (_canvas != null)
{
var leaderboardPanelObject = Instantiate(leaderboardPanel, _canvas.transform);
StartCoroutine(NetworkManager.Instance.GetLeaderboard(ranks =>
{
foreach (var rank in ranks.scores)
{
var leaderboardController = leaderboardPanelObject.GetComponent<LeaderboardPanelController>();
leaderboardController.CreateScoreCell(rank);
}
}, () =>
{
Debug.Log("랭킹 가져오기 실패");
}));
leaderboardPanelObject.GetComponent<PanelController>().Show();
}
}
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);
}
}
public void OpenGameOverPanel()
{
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
}
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (scene.name == "Game")
{
// Scene에 배치된 Object 찾기 (BlockController, GameUIController)
var blockController = GameObject.FindObjectOfType<BlockController>();
_gameUIController = GameObject.FindObjectOfType<GameUIController>();
// BlockController 초기화
blockController.InitBlocks();
// Game UI 초기화
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
// Game Logic 객체 생성 --> 생성자 호출
if (_gameLogic != null) _gameLogic.Dispose();
_gameLogic = new GameLogic(blockController, _gameType);
}
// Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
_canvas = GameObject.FindObjectOfType<Canvas>();
}
private void OnApplicationQuit()
{
_gameLogic?.Dispose();
_gameLogic = null;
}
}
>> NetworkManager.cs
: 리더보드, 승리 시 점수 획득
using System;
using System.Collections;
using Newtonsoft.Json;
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);
}
}
}
public IEnumerator AddScore(ScoreData score, Action success, Action failure)
{
string jsonString = JsonUtility.ToJson(score);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/addscore", UnityWebRequest.kHttpVerbPOST))
{
www.uploadHandler = new UploadHandlerRaw(bodyRaw);
www.downloadHandler = new DownloadHandlerBuffer();
www.SetRequestHeader("Content-Type", "application/json");
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("로그인이 필요합니다.");
}
if (www.responseCode == 404)
{
Debug.Log("사용자를 찾을 수 없습니다.");
}
failure?.Invoke();
}
else
{
Debug.Log("Success Add Score");
success?.Invoke();
}
}
}
public IEnumerator GetLeaderboard(Action<Scores> success, Action failure)
{
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/leaderboard", 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 scores = JsonUtility.FromJson<Scores>(result); // scores가 null로 나오는 문제 발생
var scores = JsonConvert.DeserializeObject<Scores>(result); // 해결 코드
success?.Invoke(scores);
}
}
}
}
>> MainPanelController.cs
: 리더보드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MainPanelController : MonoBehaviour
{
public void OnClickSinglePlayButton()
{
GameManager.Instance.ChangeToGameScene(Constants.GameType.SinglePlayer);
}
public void OnClickDualPlayButton()
{
GameManager.Instance.ChangeToGameScene(Constants.GameType.DualPlayer);
}
public void OnClickMultiplayButton()
{
GameManager.Instance.ChangeToGameScene(Constants.GameType.MultiPlayer);
}
public void OnClickLeaderboardButton()
{
GameManager.Instance.OpenLeaderboardPanel();
}
public void OnClickSettingsButton()
{
GameManager.Instance.OpenSettingsPanel();
}
}
>> LeaderboardPanelController.cs
: 리더보드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LeaderboardPanelController : PanelController
{
[SerializeField] private GameObject scoreCell;
[SerializeField] private Transform content;
public void CreateScoreCell(ScoreInfo scoreInfo)
{
var scoreCellObject = Instantiate(scoreCell, content);
var scoreCellController = scoreCellObject.GetComponent<ScoreCellController>();
scoreCellController.SetCellInfo(scoreInfo);
}
/// <summary>
/// X 버튼 클릭 시 호출되는 함수
/// </summary>
public void OnClickCloseButton()
{
Hide();
}
}
>> ScoreCellController.cs
: 리더보드
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class ScoreCellController : MonoBehaviour
{
[SerializeField] private TMP_Text nicknameText;
[SerializeField] private TMP_Text scoreText;
public void SetCellInfo(ScoreInfo scoreInfo)
{
nicknameText.text = scoreInfo.nickname;
scoreText.text = scoreInfo.score.ToString();
}
}
>> GameLogic.cs
: 승리 시 점수 획득
using System;
using UnityEngine;
public abstract class BasePlayerState
{
public abstract void OnEnter(GameLogic gameLogic); // 해당 State가 할당될 때 처리
public abstract void OnExit(GameLogic gameLogic); // 해당 State가 교체될 때 처리
public abstract void HandleMove(GameLogic gameLogic, int row, int col); // 해당 State의 행위에 따라 마커 표시
protected abstract void HandleNextTurn(GameLogic gameLogic); // State 전환
// 마커 표시 및 결과 처리
protected void ProcessMove(GameLogic gameLogic, Constants.PlayerType playerType, int row, int col)
{
if (gameLogic.SetNewBoardValue(playerType, row, col))
{
var gameResult = gameLogic.CheckGameResult();
if (gameResult == GameLogic.GameResult.None)
{
HandleNextTurn(gameLogic);
}
else
{
switch (gameResult)
{
case GameLogic.GameResult.Win:
Debug.Log("GameResult : Win");
CoroutineHelper.Instance.AddScore();
break;
}
gameLogic.EndGame(gameResult);
}
}
}
}
// 직접 플레이 (싱글, 네트워크)
public class PlayerState : BasePlayerState
{
private Constants.PlayerType _playerType;
private bool _isFirstPlayer;
private MultiplayManager _multiplayManager;
private string _roomId;
private bool _isMultiplay;
public PlayerState(bool isFirstPlayer)
{
_isFirstPlayer = isFirstPlayer;
_playerType = _isFirstPlayer ? Constants.PlayerType.PlayerA : Constants.PlayerType.PlayerB;
_isMultiplay = false;
}
public PlayerState(bool isFirstPlayer, MultiplayManager multiplayManager, string roomId) : this(isFirstPlayer)
{
_multiplayManager = multiplayManager;
_roomId = roomId;
_isMultiplay = true;
}
public override void OnEnter(GameLogic gameLogic)
{
gameLogic.blockController.OnBlockClickedDelegate = (row, col) =>
{
HandleMove(gameLogic, row, col);
};
}
public override void OnExit(GameLogic gameLogic)
{
gameLogic.blockController.OnBlockClickedDelegate = null; // 게임이 끝났을 때 board를 Click해도 바뀌지 않도록
}
public override void HandleMove(GameLogic gameLogic, int row, int col)
{
ProcessMove(gameLogic, _playerType, row, col);
if (_isMultiplay)
{
_multiplayManager.SendPlayerMove(_roomId, row * 3 + col);
}
}
protected override void HandleNextTurn(GameLogic gameLogic)
{
if (_isFirstPlayer)
{
gameLogic.SetState(gameLogic.secondPlayerState);
}
else
{
gameLogic.SetState(gameLogic.firstPlayerState);
}
}
}
// AI 플레이
public class AIState : BasePlayerState
{
public override void OnEnter(GameLogic gameLogic)
{
// AI 연산
var result = MinimaxAIController.GetBestMove(gameLogic.GetBoard());
if (result.HasValue)
{
HandleMove(gameLogic, result.Value.row, result.Value.col);
}
else
{
gameLogic.EndGame(GameLogic.GameResult.Draw);
}
}
public override void OnExit(GameLogic gameLogic)
{
}
public override void HandleMove(GameLogic gameLogic, int row, int col)
{
ProcessMove(gameLogic, Constants.PlayerType.PlayerB, row, col);
}
protected override void HandleNextTurn(GameLogic gameLogic)
{
gameLogic.SetState(gameLogic.firstPlayerState);
}
}
// 네트워크 플레이 --> 서버로부터 상대 Player를 기다림
public class MultiplayState : BasePlayerState
{
private Constants.PlayerType _playerType;
private bool _isFirstPlayer;
private MultiplayManager _multiplayManager;
public MultiplayState(bool isFirstPlayer, MultiplayManager multiplayManager)
{
_isFirstPlayer = isFirstPlayer;
_playerType = _isFirstPlayer ? Constants.PlayerType.PlayerA : Constants.PlayerType.PlayerB;
_multiplayManager = multiplayManager;
}
public override void OnEnter(GameLogic gameLogic)
{
_multiplayManager.OnOpponentMove = moveData =>
{
var row = moveData.position / 3;
var col = moveData.position % 3;
UnityThread.executeInUpdate(() =>
{
HandleMove(gameLogic, row, col);
});
};
}
public override void OnExit(GameLogic gameLogic)
{
_multiplayManager.OnOpponentMove = null;
}
public override void HandleMove(GameLogic gameLogic, int row, int col)
{
ProcessMove(gameLogic, _playerType, row, col);
}
protected override void HandleNextTurn(GameLogic gameLogic)
{
if (_isFirstPlayer)
{
gameLogic.SetState(gameLogic.secondPlayerState);
}
else
{
gameLogic.SetState(gameLogic.firstPlayerState);
}
}
}
public class GameLogic : IDisposable
{
public BlockController blockController;
private Constants.PlayerType[,] _board;
public BasePlayerState firstPlayerState; // 첫 번째 턴 상태 객체
public BasePlayerState secondPlayerState; // 두 번째 턴 상태 객체
private BasePlayerState _currentPlayerState; // 현재 턴 상태 객체
private MultiplayManager _multiplayManager;
private string _roomId;
public enum GameResult { None, Win, Lose, Draw }
public GameLogic(BlockController blockController, Constants.GameType gameType,
MultiplayManager multiplayManager = null, string roomId = null, bool isFirstPlayer = true)
{
this.blockController = blockController;
// _board 초기화
_board = new Constants.PlayerType[3, 3];
switch (gameType)
{
case Constants.GameType.SinglePlayer:
firstPlayerState = new PlayerState(true);
secondPlayerState = new AIState();
// 게임 시작
SetState(firstPlayerState);
break;
case Constants.GameType.DualPlayer:
firstPlayerState = new PlayerState(true);
secondPlayerState = new PlayerState(false);
// 게임 시작
SetState(firstPlayerState);
break;
case Constants.GameType.MultiPlayer:
// Multiplay Manager 생성
_multiplayManager = new MultiplayManager((state, roomId) =>
{
_roomId = roomId;
switch (state)
{
case Constants.MultiplayManagerState.CreateRoom:
Debug.Log("## Create Room ##");
// TODO: 대기 화면 표시
// GameManager에게 대기 화면 표시 요청
break;
case Constants.MultiplayManagerState.JoinRoom: // 게임 실행 --> Room에 입장한 사람
Debug.Log("## Join Room ##");
firstPlayerState = new MultiplayState(true, _multiplayManager);
secondPlayerState = new PlayerState(false, _multiplayManager, roomId);
// 게임 시작
SetState(firstPlayerState);
break;
case Constants.MultiplayManagerState.ExitRoom:
Debug.Log("## Exit Room ##");
// TODO: Exit Room 처리
break;
case Constants.MultiplayManagerState.StartGame: // 대기 화면을 닫고 게임 실행 --> Create Room을 한 사람
Debug.Log("## Start Game ##");
firstPlayerState = new PlayerState(true, _multiplayManager, roomId);
secondPlayerState = new MultiplayState(false, _multiplayManager);
// 게임 시작
SetState(firstPlayerState);
break;
case Constants.MultiplayManagerState.EndGame:
Debug.Log("## End Game ##");
// TODO: End Game 처리
break;
}
});
break;
}
}
public void SetState(BasePlayerState state)
{
_currentPlayerState?.OnExit(this);
_currentPlayerState = state;
_currentPlayerState?.OnEnter(this);
}
/// <summary>
/// _board에 새로운 값을 할당하는 함수
/// </summary>
/// <param name="playerType">할당하고자 하는 플레이어 타입</param>
/// <param name="row">Row</param>
/// <param name="col">Col</param>
/// <returns>False : 할당할 수 없음, True : 할당이 완료됨</returns>
public bool SetNewBoardValue(Constants.PlayerType playerType, int row, int col)
{
if (_board[row, col] != Constants.PlayerType.None) return false; // 중복 체크 방지
if (playerType == Constants.PlayerType.PlayerA)
{
_board[row, col] = playerType;
blockController.PlaceMarker(Block.MarkerType.O, row, col);
return true;
}
else if (playerType == Constants.PlayerType.PlayerB)
{
_board[row, col] = playerType;
blockController.PlaceMarker(Block.MarkerType.X, row, col);
return true;
}
return false;
}
/// <summary>
/// 게임 결과 확인 함수
/// </summary>
/// <returns>플레이어 기준 게임 결과</returns>
public GameResult CheckGameResult()
{
if (CheckGameWin(Constants.PlayerType.PlayerA)) return GameResult.Win;
if (CheckGameWin(Constants.PlayerType.PlayerB)) return GameResult.Lose;
if (MinimaxAIController.IsAllBlocksPlaced(_board)) return GameResult.Draw;
return GameResult.None;
}
// 게임의 승패를 판단하는 함수
private bool CheckGameWin(Constants.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;
}
/// <summary>
/// 게임 오버 시, 호출되는 함수
/// gameResult에 따라 결과 출력
/// </summary>
/// <param name="gameResult">win, lose, draw</param>
public void EndGame(GameResult gameResult)
{
SetState(null);
firstPlayerState = null;
secondPlayerState = null;
// 게임 오버 표시
GameManager.Instance.OpenGameOverPanel();
}
public Constants.PlayerType[,] GetBoard()
{
return _board;
}
public void Dispose()
{
_multiplayManager?.LeaveRoom(_roomId);
_multiplayManager?.Dispose();
}
}
>> CoroutineHelper.cs
: 승리 시 점수 획득
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public struct ScoreData
{
public int score;
}
public class CoroutineHelper : Singleton<CoroutineHelper>
{
ScoreData scoreData = new ScoreData { score = 10 };
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
public void MyStartCoroutine(IEnumerator coroutine)
{
StartCoroutine(coroutine);
}
public void AddScore()
{
StartCoroutine(NetworkManager.Instance.AddScore((scoreData), () =>
{
Debug.Log("AddScore Complete");
}, () => { }));
}
}
>> users.js
: 승리 시 점수 획득
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 70일차 (0) | 2025.03.10 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 69일차 (0) | 2025.03.07 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 67일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 66일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 65일차 (0) | 2025.03.05 |