카테고리 없음

Stove Studio SDK 연동(with. Shoot Out)

csiimnida 2025. 8. 6. 09:16

안녕하세요 크시입니다.

 

Stove Studio

Stove Store

최근에 Stove 스토어에 출시를 준비하며 Stove SDK 연동을 설명해 주는 블로그가 없다는 사실을 알았습니다.

그래서 이번에 한 번 연동할 때 했던 행동을 글로 한번 작성해 봅니다.

 

 이번에 게임은 Shoot Out이라는 게임입니다.

Shoot Out Stove Store

 

 

 

연동 전에는 게임 서버 '뒤끝'이라는 SDK를 이용하여 로그인, 회원가입, 랭킹을 구현했습니다.

이때는 출시 생각이 별로 없었습니다.

게임 서버 뒤끝

 

이때 뒤끝은 무료 한도 요금이 초과하면 그달부터 요금을 지불하는 형식입니다.

 

하지만 출시 후 또 다른 지출이 싫었고, 이번에도 Stove SDK를 연동하고 싶은 마음이 들었습니다.

또한 출시 때 뒤끝을 이용한 부분을 최대한 살리고 싶었습니다.

 

 

그래서 찾아보니

 

Stove SDK에서도 뒤끝과 똑같이 랭킹 시스템 및 다른 시스템도 지원하는 것을 알았습니다.

 

그래서 저는 서버 뒤끝을 버리고 Stove SDK를 연동합니다.

 

 

 

 

먼저 연동을 위해 Stove 회원 가입 및 Stove Studio 입점 신청 그리고 Stove 앱과 SDK를 다운 받았습니다.

(Stove Studio 입점 부분은 저번에 만들어서 기억이 안 나요 ㅠㅠ)

 

스토브 SDK 다운

스토브 클라이언트 다운

 

이후 다운로드한 SDK를 유니티 프로젝트에 압축을 풀며 x86_64 파일을 지웠습니다.

 

그리고 x86 파일 안에 있는 모든 dll 파일의 임포트 설정을 Windows X86으로 했습니다.

이렇게 하면 유니티에서의 설치는 끝났습니다.


이후 Stove앱에서 개발자 모드를 활성화해야 합니다.

 

설치 이후 Window + R 키를 누른 후  %appdata%를 작성하여 appdata 파일에 들어갑니다.

 

그리고 AppData\Local\STOVE 폴더에 들어갑니다.

 

그리고 Config 폴더 안에 PolicyConfig.json 파일을 추가 후 아래의 코드를 작성합니다.

{
  "stove_launcher_policy_config":
  {
    "dev_game_list": [ "게임 ID "]
  }
}

이때 게임 ID는 Stove Studio에서 프로젝트 상품관리 -> 상품 홈 -> 빌드관리 쪽 오른쪽 위에 있는 상품 키를 누르면 게임 Id가 표시됩니다.

 

이후 다시 유니티로 돌아와서 처음 시작하는 씬을 선택합니다.

(저는 따로 나눴습니다)


 

이후 GameObject를 생성 후 StovePCSDKManger.cs를 생성합니다.

 

Manager에서는 SDK Init과 CallBack 함수 호출 및 소유권 검사를 합니다.

 

먼저 using 선언과 변수 설정 후 SDK 초기화 코드를 작성하겠습니다.

using Stove.PCSDK.NET;

using StovePCToken = Stove.PCSDK.NET.StovePCToken;

using StovePCUser = Stove.PCSDK.NET.StovePCUser;

using StovePCRank = Stove.PCSDK.NET.StovePCRank;

using StovePCStat = Stove.PCSDK.NET.StovePCStat;

 

 

private bool isInitialized = false;


private Stove.PCSDK.NET.StovePCCallback callback;

 

StovePCConfig config = new StovePCConfig

{

   Env = "LIVE",

   AppKey = "/*todo : Application Key*/",

   AppSecret = "/*todo : Application Secret*/",

   GameId = "/*todo : 게임 ID*/",

   LogLevel = StovePCLogLevel.Error,

   LogPath = ""

};

bool owned = false;

 

 

if(isInitialized) return;

isInitialized = true;

StovePCResult sdkResult = StovePC.Initialize(config, callback);

 

if (StovePCResult.NoError == sdkResult)

