Unity

Unity Stove PCSDK3 연동과 Rank연동 후기

washble2 2026. 3. 8. 03:25

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 호출 결과에 대한 결과 코드 등이 기록되어 있습니다.