[Steamworks] 리더보드 API 활용하기 (2)

MINO·2025년 5월 19일

이전 포스팅에서 작성한 Steamworks 리더보드 API 활용하기의 스크립트.

SteamworksManager.cs

주요 메서드설명
InitializeLeaderboard(string difficulty)리더보드 생성 또는 불러오기
SubmitScore(string difficulty,int score)점수 제출 (국가 정보 포함)
FetchLeaderboardEntries(string difficulty)해당 난이도의 리더보드 유저 목록 가져오기
GetClientLeaderboardEntry(string difficulty)본인의 순위 및 점수 확인
EncodeCountryCode(string countryCode) / DecodeCountryCode(int countryHash)국가 데이터 암호화, 복호화 처리


using Steamworks;
using Steamworks.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

public class SteamworksManager : Singleton<SteamworksManager>
{
	// 내 게임의 steam App ID
    [SerializeField] private uint appId;
    
    // 난이도 별 리더보드를 캐싱해두는 딕셔너리
    private Dictionary<string, Leaderboard?> leaderboards = new Dictionary<string, Leaderboard?>();


    private void Awake()
    {
	    // 중복 초기화 방지
        if (SteamClient.IsValid) 
        {
            Debug.LogWarning("Steamworks already initialized. Skipping initialization.");
            return;
        }

        try
        {
            SteamClient.Init(appId, true); // Steam 초기화
            Init(); // 리더보드 초기화
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Steamworks 초기화 실패: {e.Message}");
        }
    }

    private async void Init()
    {
        await InitializeLeaderboard("Easy");
        await InitializeLeaderboard("Normal");
        await InitializeLeaderboard("Hard");
    }

    private void Update()
    {
        Steamworks.SteamClient.RunCallbacks(); // Steam 콜백 처리 루프
    }

    private void OnApplicationQuit()
    {
        SteamCloudManager.Instance.Save(); // 클라우드 저장
        SteamClient.Shutdown(); // 프로그램 종료 시, Steam 연결 해제
    }

	// 리더보드를 생성하거나 찾아서 초기화
    public async Task<bool> InitializeLeaderboard(string difficulty)
    {
        if (leaderboards.ContainsKey(difficulty) && leaderboards[difficulty].HasValue)
            return true;

        try
        {
            var leaderboard = await SteamUserStats.FindOrCreateLeaderboardAsync(
                difficulty, LeaderboardSort.Descending, LeaderboardDisplay.Numeric);

            if (leaderboard.HasValue)
            {
                leaderboards[difficulty] = leaderboard;
                Debug.Log($"Leaderboard '{difficulty}' initialized.");
                return true;
            }
            else
            {
                Debug.LogError($"Failed to initialize leaderboard '{difficulty}'.");
                return false;
            }
        }
        catch (Exception ex)
        {
            Debug.LogError($"Leaderboard initialization failed: {ex.Message}");
            return false;
        }
    }

	// 상위 10명의 리더보드 정보 불러오기
    public async Task<List<LeaderboardEntry>> FetchLeaderboardEntries(string difficulty)
    {
        if (!leaderboards.ContainsKey(difficulty) || !leaderboards[difficulty].HasValue)
        {
            Debug.LogError($"Leaderboard '{difficulty}' is not initialized.");
            return null;
        }

        var entries = await leaderboards[difficulty].Value.GetScoresAsync(10);

        if (entries == null)
            return new List<LeaderboardEntry>();

        return entries.ToList();
    }

	// 점수 업로드 (국가 정보 포함)
    public async Task<bool> SubmitScore(string difficulty, int score)
    {
        if (!leaderboards.ContainsKey(difficulty) || !leaderboards[difficulty].HasValue)
        {
            Debug.LogError($"Leaderboard '{difficulty}' is not initialized.");
            return false;
        }
		
        // SteamUtils.IpCountry 를 통해 유저의 국가 코드 받아오기
        int countryHash = EncodeCountryCode(SteamUtils.IpCountry.ToLower());
        
        // 국가 코드 인코딩 후 details 에 포함
        int[] details = new int[] { countryHash };

        var result = await leaderboards[difficulty].Value.SubmitScoreAsync(score, details);

        if (result.HasValue)
        {
            return true;
        }
        else
        {
            Debug.LogError("Failed to submit score.");
            return false;
        }
    }

