오늘은 오전에 TicTacToe 리더보드 구현을 자습으로 진행했다.
먼저 서버에 전체 유저 점수를 조회해서 보내주기 위한 get 메서드를 추가했다.
// 전체 유저 점수 조회
router.get('/scorelist', async function(req, res, next) {
try {
if (!req.session.isAuthenticated) {
return res.status(403).send("로그인이 필요합니다");
}
var database = req.app.get('database');
var users = database.collection('users');
const userList = await users.find().sort({score: -1}).toArray();
if (userList.length === 0) {
return res.status(404).send("사용자가 존재하지 않습니다");
}
const filteredUserList = userList.map(user => ({
id: user._id, // _id를 id로 변경
username: user.username,
nickname: user.nickname,
score: user.score
}));
res.json(filteredUserList);
} catch (err) {
console.error("점수 조회 중 오류 발생:", err);
res.status(500).send("서버 오류가 발생했습니다");
}
});
서버랑 통신하는 NetworkManager.cs에 아래처럼 점수 추가, 전체 유저 점수 조회 메서드를 추가했다.
해당 유저 점수 조회는 GameManager에서 맨 처음 실행되고 있는데 이때 가져온 userId를 PlayerPrefs에 할당해서 해당 userId를 활용해 리더보드에서 현재 유저의 점수를 최상단에 표시하도록 구현했다.
public void GetScore(Action<ScoreResult> success, Action failure)
{
StartCoroutine(GetScoreCoroutine(success, failure));
}
public IEnumerator GetScoreCoroutine(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)
{
Debug.Log("Error: " + www.error);
if (www.responseCode == 403)
{
GameManager.Instance.OpenConfirmPanel("로그인이 필요합니다", () =>
{
// 로그인 화면
GameManager.Instance.OpenSigninPanel();
}); }
failure?.Invoke();
}
else
{
var result = www.downloadHandler.text;
var userScore = JsonUtility.FromJson<ScoreResult>(result); // ScoreResult로 result를 변환
Debug.Log("result" + result);
PlayerPrefs.SetString("user_id", userScore.id); // 서버에서 로그인 시 ID 저장 필요
Debug.Log(userScore.id);
success?.Invoke(userScore);
}
}
}
public void AddScore(Action success, Action failure)
{
StartCoroutine(AddScoreCoroutine(success, failure));
}
public IEnumerator AddScoreCoroutine(Action success, Action failure)
{
var scoreRequest = new ScoreRequest(ScoreData.score);
string jsonString = JsonUtility.ToJson(scoreRequest);
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");
// 세션 ID 가져와서 요청에 포함
string sid = PlayerPrefs.GetString("sid", "");
if (string.IsNullOrEmpty(sid))
{
Debug.Log("세션 정보가 없습니다. 로그인이 필요합니다.");
GameManager.Instance.OpenSigninPanel();
yield break; // 요청 중단
}
www.SetRequestHeader("Cookie", sid);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
if (www.responseCode == 403)
{
GameManager.Instance.OpenConfirmPanel("로그인이 필요합니다", () =>
{
// 로그인 화면
GameManager.Instance.OpenSigninPanel();
}); }
failure?.Invoke();
}
else
{
var result = www.downloadHandler.text;
Debug.Log("Result: " + result);
success?.Invoke();
}
}
}
public void GetScoreList(Action<string> success, Action failure)
{
StartCoroutine(GetScoreListCoroutine(success, failure));
}
public IEnumerator GetScoreListCoroutine(Action<string> success, Action failure)
{
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/scorelist", 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)
{
Debug.Log("Error: " + www.error);
if (www.responseCode == 403)
{
GameManager.Instance.OpenConfirmPanel("로그인이 필요합니다", () =>
{
// 로그인 화면
GameManager.Instance.OpenSigninPanel();
}); }
failure?.Invoke();
}
else
{
var result = www.downloadHandler.text;
Debug.Log(result);
success?.Invoke(result);
}
}
}
각 리더보드 아이템에 추가할 코드는 아래처럼 추가했다.
LeaderboardItem.cs
using TMPro;
using UnityEngine;
public class LeaderboardItem : MonoBehaviour
{
public TextMeshProUGUI nicknameText;
public TextMeshProUGUI scoreText;
public void SetLeaderboardItems(string nickname, int score)
{
nicknameText.text = nickname;
scoreText.text = score.ToString();
}
}
리더보드 패널에 들어갈 스크립트는 아래처럼 작성했다. struct 사용이 아직 어색하게 느껴진다. 쓰다보면 나아지겠지...
LeaderboardPanel.cs
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;
public struct ScoreResult
{
public string id;
public string username;
public string nickname;
public int score;
}
public static class ScoreData
{
public static int score = 0; // 기본 점수는 0
// 점수를 추가하는 메서드
public static void AddScore(int amount)
{
score += amount;
}
public static void SetScore(int amount)
{
score = amount;
}
}
public class ScoreRequest
{
public int score; // 서버에 보낼 점수 데이터
public ScoreRequest(int score)
{
this.score = score;
}
}
public class LeaderboardPanel : MonoBehaviour
{
[SerializeField] public Transform content;
[SerializeField] public GameObject LeaderboardItemPrefab;
private void Start()
{
LoadLeaderboard();
}
public void LoadLeaderboard()
{
NetworkManager.Instance.GetScoreList(OnSuccess, OnFailure);
}
private void OnSuccess(string jsonData)
{
// 서버에서 받은 JSON을 리스트로 변환
List<ScoreResult> scores = JsonConvert.DeserializeObject< List<ScoreResult>>(jsonData);
string currentUserId = PlayerPrefs.GetString("user_id", "");
// 현재 유저의 데이터 찾기
ScoreResult? currentUserData = scores.FirstOrDefault(s => s.id == currentUserId);
// 현재 유저를 제외한 다른 유저 리스트
List<ScoreResult> otherUsers = scores.Where(s => s.id != currentUserId).ToList();
otherUsers = otherUsers.OrderByDescending(s => s.score).ToList();
// 기존 목록 삭제
foreach (Transform child in content)
{
Destroy(child.gameObject);
}
/// 현재 유저 데이터를 먼저 추가 (있을 경우)
if (currentUserData.HasValue)
{
GameObject leaderboardItem = Instantiate(LeaderboardItemPrefab, content);
LeaderboardItem item = leaderboardItem.GetComponent<LeaderboardItem>();
item.SetLeaderboardItems(currentUserData.Value.nickname, currentUserData.Value.score);
// 현재 유저 강조 표시
item.nicknameText.GetComponent<TMP_Text>().color = new Color(255f, 200f, 0f, 255f);
item.scoreText.GetComponent<TMP_Text>().color = new Color(255f, 200f, 0f, 255f);
}
// 나머지 유저 데이터 추가
foreach (var scoredata in otherUsers)
{
GameObject leaderboardItem = Instantiate(LeaderboardItemPrefab, content);
LeaderboardItem item = leaderboardItem.GetComponent<LeaderboardItem>();
item.SetLeaderboardItems(scoredata.nickname, scoredata.score);
}
}
private void OnFailure()
{
Debug.Log("리더보드 데이터를 불러오지 못했습니다.");
}
}
점수 추가는 GameManager.cs에서 기존에 있던 코드에 추가했다.
/// <summary>
/// 게임 오버시 호출되는 함수
/// gameResult에 따라 결과 출력
/// </summary>
/// <param name="gameResult">win, lose, draw</param>
private void EndGame(GameResult gameResult)
{
// 게임오버 표시
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
_blockController.OnBlockClickedDelegate = null;
// TODO: 나중에 구현!!
switch (gameResult)
{
case GameResult.Win:
ScoreData.AddScore(10);
NetworkManager.Instance.AddScore(() => { Debug.Log("점수 업데이트 성공!"); },
() => { Debug.Log("점수 업데이트 실패!"); });
Debug.Log("Player A win");
break;
case GameResult.Lose:
Debug.Log("Player B win");
break;
case GameResult.Draw:
Debug.Log("Draw");
break;
}
}
리더보드 패널 여는 메서드도 아래처럼 GameManager.cs에 추가했다.
public void OpenLeaderboardPanel()
{
if (_canvas != null)
{
var leaderboardPanelObject = Instantiate(leaderboardPanel, _canvas.transform);
}
}
마지막으로 해당 버튼을 누르면 리더보드 패널이 열리도록 구현했다.
public void OnClickScoreButton()
{
GameManager.Instance.OpenLeaderboardPanel();
// NetworkManager.Instance.GetScoreList((scoreList) =>
// {
//
// }, () => { });
}
후다닥 짜다보니 헷갈려서 중간에 삽질을 많이했고 코드도 발전가능성이 많아 보이지만 일단은 나름 원하던 결과물은 나온 것 같다.
특히 서버에서 처음에 그냥 userList를 넘겼더니 _id로 id값이 넘어와서 클라이언트에서 처리할 때 제대로 된 값을 할당해주지 못하는 이슈가 있었다.
처음에는 클라이언트에서 현재 유저 찾을 때 _id로 찾을 수 있도록 수정했으나 이미 기존 서버에서 현재 유저의 점수만 가져올 때 id로 바꿔서 가져오고 있었기 때문에, 이러면 currentUserId가 null이 되면서(id로 찾아야하는데 _id로 찾아서 값이 null) 통일이 되지 않았다.
그래서 서버에서 통일하는 게 맞다는 생각이 들어 map을 써서 수정했다. 오랜만에 백엔드 잡으니 재밌었다.
'공부 > Game Bootcamp' 카테고리의 다른 글
[TIL] #2. 에셋 캐릭터 애니메이션 편집 : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.17 |
---|---|
[TIL] #1. 모바일 게임 플레이어 이동 구현 : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.16 |
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : 궤적, HpBar (0) | 2024.12.19 |
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : 스킬 + 액션툴 (0) | 2024.12.18 |
[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : Curve, 몬스터 생성, 몬스터와 충돌 이벤트 (1) | 2024.12.17 |