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

简介

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

技术概览

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

第一步 - 设定游戏的成就

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

spacewar_achievement_examplescreenshot.jpg

第二步 - 封装成就作业

以下代码独立于游戏存在,可在合适的地方添加至游戏当中。 此类代码已可正常运行,但也可轻易扩展,满足更多需求。 所有的程序代码都是直接从 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_pAchievements,以反映 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>