	// 현재 유저의 리더보드 순위 및 점수 조회
    public async Task<LeaderboardEntry?> GetClientLeaderboardEntry(string difficulty)
    {
        if (!leaderboards.ContainsKey(difficulty) || !leaderboards[difficulty].HasValue)
        {
            Debug.LogError($"Leaderboard '{difficulty}' is not initialized.");
            return null;
        }

        var entry = await leaderboards[difficulty].Value.GetScoresForUsersAsync(new SteamId[] { SteamClient.SteamId });

        return (entry != null && entry.Length > 0) ? entry[0] : (LeaderboardEntry?)null;
    }
	
    // 국가 코드를 숫자로 인코딩 ("kr" -> 정수)
    public int EncodeCountryCode(string countryCode)
    {
        if (countryCode.Length != 2) return -1;
        return (countryCode[0] - 'a') * 26 + (countryCode[1] - 'a');
    }
	
    // 인코딩된 숫자를 국가 코드로 디코딩 (정수 -> "kr")
    public string DecodeCountryCode(int countryHash)
    {
        if (countryHash < 0 || countryHash > 675) return "??";

        char firstChar = (char)('a' + (countryHash / 26));
        char secondChar = (char)('a' + (countryHash % 26));

        return $"{firstChar}{secondChar}";
    }
}

LeaderboardManager.cs

using Steamworks;
using Steamworks.Data;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

public class LeaderboardManager : MonoBehaviour
{
    public GameObject leaderboardEntryPrefab; // 리더보드 요소 프리팹
    public Transform leaderboardParent; // 리더보드 UI 를 붙일 부모 오브젝트
    public GameObject myScore; // 플레이어 자신의 점수 UI 오브젝트
    private string currentDifficulty = "Easy"; // 기본 난이도

	// UI 가 활성화될 때 호출
    private void OnEnable()
    {
        OnDifficultySelected(currentDifficulty);
    }
	
    // UI 가 비활성화될 때 호출
    private void OnDisable()
    {
        StopAllCoroutines();
    }
    
    // 난이도 변경 시 호출
    public void OnDifficultySelected(string difficulty)
    {
        currentDifficulty = difficulty;
        StartCoroutine(UpdateLeaderboardCoroutine());
    }
	
    // 비동기 함수와 Unity 코루틴 연결
    private IEnumerator UpdateLeaderboardCoroutine()
    {
        var task = FetchAndUpdateLeaderboard();
        yield return new WaitUntil(() => task.IsCompleted);
    }
	
    // 리더보드 데이터 요청 및 UI 갱신
    private async Task FetchAndUpdateLeaderboard()
    {
        bool initialized = await SteamworksManager.Instance.InitializeLeaderboard(currentDifficulty);

        if (!initialized)
        {
            Debug.LogError("Failed to initialize leaderboard.");
            return;
        }


		// 리더보드 항목 가져오기
        List<LeaderboardEntry> entries = await SteamworksManager.Instance.FetchLeaderboardEntries(currentDifficulty);
        UpdateLeaderboardUI(entries);

		// 내 데이터 가져오기
        var myEntry = await SteamworksManager.Instance.GetClientLeaderboardEntry(currentDifficulty);

        if (myEntry.HasValue)
            UpdateMyScoreUI(myEntry.Value);
        else
            UpdateMyScoreUI();
        
    }
	
    // 리더보드 목록 UI 갱신
    private void UpdateLeaderboardUI(List<LeaderboardEntry> entries = null)
    {
        // 기존 항목 제거
        foreach (Transform child in leaderboardParent)
        {
            Destroy(child.gameObject);
        }

        if (entries == null || entries.Count == 0)
        {
            return;
        }

        // 새로운 항목 추가
        foreach (var entry in entries)
        {
            GameObject entryObject = Instantiate(leaderboardEntryPrefab, leaderboardParent);
            LeaderboardEntryUI entryUI = entryObject.GetComponent<LeaderboardEntryUI>();

            if (entryUI != null)
            {
                string countryCode = SteamworksManager.Instance.DecodeCountryCode(entry.Details.Length > 0 ? entry.Details[0] : -1);
                entryUI.SetEntry(entry.GlobalRank, countryCode, entry.User.Name, entry.Score);
            }
        }
    }
	
    // 리더보드에 내 데이터가 있는 경우 UI 갱신
    private void UpdateMyScoreUI(LeaderboardEntry entry)
    {
        LeaderboardEntryUI entryUI = myScore.GetComponent<LeaderboardEntryUI>();
        
        if (entryUI != null)
        {
            string countryCode = SteamworksManager.Instance.DecodeCountryCode(entry.Details.Length > 0 ? entry.Details[0] : -1);
            entryUI.SetEntry(entry.GlobalRank, countryCode, entry.User.Name, entry.Score);
        }
    }
	
