Steamworks 文献库
逐步指导:成就

简介

本文是一份快速指南,逐步指导您在 10 分钟内,对您的代码库添加 10 行以内的代码,将非常基础的 Steam 成就整合到您的应用程序中。 Steamworks SDK 中有一个名为 Spacewar 的应用程序实例,完整展示了所有 Steam 功能。若要全面了解 Steam 的所有功能,这便会是您的第一站。 本教程将 Spacewar 和统计与成就 API 中的信息简化至 Steam 统计最基本必要的数据,让教程尽量简单明了。 请注意,统计和成就之间有很多重叠的部分,所以如果您要两者一起整合,请注意很多调用都可以合并使用。

第一步 - 设定游戏的成就

成就因各应用程序而异,需要在后端的 Steamworks 应用程序管理的成就配置页面进行设置。 以下是 Steamworks 示例应用程序 Spacewar 的成就列表:
spacewar_achievements.png

第二步 - 封装成就

以下代码独立于游戏之外,您可将其放入游戏中合适的地方。 此类目前可正常运行,但如有更多需求,也可轻易扩展。 所有的程序代码都是直接从 Spacewar 实例文件 StatsAndAchievements.cpp/h 中获取的。

头文件

首先我们需要定义一个结构,以接收来自于 Steam 的成就数据,并创建该类的对象提供一个宏。 这些数据可直接与成就配置页面上的字段相对应。
#define _ACH_ID( id, name ) { id, #id, name, "", 0, 0 } struct Achievement_t { int m_eAchievementID; const char *m_pchAchievementID; char m_rgchName[128]; char m_rgchDescription[256]; bool m_bAchieved; int m_iIconImage; };

