Unity6로 작업했습니다.
Unity Stove PCSDK3 연동방법은 스토브 developers에 자세히 나와 있습니다.
따라서 연동하면 되기는 하지만 경험한 에러나 주의할 점에 대해서 적어봅니다.
https://developers-beta.onstove.com/ko/docs/Store/PCSDK3/PCSDK_v3_intro
스토브 스토어 개발자 가이드
developers-beta.onstove.com
제가 경험한 것에 따라 적은 것이기에 참고로만 봐주세요!
1. 작성한 전체 StovePCSDK3Manager
- Unitask를 사용하였기에 Unitask를 Coroutine으로 변경해서 쓰셔도 됩니다.
- 상속한 Singleton코드도 아래에 같이 올립니다.
- StovePCSDK3Manager.cs
using System;
using System.Text;
using UnityEngine;
using Cysharp.Threading.Tasks;
using static Stove.PCSDK.Base;
using static Stove.PCSDK.GameSupport;
/// <summary>
/// https://developers-beta.onstove.com/ko/docs/Store/info/buildtest
/// https://developers-beta.onstove.com/ko/docs/Store/PCSDK3/PCSDK_v3_intro
/// https://developers-beta.onstove.com/ko/docs/Store/PCSDK3/PCSDK_v3_gamesupport
/// </summary>
public class StovePCSDK3Manager : Singleton<StovePCSDK3Manager>
{
[Header("[StovePCInitializeParam]")]
[SerializeField] private string environment = "LIVE";
[SerializeField] private string gameId;
[SerializeField] private string applicationKey;
[Header("[Running Setting]")]
[SerializeField] private int runCallbackInterval = 1000; // ms 단위
private bool isCallbackRunning = false;
// Base SDK 초기화 성공 여부
[field:Space(10)]
[field:SerializeField] public bool isInitialized { get; private set; } = false;
// Base SDK 초기화 여부 콜백
private OnInitializeFinished onInitializeFinished;
// Access token 갱신 콜백
private OnRenewTokenFinished onRenewTokenFinished;
// Stat 조회 콜백
public OnStatFinished onStatOnFinished;
// Stat 변경 콜백
public OnModifyStatFinished onModifyStatOnFinished;
// Rank 조회 콜백
public OnRankFinished onRankOnFinished;
private readonly StringBuilder sb = new StringBuilder(60);
protected override void Awake()
{
base.Awake();
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
private void Start()
{
Initialize();
}
private void OnApplicationQuit()
{
UnInitialize();
}
/// <summary>
/// SDK 초기화
/// </summary>
private void Initialize()
{
StovePCInitializeParam initParam = new StovePCInitializeParam
{
environment = environment,
gameId = gameId,
applicationKey = applicationKey
};
// 콜백 루프 시작
StartRunCallbackLoop();
// PC 클라이언트로부터 게임이 실행이 되었는지 검증
Base_RestartAppIfNecessaryAsync(initParam, 10_000, (callbackResult, restartAppIfNecessary) =>
{
PrintCallbackResult(callbackResult);
if (restartAppIfNecessary)
{
// 런처를 통해 게임을 실행하지 않았므로 게임종료 처리를 진행합니다.
Debug.Log("Please run the game through the stove launcher.");
Application.Quit();
}
else
{
// onInitializeFinished 콜백 설정
BaseSDKInitializeFinished();
// Base SDK 초기화
Base_Initialize(initParam, onInitializeFinished);
}
});
}
/// <summary>
/// Base SDK 초기화 시도 후 작동 설정 함수
/// </summary>
protected virtual void BaseSDKInitializeFinished()
{
onInitializeFinished = (callbackResult) =>
{
PrintCallbackResult(callbackResult);
if (callbackResult.result.IsSuccessful())
{
// Base SDK 초기화 성공 시 로직을 구현해 주세요.
// ex. 이곳에서 다른 SDK 모듈의 Initialize를 진행할 수 있습니다.
isInitialized = true;
// onRenewTokenFinished 콜백 설정
// RenewTokenFinished();
// onRenewTokenFinished 콜백을 이용해 Base_AccessTokenRenewed 호출 (콜백 등록)
// Base_AccessTokenRenewed(onRenewTokenFinished);
// GameSupport_Initialize 호출
Result result = GameSupport_Initialize();
PrintResult(result);
}
else
{
Debug.LogError("Fail to initialize Base SDK");
}
};
}
/// <summary>
/// SDK 해제
/// </summary>
private void UnInitialize()
{
StopRunCallbackLoop();
Result result = default;
// 3. API를 호출하여 Base SDK를 정리합니다. 다른 SDK를 사용하였다면 Base_Uninitialize()가 먼저으로 호출되어야 합니다.
// GameSupport_UnInitialize 호출
result = GameSupport_UnInitialize();
PrintResult(result);
// Base_UnInitialize 호출
result = Base_UnInitialize();
PrintResult(result);
isInitialized = false;
}
private void StartRunCallbackLoop()
{
RunCallbackLoopAsync().Forget();
}
private void StopRunCallbackLoop()
{
isCallbackRunning = false;
}
/// <summary>
/// 콜백 루프 시작 (UniTask 기반)
/// </summary>
private async UniTaskVoid RunCallbackLoopAsync()
{
isCallbackRunning = true;
try
{
while (isCallbackRunning)
{
Base_RunCallback();
await UniTask.Delay(runCallbackInterval, cancellationToken: destroyCancellationToken);
}
}
catch (OperationCanceledException)
{
Debug.Log("RunCallbackLoop canceled");
}
}
/// <summary>
/// 결과 출력 유틸
/// </summary>
private void PrintResult(Result result)
{
sb.Clear();
sb.AppendLine("# Result");
sb.AppendLine($" - Result.IsSuccessful : {result.IsSuccessful()}");
sb.AppendLine($" - Result.sdkName : {result.sdkName}");
sb.AppendLine($" - Result.methodCode : {result.methodCode}");
sb.AppendLine($" - Result.resultCode : {result.resultCode}");
sb.AppendLine($" - Result.exceptionMessage : {result.exceptionMessage}");
Debug.Log(sb.ToString());
}
/// <summary>
/// CallbackResult 구조체 출력 메서드
/// </summary>
public void PrintCallbackResult(CallbackResult callbackResult)
{
sb.Clear();
sb.AppendLine("# CallbackResult");
sb.AppendLine($" - CallbackResult.ResultResult.IsSuccessful : {callbackResult.result.IsSuccessful()}");
sb.AppendLine($" - CallbackResult.Result.sdkName : {callbackResult.result.sdkName}");
sb.AppendLine($" - CallbackResult.Result.methodCode : {callbackResult.result.methodCode}");
sb.AppendLine($" - CallbackResult.Result.resultCode : {callbackResult.result.resultCode}");
sb.AppendLine($" - CallbackResult.Result.exceptionMessage : {callbackResult.result.exceptionMessage}");
sb.AppendLine($" - CallbackResult.message : {callbackResult.errorMessage}");
sb.AppendLine($" - CallbackResult.externalError : {callbackResult.externalError}");
Debug.Log(sb.ToString());
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_GetAccessToken 진행합니다.
public string GetAccessToken()
{
// Access token 문자열을 받기 위해 변수를 선언합니다.
string token = default;
uint strlen = 1024;
// Base_GetAccessToken 호출
Result result = Base_GetAccessToken(ref token, strlen);
PrintResult(result);
return result.IsSuccessful() ? token : null;
}
// Unity event method인 Start에서 콜백 구현체 등록
private void RenewTokenFinished()
{
// 4. 해당 콜백 구현체 변수에 할당
onRenewTokenFinished = (callbackResult, token) =>
{
PrintCallbackResult(callbackResult);
if(callbackResult.result.IsSuccessful())
{
// Access token 갱신 성공 시 로직을 구현해 주세요.
}
else
{
// Access token 갱신 실패 시 로직을 구현해 주세요.
}
};
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_GetUser 진행합니다.
public (bool, StovePCUser) GetUser()
{
// 3. 유저 정보를 받기 위해 변수를 선언합니다.
StovePCUser user = default;
// 4. Base_GetUser 호출
Result result = Base_GetUser(ref user);
PrintResult(result);
return result.IsSuccessful() ? (true, user) : (false, user);
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_GetGds 진행합니다.
public (bool, StovePCGds) GetGds()
{
// 3. 유저 정보를 받기 위해 변수를 선언합니다.
StovePCGds gds = default;
// 4. Base_GetUser 호출
Result result = Base_GetGds(ref gds);
PrintResult(result);
return result.IsSuccessful() ? (true, gds) : (false, gds);
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_GetSignin 진행합니다.
public (bool, StovePCSignin) GetSignin()
{
// 유저 정보를 받기 위해 변수를 선언합니다.
StovePCSignin signin = default;
// Base_GetSignin 호출
Result result = Base_GetSignin(ref signin);
PrintResult(result);
return result.IsSuccessful() ? (true, signin) : (false, signin);
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_SetLanguage 진행합니다.
public (bool, StoveLanguage) SetLanguage()
{
// 언어 설정을 위해 변수를 선언합니다.
// StoveLanguage 경우 개발 의도에 맞게 적절한 enum 값을 할당하세요.
StoveLanguage language = default;
// Base_GetUser 호출
Result result = Base_SetLanguage(language);
PrintResult(result);
return result.IsSuccessful() ? (true, language) : (false, language);
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_GetTraceHint 진행합니다.
public (bool, StovePCTraceHint) GetTraceHint()
{
// 추적 힌트를 받기 위해 변수를 선언합니다.
StovePCTraceHint hint = default;
// Base_GetUser 호출
Result result = Base_GetTraceHint(ref hint);
PrintResult(result);
return result.IsSuccessful() ? (true, hint) : (false, hint);
}
// Button click이나 특정 이벤트 처리 함수 내에서 SetGameProfile 진행합니다.
public bool SetGameProfile(string worldId, long characterNumber)
{
// StovePCGameProfile 객체 생성
// 구조체 필드에 적절한 worldId, characterNumber 값을 할당하세요.
StovePCGameProfile gameProfile = new StovePCGameProfile
{
worldId = worldId,
characterNumber = characterNumber,
};
// StovePCGameProfile 객체를 이용한 Base_SetGameProfile 호출
Result result = Base_SetGameProfile(gameProfile);
PrintResult(result);
return result.IsSuccessful();
}
// Button click이나 특정 이벤트 처리 함수 내에서 Base_GetVersion 진행합니다.
public (bool, string) GetVersion()
{
// version 문자열을 받기 위해 변수를 선언합니다.
string version = default;
uint strlen = 256;
// Base_GetVersion 호출
Result result = Base_GetVersion(ref version, strlen);
PrintResult(result);
return result.IsSuccessful() ? (true, version) : (false, version);
}
// Button click이나 특정 이벤트 처리 함수 내에서 스탯 정보를 조회합니다.
public void GetStat(string gameStatId)
{
// GameSupport_Stat 호출
GameSupport_Stat(gameStatId, onStatOnFinished);
}
// Button click이나 특정 이벤트 처리 함수 내에서 스탯 정보를 조회합니다.
public void SetStat(string gameStatId, int statValue)
{
// GameSupport_ModifyStat 호출
GameSupport_ModifyStat(gameStatId, statValue, onModifyStatOnFinished);
}
// Button click이나 특정 이벤트 처리 함수 내에서 스탯 정보를 조회합니다.
public void GetRank(string leaderboardId, uint pageIndex, uint pageSize, bool includeMyRank = true)
{
// StovePCRankParams 를 선언 및 값을 세팅
StovePCRankParams param;
param.leaderboardId = leaderboardId;
param.pageIndex = pageIndex; // 조회할 페이지 번호
param.pageSize = pageSize; // 조회할 순위의 개수
param.includeMyRank = includeMyRank; // 조회결과에 로그인한 사용자의 순위를 포함할지 여부
// GameSupport_Rank 호출
GameSupport_Rank(param, onRankOnFinished);
}
}
- Singleton.cs
using UnityEngine;
/// <summary>
/// Singleton class.
/// </summary>
/// <typeparam name="T">Type of the singleton.</typeparam>
public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
/// <summary>
/// The static reference to the instance.
/// </summary>
public static T Instance { get; protected set; }
/// <summary>
/// Gets whether an instance of this singleton exists.
/// </summary>
public static bool InstanceExists => Instance != null;
/// <summary>
/// Gets the instance of this singleton, and returns true if it is not null.
/// Prefer this whenever you would otherwise use InstanceExists and Instance together.
/// </summary>
public static bool TryGetInstance(out T result)
{
result = Instance;
return result != null;
}
/// <summary>
/// Awake method to associate singleton with instance.
/// </summary>
protected virtual void Awake()
{
if (Instance != null)
{
Debug.LogWarningFormat("Trying to create a second instance of {0}", typeof(T));
Destroy(gameObject);
}
else
{
Instance = (T)this;
}
}
/// <summary>
/// OnDestroy method to clear singleton association.
/// </summary>
protected virtual void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
}
}
2. Rank 연결 예시
미리 AddSetRankListener 또는 AddGetRankListener 에 이벤트 등록해두고
SetRank 또는 GetRank를 작동 시키면 StoveSDK에서 작업 처리 후 준 값이 등록해둔 이벤트로 들어옵니다.
- StoveRank.cs
using System;
using UnityEngine;
using static Stove.PCSDK.Base;
using static Stove.PCSDK.GameSupport;
public class StoveRank : Singleton<StoveRank>
{
private StovePCSDK3Manager stovePcsdk3Manager;
public event Action<CallbackResult, StovePCRank[], uint> OnGetRank;
public event Action<CallbackResult, StovePCModifyStatValue> OnSetRank;
public void AddGetRankListener(Action<CallbackResult, StovePCRank[], uint> action) => OnGetRank += action;
public void RemoveGetRankListener(Action<CallbackResult, StovePCRank[], uint> action) => OnGetRank -= action;
public void AddSetRankListener(Action<CallbackResult, StovePCModifyStatValue> action) => OnSetRank += action;
public void RemoveSetRankListener(Action<CallbackResult, StovePCModifyStatValue> action) => OnSetRank -= action;
[SerializeField] protected string rankId;
[SerializeField] protected string setRankId;
private void Start()
{
SDKInit();
}
private void SDKInit()
{
stovePcsdk3Manager = BuildSDK.Instance as StovePCSDK3Manager;
#if UNITY_EDITOR
Debug.Assert(stovePcsdk3Manager is not null, "There is not StovePCSDKManager");
#endif
stovePcsdk3Manager.onModifyStatOnFinished +=
(callbackResult, stat) =>
{
stovePcsdk3Manager.PrintCallbackResult(callbackResult);
OnSetRank?.Invoke(callbackResult, stat);
};
stovePcsdk3Manager.onRankOnFinished +=
(callbackResult, stovePcRank, totalCount) =>
{
stovePcsdk3Manager.PrintCallbackResult(callbackResult);
OnGetRank?.Invoke(callbackResult, stovePcRank, totalCount);
};
}
public void SetRank(int value)
{
stovePcsdk3Manager.SetStat(setRankId, value);
}
public void GetRank(uint pageIndex = 1, uint pageSize = 20, bool includeMyRank = true)
{
stovePcsdk3Manager!.GetRank(rankId, pageIndex, pageSize, includeMyRank);
}
}
※ 경험한 오류 및 삽질
1. OnClickStartRunCallbackLoop()실행이 Base_Initialize() 실행보다 앞에 있어야 합니다.
(STOVE develpoers에서는 OnClickStartRunCallbackLoop함수, 제 코드에서는 StartRunCallbackLoop함수 입니다.)
그렇지 않으면 Base가 제대로 작동했는지 Callback으로 오지도 않을 뿐더러 덕분에 Base_RestartAppIfNecessaryAsync의 콜백도 받지 못해 전혀 작동하지 않습니다.
2. Ranking에서 Ranking의 목록은 랭킹ID로 가져오지만 Ranking에 값을 넣는건 Ranking과 연동된 StatID에 넣어야합니다.
랭킹ID = 게임ID|StatID 이며 랭킹에 값 넣을 때는 저 StatID에 넣어야합니다.
※ 로컬PC 빌드 테스트 방법
(잘 안되면 유니티와 Stove 클라이언트도 껐다 키고 이전에 빌드 했던 파일도 지우면서 다시 해보세요)
https://developers-beta.onstove.com/ko/docs/Store/info/buildtest
스토브 스토어 개발자 가이드
developers-beta.onstove.com
PolicyConfig.json은 json형식으로만 적으셔야합니다.
링크 예시에서 PolicyConfig.json가 붙어있는건 파일이름을 나타내주는 것 일 뿐입니다.
{
"stove_launcher_policy_config":
{
"dev_game_list": [ "YourGameID", "YourGameID"],
"dev_game_exhibit_list":
[
{
"game_id": "YourGameID"
},
{
"game_id": "YourGameID"
}
]
}
}
※ 로그 디렉터리 위치 확인해서 로그를 꼭 확인해주세요. 전 매우 유용하게 사용했습니다. (추가로 player log 파일 확인도)
(아래는 STOVE developers에 나와있는 설명입니다.)
Log path
PC SDK 초기화 후, Env, GameId 정보를 통해 환경 및 게임 별로 로그가 쌓이게 되는 위치 입니다.
정상적으로 초기화가 된 경우의 로그 위치
C:\Users\{유저명}\AppData\Local\STOVEPCSDK3{Env}\logs\{GameID}
C:\Users\user12\AppData\Local\STOVEPCSDK3\logs\PCSDK3_SAMPLE_SB
초기화가 실패한 경우의 로그 위치
C:\Users\{유저명}\AppData\Local\STOVEPCSDK3Temp\logs\{GameExeName}
C:\Users\user12\AppData\Local\STOVEPCSDK3Temp\logs\stoveSample
로그 생성 시 모듈명_생성시점.log 의 형태로 저장됩니다.
로그의 경우 출력 시점과 PC SDK 3.0 API 호출 결과에 대한 결과 코드 등이 기록되어 있습니다.
'Unity' 카테고리의 다른 글
| OperationException : Failed to initialize localization, could not preload asset tables (0) | 2026.03.13 |
|---|---|
| Unity IAP Codeless 없이 수동 IAPManager 작성 (0) | 2026.03.09 |
| Unity Codeless IAP 사용 때 소비아이템 자동 구매 확정 문제 해결 (0) | 2026.03.05 |
| Unity TextMeshPro - Text에 Sprite Mask 되게 적용하기 (0) | 2026.02.06 |
| 유니티 SerializeHashSet, HashSet직렬화 (0) | 2026.02.03 |