{

   print("SDK Initialization NoError");

   // 초기화 오류가 없어

   StartCoroutine(RunCallback(0.5f));

 

}

else

{

   Debug.Log(sdkResult);

   // 초기화 실패로 게임 종료

   Debug.LogError("SDK Initialization Error");

   BeginQuitAppDueToError();

}

위의 코드는 Start에서 호출하였습니다.

 

그다음 CallBack 함수 호출을 위해 코루틴을 사용했습니다.

private IEnumerator RunCallback(float intervalSeconds)

{

   WaitForSeconds wfs = new WaitForSeconds(intervalSeconds);

   while (true)

   {

       StovePC.RunCallback();

       yield return wfs;

   }

}

 

그리고 콜백으로 어떤 함수를 호출해 주는지 설정해 주는 코드를 작성하겠습니다.

this.callback = new Stove.PCSDK.NET.StovePCCallback

{

   OnError = new StovePCErrorDelegate(this.OnError),

   OnInitializationComplete = new StovePCInitializationCompleteDelegate(this.OnInitializationComplete),

   OnToken = new StovePCTokenDelegate(this.OnToken),

   OnUser = new StovePCUserDelegate(this.OnUser),

   OnOwnership = new StovePCOwnershipDelegate(this.OnOwnership),

  

   // 게임지원서비스

   OnStat = new StovePCStatDelegate(this.OnStat),

   OnSetStat = new StovePCSetStatDelegate(this.OnSetStat),

   OnRank = new StovePCRankDelegate(this.OnRank)

   //OnAchievement = new StovePCAchievementDelegate(this.OnAchievement),

   //OnAllAchievement = new StovePCAllAchievementDelegate(this.OnAllAchievement),

};

초기화랑 같이 Start 함수에서 작성하면 됩니다.

 

private void OnError(StovePCError error)

{

   #region Log

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("OnError");

   sb.AppendFormat(" - error.FunctionType : {0}" + Environment.NewLine, error.FunctionType.ToString());

   sb.AppendFormat(" - error.Result : {0}" + Environment.NewLine, (int)error.Result);

   sb.AppendFormat(" - error.Message : {0}" + Environment.NewLine, error.Message);

   sb.AppendFormat(" - error.ExternalError : {0}", error.ExternalError.ToString());

   Debug.LogError(sb.ToString());

   #endregion

 

   switch (error.FunctionType)

   {

       case StovePCFunctionType.Initialize:

       case StovePCFunctionType.GetUser:

       case StovePCFunctionType.GetOwnership:

           BeginQuitAppDueToError();

           break;

   }

}

 

private void OnInitializationComplete()

{

   Debug.Log("PC SDK initialization success");

   StovePCResult result = StovePC.GetOwnership();

   if (result == StovePCResult.NoError)

   {

       Debug.Log("소유권 확인 요청");

       // 성공 처리

   }

   else

   {

       Debug.Log("소유권 확인 요청 실패");

       BeginQuitAppDueToError();

   }

}


private void OnOwnership(StovePCOwnership[] ownerships)

{

   Debug.Log("소유권 확인중");

   foreach(var ownership in ownerships)

   {

       // [LOGIN_USER_MEMBER_NO] StovePCUser 구조체의 MemberNo

       // [OwnershipCode] 1: 소유권 획득, 2: 소유권 해제(구매 취소한 경우)

       if (ownership.OwnershipCode != 1)

       {

           continue;

       }

 

       // [GameCode] 3: BASIC 게임, 4: DEMO

       if (ownership.GameId == config.GameId &&

           ownership.GameCode == 3)

       {

           owned = true; // 소유권 확인 변수 true로 설정

       }

   }

 

   if(owned)

   {

       // 소유권 검증이 정상적으로 완료 된 이후 게임진입 로직 작성

       Debug.Log("소유권있음");

       LoadingSceneManager.LoadScene("Start"); 

   }

   else

   {

       // 소유권 검증실패 후 게임을 종료하고 에러 메시지 표출 로직 작성

       Debug.Log("비소유");

       BeginQuitAppDueToError();

   }

}


private void OnToken(StovePCToken token)

{

   // 토큰 정보 출력

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("OnToken");

   sb.AppendFormat(" - token.AccessToken : {0}", token.AccessToken);

 

   Debug.Log(sb.ToString());

}

 