接下来,定义一个帮助程序类,用来包装所有 Steam 统计 API 的调用和建立所有 Steam 回调
class CSteamAchievements { private: int64 m_iAppID; // 我们当前的 AppID。 Achievement_t *m_pAchievements; // 成就数据。 int m_iNumAchievements; // 成就数量。 bool m_bInitialized; //我们是否已调用 RequestStats,并收到回调? public: CSteamAchievements(Achievement_t *Achievements, int NumAchievements); ~CSteamAchievements(); bool RequestStats(); bool SetAchievement(const char* ID); STEAM_CALLBACK( CSteamAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived ); STEAM_CALLBACK( CSteamAchievements, OnUserStatsStored, UserStatsStored_t, m_CallbackUserStatsStored ); STEAM_CALLBACK( CSteamAchievements, OnAchievementStored, UserAchievementStored_t, m_CallbackAchievementStored ); };

代码文件

构造函数

参数 - 构造函数使用一个指针,指向成就数组和数组的长度。 后文中将与主要游戏代码一起对数组的格式进行说明。
返回 – 不适用
作用 - 此构造函数初始化了几个成员函数,并抓取了目前运行的 AppID, 另外还挂钩了处理对 Steam 的异步调用的回调方法。 最后还对 RequestStats() 进行首次调用,以获取当前用户的统计与成就。
CSteamAchievements::CSteamAchievements(Achievement_t *Achievements, int NumAchievements): m_iAppID( 0 ), m_bInitialized( false ), m_CallbackUserStatsReceived( this, &CSteamAchievements::OnUserStatsReceived ), m_CallbackUserStatsStored( this, &CSteamAchievements::OnUserStatsStored ), m_CallbackAchievementStored( this, &CSteamAchievements::OnAchievementStored ) { m_iAppID = SteamUtils()->GetAppID(); m_pAchievements = Achievements; m_iNumAchievements = NumAchievements; RequestStats(); }

RequestStats()

参数 - 无
返回 - 代表调用是否成功的 Bool。 如果调用失败,很可能是因为 Steam 尚未初始化。 进行此调用时请确定 Steam 客户端已开启,而且已调用了 SteamAPI_Init
作用 - 此方法基本上只是将对 ISteamUserStats::RequestCurrentStats
的调用包装了起来。该调用为异步调用,用来向 Steam 请求当前用户的统计与成就。 您必须先进行此调用,才能设置统计或成就。 在构造函数内对此方法进行首次调用。 日后若需检查更新后的统计与成就,可随时调用此方法。
bool CSteamAchievements::RequestStats() { // 是否已加载 Steam? 若否,则我们无法获取统计。 if ( NULL == SteamUserStats() || NULL == SteamUser() ) { return false; } // 用户是否已登录? 若否,则我们无法获取统计。 if ( !SteamUser()->BLoggedOn() ) { return false; } // 请求用户统计数据。 return SteamUserStats()->RequestCurrentStats(); }

SetAchievement()

参数 - 您要设置的成就字符串标识符(如 Ach_WIN_ONE_GAME )。
返回 - 代表调用是否成功的 Bool 。 如果调用失败,很可能是因为 Steam 尚未初始化,或是您尚未处理初始调用RequestStats 的回调。 您在收到回调之前无法设置任何成就。
作用 - 此方法将给定成就设置为已达成,并将结果发至 Steam。 您可重复设定同一项给定成就,便不用担心只能设定尚未达成的成就。 此调用为异步调用,会触发两个回调:OnUserStatsStored()OnAchievementStored()
bool CSteamAchievements::SetAchievement(const char* ID) { // 是否已收到来自 Steam 的回调? if (m_bInitialized) { SteamUserStats()->SetAchievement(ID); return SteamUserStats()->StoreStats(); } // 若否,则无法设置成就。 return false; }

OnUserStatsReceived()

参数 – 不适用
返回 - 无
作用 - 每次尝试请求统计时使用的回调方法。 使用 RequestStats() 请求统计与成就数据。 此方法将更新成员变量 m_Achievements 以反映 Steam 返回的统计与成就的最新数据。
void CSteamAchievements::OnUserStatsReceived( UserStatsReceived_t *pCallback ) { // 我们也许会收到其他游戏的统计回调,请忽略。 if ( m_iAppID == pCallback->m_nGameID ) { if ( k_EResultOK == pCallback->m_eResult ) { OutputDebugString("Received stats and achievements from Steam\n"); m_bInitialized = true; // 载入成就。 for ( int iAch = 0; iAch < m_iNumAchievements; ++iAch ) { Achievement_t &ach = m_pAchievements[iAch]; SteamUserStats()->GetAchievement(ach.m_pchAchievementID, &ach.m_bAchieved); _snprintf( ach.m_rgchName, sizeof(ach.m_rgchName), "%s", SteamUserStats()->GetAchievementDisplayAttribute(ach.m_pchAchievementID, "name")); _snprintf( ach.m_rgchDescription, sizeof(ach.m_rgchDescription), "%s", SteamUserStats()->GetAchievementDisplayAttribute(ach.m_pchAchievementID, "desc")); } } else { char buffer[128]; _snprintf( buffer, 128, "RequestStats - failed, %d\n", pCallback->m_eResult ); OutputDebugString( buffer ); } } }

OnUserStatsStored()

参数 – 不适用
返回 - 无
作用 - 每次尝试在 Steam 上存储统计时使用的回调方法。
void CSteamAchievements::OnUserStatsStored( UserStatsStored_t *pCallback ) { // 我们也许会收到其他游戏的统计回调,请忽略。 if ( m_iAppID == pCallback->m_nGameID ) { if ( k_EResultOK == pCallback->m_eResult ) { OutputDebugString( "Stored stats for Steam\n" ); } else { char buffer[128]; _snprintf( buffer, 128, "StatsStored - failed, %d\n", pCallback->m_eResult ); OutputDebugString( buffer ); } } }

OnAchievementStored()

参数 – 不适用
返回 - 无
作用 - 每次在 Steam 上成功储存了成就时使用的回调方法。
void CSteamAchievements::OnAchievementStored( UserAchievementStored_t *pCallback ) { // 我们也许会收到其他游戏的统计回调,请忽略。 if ( m_iAppID == pCallback->m_nGameID ) { OutputDebugString( "Stored Achievement for Steam\n" ); } }

第三步 - 与游戏整合

以下是您需要整合入游戏中适当位置的代码段的完整列表。

定义和全局

以下包含了建立成就需要的数据、游戏特定成就的枚举,和一个指向帮助程序对象的指针。 请注意须与 Steamworks 管理页面上的成就相符。
... #include "steam_api.h" // 定义我们的成就。 enum EAchievements { ACH_WIN_ONE_GAME = 0, ACH_WIN_100_GAMES = 1, ACH_TRAVEL_FAR_ACCUM = 2, ACH_TRAVEL_FAR_SINGLE = 3, }; // 包含成就及其状态的相关数据的成就数组。 Achievement_t g_Achievements[] = { _ACH_ID( ACH_WIN_ONE_GAME, "Winner" ), _ACH_ID( ACH_WIN_100_GAMES, "Champion" ), _ACH_ID( ACH_TRAVEL_FAR_ACCUM, "Interstellar" ), _ACH_ID( ACH_TRAVEL_FAR_SINGLE, "Orbiter" ), }; // 成就对象的全局访问。 CSteamAchievements* g_SteamAchievements = NULL; ...

初始化

调用 SteamAPI_Init 初始化 Steam,并且必须在其他调用之前进行。 如果成功,便可传入成就数组和数组大小来建立帮助程序对象。
... // 初始化 Steam。 bool bRet = SteamAPI_Init(); // 如 Steam 已成功初始化,创建 SteamAchievements 对象。 if (bRet) { g_SteamAchievements = new CSteamAchievements(g_Achievements, 4); } ...

回调处理

为了确保处理到所有 Steam 回调,我们需要经常抽取新的信息。 将此调用加入游戏循环即可达到效果。
... SteamAPI_RunCallbacks(); ...

触发成就

触发成就非常简单,如同传入一个成就标识符的调用一样简单。
... if (g_SteamAchievements) g_SteamAchievements->SetAchievement("ACH_WIN_100_GAMES"); ...

关闭

您的代码中也许已经有对 SteamAPI_Shutdown 的调用了。 此调用将关闭 Steam,且必须在应用程序退出之前调用。 最后我们需要删除先前建立的帮助程序对象。
... // 关闭 Steam。 SteamAPI_Shutdown(); // 删除 SteamAchievements 对象。 if (g_SteamAchievements) delete g_SteamAchievements; ...

第四步 - 测试和疑难解答

此示例代码将把调试信息输出至调试控制台,帮助您了解哪些调用成功或失败了。 以下是一些常见的错误信息和修正方法:

This application has failed to start because steam_api.dll was not found. Re-installing the application may fix this problem.
确认 steam_api.dll 和可执行文件是否在同一个目录。

[S_API FAIL] SteamAPI_Init() failed; unable to locate a running instance of Steam, or a local steamclient.dll
很可能是因为您的 Steam 客户端没有在运行。 开启并登录 Steam。

[S_API FAIL] SteamAPI_Init() failed; no appID found.
很可能是因为您的 steam_appid.txt 文件不在正确的位置上。 将它放入源文件夹,并确认里面有您的 appID。