Steamworks 文献库
逐步指导:成就
简要说明
使用成就来奖励到达特定里程碑的玩家,或以特定方式和您的游戏互动的玩家。
整合水平
10 分钟且代码少于 10 行。 需要 Steamworks SDK 整合。

简介

成就可以用来鼓励并奖励您玩家在您游戏中的互动和取得的里程碑。 成就通常用来记录您游戏中的击杀数、里程数、开箱数或其他常见行为。 同时也可以用来帮助玩家发现游玩您游戏的不同方式。 解锁后,这些成就将会在玩家窗口的角落弹出,并会在该玩家的成就页面上标示。

技术概览

本文是一份快速指南,逐步指导您在 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; ...

第四步:测试和故障排除


若要在不为您的游戏添加代码的情况下设置成就或清除统计,您可以使用 Steam 客户端控制台。 使用 "steam.exe -console" 运行,然后:
  • achievement_clear <appid> <achievement name>
  • reset_all_stats <appid>