private void OnUser(StovePCUser user)

{

   // 사용자 정보 출력

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("OnUser");   

   sb.AppendFormat(" - user.MemberNo : {0}" + Environment.NewLine, user.MemberNo.ToString());

   sb.AppendFormat(" - user.Nickname : {0}" + Environment.NewLine, user.Nickname);

   sb.AppendFormat(" - user.GameUserId : {0}", user.GameUserId);

 

   Debug.Log(sb.ToString());

}

 

 

 

#region SupportCallBack

 

private void OnStat(StovePCStat stat)

{

   // 스탯 정보 출력

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("OnStat");

   sb.AppendFormat(" - stat.StatFullId.GameId : {0}" + Environment.NewLine, stat.StatFullId.GameId);

   sb.AppendFormat(" - stat.StatFullId.StatId : {0}" + Environment.NewLine, stat.StatFullId.StatId);

   sb.AppendFormat(" - stat.MemberNo : {0}" + Environment.NewLine, stat.MemberNo.ToString());

   sb.AppendFormat(" - stat.CurrentValue : {0}" + Environment.NewLine, stat.CurrentValue.ToString());

   sb.AppendFormat(" - stat.UpdatedAt : {0}", stat.UpdatedAt.ToString());

 

   Debug.Log(sb.ToString());

}

private void OnSetStat(StovePCStatValue statValue)

{

   // 스탯 업데이트 결과 정보 출력

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("OnSetStat");

   sb.AppendFormat(" - statValue.CurrentValue : {0}" + Environment.NewLine, statValue.CurrentValue.ToString());

   sb.AppendFormat(" - statValue.Updated : {0}" + Environment.NewLine, statValue.Updated.ToString());

   sb.AppendFormat(" - statValue.ErrorMessage : {0}", statValue.ErrorMessage);

 

   Debug.Log(sb.ToString());

}

private void OnRank(StovePCRank[] ranks, uint rankTotalCount)

{

   // 순위 정보 출력

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("OnRank");

   sb.AppendFormat(" - ranks.Length : {0}" + Environment.NewLine, ranks.Length);

   for (int i = 0; i < ranks.Length; i++)

   {

       sb.AppendFormat(" - ranks[{0}].MemberNo : {1}" + Environment.NewLine, i, ranks[i].MemberNo.ToString());

       sb.AppendFormat(" - ranks[{0}].Score : {1}" + Environment.NewLine, i, ranks[i].Score.ToString());

       sb.AppendFormat(" - ranks[{0}].Rank : {1}" + Environment.NewLine, i, ranks[i].Rank.ToString());

       sb.AppendFormat(" - ranks[{0}].Nickname : {1}" + Environment.NewLine, i, ranks[i].Nickname);

       sb.AppendFormat(" - ranks[{0}].ProfileImage : {1}" + Environment.NewLine, i, ranks[i].ProfileImage);

   }

 

   sb.AppendFormat(" - rankTotalCount : {0}", rankTotalCount);

 

   Debug.Log(sb.ToString());

  

   DataBase.Instance.Ranks = ranks;

   StoveRank.Instance.SetUI(ranks);

}

 

 

#endregion

 

private void BeginQuitAppDueToError()

{

   #region Log

   StringBuilder sb = new StringBuilder();

   sb.AppendLine("BeginQuitAppDueToError");

   sb.AppendFormat(" - nothing");

   Debug.LogError(sb.ToString());

   #endregion

 

   // 어쩌면 당신은 즉시 앱을 중단하기보다는 사용자에게 앱 중단에 대한 메시지를 보여준 후

   // 사용자 액션(e.g. 종료 버튼 클릭)에 따라 앱을 중단하고 싶어 할지도 모릅니다.

   // 그렇다면 여기에 QuitApplication을 지우고 당신만의 로직을 구현하십시오.

   // 권장하는 필수 사전 작업 오류에 대한 메시지는 아래와 같습니다.

   // 한국어 : 필수 사전 작업이 실패하여 게임을 종료합니다.

   // 그 외 언어 : The required pre-task fails and exits the game.

   Application.Quit();

}

 

위의 코드를 작성하면 SDK 초기화 및 소유권 검사 중 에러가 나오거나 게임 소유권이 없으면 게임을 나가는 시스템이 작동합니다.

 

 

 

