공부/Game Bootcamp

[멋쟁이사자처럼 부트캠프 TIL] 유니티 게임 개발 3기 : 리더보드 구현

Ail_ 2025. 3. 6. 18:26

 

 

오늘은 오전에 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을 써서 수정했다. 오랜만에 백엔드 잡으니 재밌었다.