이전 포스팅에서 작성한 Steamworks 리더보드 API 활용하기의 스크립트.
| 주요 메서드 | 설명 |
|---|---|
| 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}";
}
}
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);
}
}
}
받아온 리더보드 데이터를 각각 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 |
|---|---|---|
![]() | ![]() | ![]() |