소개
애플리케이션에 아주 기본적인 Steam 통계를 통합하는 과정을 단계별로 간단히 보여 드리겠습니다. 코드 베이스에 10줄 미만의 코드를 10분 이내에 통합할 수 있는 방법입니다.
Steamworks SDK에는 Steam의 모든 기능을 구현해 볼 수 있는
Spacewar라는 예시용 애플리케이션이 포함되어 있어, Steam 기능을 실제로 시험해 볼 수 있는 좋은 기회를 제공합니다. 이 튜토리얼은 Spacewar와 통계 및 도전 과제 API에서 찾을 수 있는 정보 중 필요한 정보만을 간추려 Steam 통계를 최대한 간단하게 활용할 수 있도록 했습니다. 통계와 도전 과제 사이에는 겹치는 내용이 상당히 많으므로, 두 개를 모두 통합할 경우 다수의 호출을 통합해야 한다는 점에 유의해 주세요.
1단계 - 게임 통계 정의하기
통계는 애플리케이션에 따라 다르며, Steamworks 앱 관리자 백앤드의
통계 구성 페이지에서 설정할 수 있습니다. 다음은 Steamworks 샘플 앱인 Spacewar의 통계 목록입니다.
2단계 - 통계 캡슐화 작업
다음 코드는 게임과 별도로 존재하며 필요에 따라 게임에 추가할 수 있습니다. 클래스는 그 자체로도 작동하지만, 필요에 따라 손쉽게 확장할 수 있습니다. 모든 코드는 Spacewar 예제 파일
StatsAndAchievements.cpp/h
에서 직접 가져온 것입니다.
헤더 파일
먼저 Steam에서 받은 통계 데이터를 보존할 구조, 통계 형식을 간편한 열거로 정의한 다음 통계 개체의 생성을 위한 매크로를 제공합니다. 이 데이터는
통계 구성 페이지의 내용을 바로 매핑합니다.
#define _STAT_ID( id,type,name ) { id, type, name, 0, 0, 0, 0 }
enum EStatTypes
{
STAT_INT = 0,
STAT_FLOAT = 1,
STAT_AVGRATE = 2,
};
struct Stat_t
{
int m_ID;
EStatTypes m_eStatType;
const char *m_pchStatName;
int m_iValue;
float m_flValue;
float m_flAvgNumerator;
float m_flAvgDenominator;
};
다음으로 Steam 통계 API 호출을 모두 래핑하고
Steam 콜백을 모두 생성할 헬퍼 클래스를 정의합니다.
class CSteamStats
{
private:
int64 m_iAppID; // 현재 AppID
Stat_t *m_pStats; // 통계 데이터
int m_iNumStats; // 통계 개수
bool m_bInitialized; // 요청 통계를 호출하고 콜백을 수신했나요?
public:
CSteamStats(Stat_t *Stats, int NumStats);
~CSteamStats();
bool RequestStats();
bool StoreStats();
STEAM_CALLBACK( CSteamStats, OnUserStatsReceived, UserStatsReceived_t,
m_CallbackUserStatsReceived );
STEAM_CALLBACK( CSteamStats, OnUserStatsStored, UserStatsStored_t,
m_CallbackUserStatsStored );
};
코드 파일
생성자
매개변수 - 생성자는 통계 배열과 배열 길이를 가리키는 포인터를 받습니다. 해당 배열의 서식에 대해서는 추후에 주요 게임 코드에서 다루겠습니다.
반환값 - 해당 없음
기능 - 생성자는 일련의 멤버를 초기화하고 현재 실행 중인 앱 ID를 가져옵니다. 또한, Steam에 대한 비동기 호출을 처리하는 콜백 메서드를 연결합니다. 마지막으로 현재 사용자의 통계와 도전 과제를 가져오기 위해
RequestStats()
를 최초로 호출합니다.
CSteamStats::CSteamStats(Stat_t *Stats, int NumStats) :
m_iAppID( 0 ),
m_bInitialized( false ),
m_CallbackUserStatsReceived( this, &CSteamStats::OnUserStatsReceived ),
m_CallbackUserStatsStored( this, &CSteamStats::OnUserStatsStored )
{
m_iAppID = SteamUtils()->GetAppID();
m_pStats = Stats;
m_iNumStats = NumStats;
RequestStats();
}
RequestStats()
매개변수 - 없음
반환값 - 호출의 성공 여부를 나타내는 부울 값. 호출이 실패했다면 Steam이 초기화되지 않았기 때문일 가능성이 높습니다. 이 호출을 시도할 때는 Steam 클라이언트가 열려 있고
SteamAPI_Init가 먼저 호출되었는지 반드시 확인해 주세요.
기능 - 이 메서드는 기본적으로 현재 사용자의 통계를 Steam에 요청하는 비동기 호출인
ISteamUserStats::RequestCurrentStats에 호출을 랩핑합니다. 이 호출을 완료해야 통계 또는 도전 과제를 설정할 수 있습니다. 이 메서드에 대한 최초 호출은 생성자에서 이루어집니다. 이후 업데이트된 통계 또는 도전 과제 상태를 확인하고자 하는 경우 언제든지 다시 호출할 수 있습니다.
bool CSteamStats::RequestStats()
{
// Steam이 실행 중인가요? 그렇지 않은 경우 통계를 얻을 수 없습니다.
if ( NULL == SteamUserStats() || NULL == SteamUser() )
{
return false;
}
// 사용자가 로그인했나요? 그렇지 않은 경우 통계를 얻을 수 없습니다.
if ( !SteamUser()->BLoggedOn() )
{
return false;
}
// 사용자 통계를 요청합니다.
return SteamUserStats()->RequestCurrentStats();
}
StoreStats()
매개변수 - 없음
반환값 - 호출의 성공 여부를 나타내는 부울 값. 호출이 실패했다면 Steam이 초기화되지 않았기 때문일 가능성이 높습니다. 이 호출을 시도할 때는 Steam 클라이언트가 열려 있고
SteamAPI_Init가 먼저 호출되었는지 반드시 확인해 주세요.
기능 - 이 메서드는 기본적으로 서버에 있는 현재 사용자의 통계를 Steam에 요청하는 비동기 호출인
ISteamUserStats::StoreStats에 호출을 랩핑합니다. 이 호출은 사용자의 통계를 업데이트하고자 할 때마다 이루어져야 합니다.
bool CSteamStats::StoreStats()
{
if ( m_bInitialized )
{
// 통계를 불러오세요.
for ( int iStat = 0; iStat < m_NumStats; ++iStat )
{
Stat_t &stat = m_pStats[iStat];
switch (stat.m_eStatType)
{
case STAT_INT:
SteamUserStats()->SetStat( stat.m_pchStatName, stat.m_iValue );
break;
case STAT_FLOAT:
SteamUserStats()->SetStat( stat.m_pchStatName, stat.m_flValue );
break;
case STAT_AVGRATE:
SteamUserStats()->UpdateAvgRateStat(stat.m_pchStatName, stat.m_flAvgNumerator, stat.m_flAvgDenominator );
// 평균 결과가 계산됩니다.
SteamUserStats()->GetStat(stat.m_pchStatName, &stat.m_flValue );
break;
default:
break;
}
}
return SteamUserStats()->StoreStats();
}
}
OnUserStatsReceived()
매개변수 - 해당 없음
반환값 - 없음
기능 - 이 메서드는 통계 요청을 시도할 때마다 호출되는 콜백입니다. 통계는
RequestStats()
를 사용하여 호출합니다. 해당 메서드는 Steam에서 반환된 최신 통계를 반영하는 m_Stats 멤버 변수를 업데이트합니다.
void CSteamStats::OnUserStatsReceived( UserStatsReceived_t *pCallback )
{
// 다른 게임 통계의 콜백을 수신할 경우 무시하세요.
if ( m_iAppID == pCallback->m_nGameID )
{
if ( k_EResultOK == pCallback->m_eResult )
{
OutputDebugString( "Received stats and achievements from Steam\n" );
// 통계를 불러오세요.
for ( int iStat = 0; iStat < m_iNumStats; ++iStat )
{
Stat_t &stat = m_Stats[iStat];
switch (stat.m_eStatType)
{
case STAT_INT:
SteamUserStats()->GetStat(stat.m_pchStatName, &stat.m_iValue);
break;
case STAT_FLOAT:
case STAT_AVGRATE:
SteamUserStats()->GetStat(stat.m_pchStatName, &stat.m_flValue);
break;
default:
break;
}
}
m_bInitialized = true;
}
else
{
char buffer[128];
_snprintf( buffer, 128, "RequestStats - failed, %d\n", pCallback->m_eResult );
OutputDebugString( buffer );
}
}
}
OnUserStatsStored()
매개변수 - 해당 없음
반환값 - 없음
기능 - 이 메서드는 Steam에 통계 저장을 시도할 때마다 호출되는 콜백입니다. 설정하고자 하는 통계 중에 제약 조건을 위반하는 통계가 있으면 이전 통계 값으로 돌아가서 해당 값을 다시 불러와야 하는 상황이 발생합니다.
void CSteamStats::OnUserStatsStored( UserStatsStored_t *pCallback )
{
// 다른 게임 통계의 콜백을 수신할 경우 무시하세요.
if ( m_iAppID == pCallback->m_nGameID )
{
if ( k_EResultOK == pCallback->m_eResult )
{
OutputDebugString( "StoreStats - success\n" );
}
else if ( k_EResultInvalidParam == pCallback->m_eResult )
{
// 한 개 이상의 통계가 제약 조건을 위반했습니다. 이전 통계 값으로 되돌아갔으며,
// 동기화 유지를 위해 값을 다시 반복해야 합니다.
OutputDebugString( "StoreStats - some failed to validate\n" );
// 여기에 콜백을 만들어 값을 다시 로드하세요.
UserStatsReceived_t callback;
callback.m_eResult = k_EResultOK;
callback.m_nGameID = m_iAppID;
OnUserStatsReceived( &callback );
}
else
{
char buffer[128];
_snprintf( buffer, 128, "StoreStats - failed, %d\n", pCallback->m_eResult );
OutputDebugString( buffer );
}
}
}
3단계 - 게임에 통합하기
다음은 적절한 위치에서 게임에 통합해야 하는 코드 조각의 전체 목록입니다.
정의 및 전역
다음은 통계를 빌드하는 데 필요한 항목, 게임별 통계의 배열, 헬퍼 개체에 대한 전역 포인터를 포함한 목록입니다. 도전 과제는 Steamworks 관리자 페이지의 통계와 일치해야 한다는 점에 유의해 주세요.
...
#include "steam_api.h"
#include "isteamuserstats.h"
#include "SteamStats.h"
// 통계와 통계의 상태에 대한 데이터를 가지고 있는 통계 모음
Stat_t g_Stats[] =
{
_STAT_ID( 1, STAT_INT, "NumGames"),
_STAT_ID( 2, STAT_INT, "NumWins"),
_STAT_ID( 3, STAT_INT, "NumLosses"),
_STAT_ID( 4, STAT_FLOAT, "FeetTraveled"),
_STAT_ID( 5, STAT_AVGRATE, "AverageSpeed"),
_STAT_ID( 7, STAT_FLOAT, "MaxFeetTraveled"),
};
// 통계 개체의 전역 액세스
CSteamStats* g_SteamStats = NULL;
...
초기화
SteamAPI_Init을 호출하면 Steam 전체가 초기화되므로 가장 먼저 호출해야 합니다. 해당 호출이 성공적일 경우 통계 배열과 배열의 크기를 전달해 헬퍼 개체를 생성합니다.
...
// Steam 초기화
bool bRet = SteamAPI_Init();
// Steam이 성공적으로 초기화되었을 경우, Steam 통계 개체 생성
if (bRet)
{
g_SteamStats = new CSteamStats(g_Stats, 6);
}
...
콜백 처리하기
모든 Steam 콜백을 처리하려면 신규 메시지를 규칙적으로 확인해야 합니다. 그렇게 하려면 게임 루프에 다음 호출을 추가하면 됩니다.
...
SteamAPI_RunCallbacks();
...
통계 저장하기
StoreStats()
를 한번 호출하면 통계가 저장됩니다.
...
if (g_SteamStats)
g_SteamStats->StoreStats();
...
종료
SteamAPI_Shutdown 호출은 이미 코드 안에 있을 것입니다. 이 호출은 Steam을 종료하는 것으로, 애플리케이션에서 나가기 전에 호출해야 합니다. 마지막으로, 생성한 모든 헬퍼 개체를 삭제합니다.
...
// Steam 종료
SteamAPI_Shutdown();
// SteamStats 객체 삭제
if (g_SteamStats)
delete g_SteamStats;
...
Step 4 - 테스트 및 문제 해결
이 샘플 코드는 디버그 콘솔에 디버그 정보를 출력하여, 어느 호출이 성공하고 실패하는지 파악하도록 해줍니다. 다음은 일반적인 오류 메시지와 해결 방법입니다.
Steam_api.dll을 찾을 수 없어 애플리케이션을 시작할 수 없습니다. 애플리케이션을 재설치하면 문제가 해결될 수 있습니다.Steam_api.dll이 실행 파일과 같은 디렉터리에 있는지 확인해 주세요.
[S_API FAIL] SteamAPI_Init() 호출에 실패했습니다. 실행 중인 Steam 또는 로컬 steamclient.dll을 찾을 수 없습니다.Steam 클라이언트가 실행 중이 아닐 가능성이 높습니다. Steam을 시작하고 로그인해 주세요.
[S_API FAIL] SteamAPI_Init() 호출에 실패했습니다. appID를 찾을 수 없습니다.Steam_appid.txt 파일이 제자리에 없을 가능성이 높습니다. Steam_appid.txt 파일을 소스 폴더에 넣고 파일에 appID 번호가 있는지 확인해 주세요.