Steamworks 문서
단계별 지침: 통계

소개

애플리케이션에 아주 기본적인 Steam 통계를 통합하는 과정을 단계별로 간단히 보여 드리겠습니다. 코드 베이스에 10줄 미만의 코드를 10분 이내에 통합할 수 있는 방법입니다. Steamworks SDK에는 Steam의 모든 기능을 구현해 볼 수 있는 Spacewar라는 예시용 애플리케이션이 포함되어 있어, Steam 기능을 실제로 시험해 볼 수 있는 좋은 기회를 제공합니다. 이 튜토리얼은 Spacewar와 통계 및 도전 과제 API에서 찾을 수 있는 정보 중 필요한 정보만을 간추려 Steam 통계를 최대한 간단하게 활용할 수 있도록 했습니다. 통계와 도전 과제 사이에는 겹치는 내용이 상당히 많으므로, 두 개를 모두 통합할 경우 다수의 호출을 통합해야 한다는 점에 유의해 주세요.

1단계 - 게임 통계 정의하기

통계는 애플리케이션에 따라 다르며, Steamworks 앱 관리자 백앤드의 통계 구성 페이지에서 설정할 수 있습니다. 다음은 Steamworks 샘플 앱인 Spacewar의 통계 목록입니다.
stats_spacewar.png

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 번호가 있는지 확인해 주세요.