    // 리더보드에 내 데이터가 없는 경우 UI 갱신
    private void UpdateMyScoreUI()
    {
        LeaderboardEntryUI entryUI = myScore.GetComponent<LeaderboardEntryUI>();
        int countryHash = SteamworksManager.Instance.EncodeCountryCode(SteamUtils.IpCountry.ToLower());
        string countryCode = SteamworksManager.Instance.DecodeCountryCode(countryHash);
        string name = SteamClient.Name;
        if (entryUI != null)
        {
            entryUI.SetEntry(0, countryCode, name, 0);
        }
    }
}

LeaderboardEntryUI.cs

받아온 리더보드 데이터를 각각 UI 에 표시하기 위한 EntryUI 스크립트

국가 코드를 Web API 를 통해 국기 이미지로 받아오고 캐시에 저장해두는 역할도 수행

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class LeaderboardEntryUI : MonoBehaviour
{
    public TextMeshProUGUI rankText; // 순위 텍스트
    public Image countryImg; // 국기 이미지
    public TextMeshProUGUI nameText; // 플레이어 이름 텍스트
    public TextMeshProUGUI scoreText; // 점수 텍스트
	
    // 국기 이미지 캐시
    private static Dictionary<string, Sprite> flagCache = new Dictionary<string, Sprite>();
    // ISO 국가 코드 기반 국기 이미지 URL
    private const string FLAG_API_URL = "https://flagcdn.com/w80/{0}.png"; 

	// 오브젝트 비활성화 시 실행 중인 코루틴 중지
    private void OnDisable()
    {
        StopAllCoroutines();
    }

	// 리더보드 항목 UI 세팅
    public void SetEntry(int rank, string countryCode, string name, int score)
    {
        if (!gameObject.activeInHierarchy)
            return;
		
        // 국가 코드가 없을 경우 국기 없이 표시
        if (string.IsNullOrEmpty(countryCode))
        {
            rankText.text = rank.ToString();
            nameText.text = name;
            countryImg.sprite = null;
            scoreText.text = score.ToString();
            return;
        }
		
        // 이미지 로딩 중 깜빡임 방지를 위해 텍스트 임시 비활성화
        rankText.gameObject.SetActive(false);
        nameText.gameObject.SetActive(false);
        scoreText.gameObject.SetActive(false);
        countryImg.gameObject.SetActive(false);

		// 국기 이미지 비동기 로드 후 UI 활성화
        LoadCountryFlag(countryCode, () =>
        {
            rankText.text = rank.ToString();
            nameText.text = name;
            scoreText.text = score.ToString();

            rankText.gameObject.SetActive(true);
            nameText.gameObject.SetActive(true);
            scoreText.gameObject.SetActive(true);
            countryImg.gameObject.SetActive(true);
        });
    }

	// 국기 이미지 불러오기 (캐시 확인 → 다운로드)
    private void LoadCountryFlag(string countryCode, System.Action onFlagLoaded)
    {
        if (flagCache.TryGetValue(countryCode, out Sprite cachedFlag))
        {
            countryImg.sprite = cachedFlag;
            countryImg.SetNativeSize();
            onFlagLoaded?.Invoke(); // 캐시에서 불러온 경우 즉시 콜백
            return;
        }

        string url = string.Format(FLAG_API_URL, countryCode.ToLower());
        StartCoroutine(DownloadFlag(url, countryCode, onFlagLoaded));
    }


	// 국기 이미지 다운로드 후 적용
    private IEnumerator DownloadFlag(string url, string countryCode, System.Action onFlagLoaded)
    {
        if (!gameObject.activeInHierarchy) 
            yield break;

        UnityWebRequest request = UnityWebRequestTexture.GetTexture(url);
        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            Texture2D texture = ((DownloadHandlerTexture)request.downloadHandler).texture;
            Sprite flagSprite = SpriteFromTexture(texture);

            flagCache[countryCode] = flagSprite; // 캐시 저장
            countryImg.sprite = flagSprite;
            countryImg.SetNativeSize();
        }
        else
        {
            countryImg.sprite = null; // 실패 시 빈 이미지 처리
            Debug.LogWarning($"Failed to load flag for {url}: {request.error}");
        }

        onFlagLoaded?.Invoke(); // 로딩 완료 후 UI 활성화
    }

	// Texture2D → Sprite 변환
    private Sprite SpriteFromTexture(Texture2D texture)
    {
        return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
    }
}

하이어라키 및 씬 설정

계층 구조배치 구조Entry
profile
안녕하세요 게임 개발하는 MINO 입니다.

0개의 댓글