하지만 게임의 소유권은 따로 문의하여 얻어야 합니다.

https://discord.gg/cDEcAMczZq

위의 링크는 Stove Store 지원 디스코드 초대 링크입니다.

 


 

이번엔 랭킹과 연동하겠습니다.

 

먼저 랭킹 목록을 Stove Studio에서 생성하겠습니다.

(여기서 랭킹은 기존에 있던 스탯 시스탬을 이용하여 작동됩니다)

프로젝트 상품관리 -> 상품홈 -> 랭킹 오른쪽 위에 등록 버튼이 있습니다.

 

 

등록 버튼을 누르면 이러한 화면이 나옵니다.

신규 스탯을 누르고 스탯 이름을 생성합니다.

 

이후 업데이트 방식을 수정합니다.

업데이트 방식에 관한 설명은 아래에 있습니다.

  • INCREMENT : 기존 값에 신규 값을 합산합니다.
  • REPLACE : 기존 값을 신규 값으로 대체합니다.
  • MAX : 기존 값 보다 신규 값이 큰 경우에만 신규 값으로 대체합니다.
  • MIN : 기존 값 보다 신규 값이 작은 경우에만 신규 값으로 대체합니다.

 

랭킹 정보를 스토어 페이지에 노출할 수도 있습니다.

노출된 사진

이는 등록 버튼 좌측에 있는 스토어 노출 설정 버튼을 눌러 설정할 수 있습니다.

 

 


다음은 랭킹 불러오는 기능을 만들겠습니다.

저는 새로운 cs파일에서 제작했습니다.

using Stove.PCSDK;

using Stove.PCSDK.NET;

using StovePCRank = Stove.PCSDK.NET.StovePCRank;

 

 

public void GetStoveRank()

{

   StovePCResult result = StovePC.GetRank("/*todo : 랭킹 ID*/", 1, 50, false);

   if(result == StovePCResult.NoError)

   {

       Debug.Log("Rank 불러오기 Success");

   }

}

 

GetRank에 관한 입력 파라미터입니다.

string leaderboardId : 스튜디오에서 생성한 리더보드 식별자

uint pageIndex : 조회할 페이지 번호 (1 <= pageIndex)

uint pageSize : 조회할 순위의 개수 (1 <= pageSize <= 50)

bool includeMyRank : 조회결과에 로그인한 사용자의 순위를 포함할지 여부

includeMyRank를 true로 하면 맨 첫 번째 배열에 자신의 랭킹 정보가 표기됩니다.

 

 

GetRank 함수 호출 시(성공했다면) 방금 Manager에서 만든 OnRank 함수가 CallBack됩니다.

그 함수에 관한 처리는 자신의 게임에 맞게 코드를 작성하면 됩니다.


 

이제 마지막으로 랭킹에 올리는 것을 하겠습니다.

 

랭킹은 기존의 스탯 시스템을 이용하는 것이기에 

랭킹 정보를 수정하려면 자신의 스탯을 올려야 합니다.

 

스탯을 수정하는 코드는 아래와 같습니다.

StovePCResult result = StovePC.SetStat(game_stat_id, round);

if(result == StovePCResult.NoError)

{

   // 성공 처리

   Debug.Log("Stat Inseert Success");

}

else

{

   Debug.LogError("Stat Insert Error");

}

 

랭킹 불러오는 거와 마찬가지로 성공시 Manager에서 만든 OnSetStat 함수가 CallBack됩니다

 

 

 

 

이렇게 해서 Stove Studio 가입부터 게임에 랭킹 삽입까지 알아봤습니다.

 

저는 이러한 과정이 꽤 오래 걸렸습니다.

 

하지만,이 글을 읽고 있는 여러분은 빠르게 적용하실 수 있도록 바랍니다.

 

긴 글 읽어 주셔서 감사합니다.

 

 

 

 

 

 

 

 

 

 

참고 자료 : 

https://studiodocs.onstove.com/0efca418-ae92-4408-a5e3-413ca4556ebd

https://studiodocs.onstove.com/d4bc5105-a892-4297-9af9-dceeb584bf5b

https://studiodocs.onstove.com/5ce660e5-95b9-433b-be91-017f5bc69e1e

Stove Studio

Stove Store