> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stats 与成就

> 将 Steam 的成就与 stats 移植到 XBOX GDK 作品管理型 API,在游戏中跟踪进度并将 check-in 同步到 XBOX 服务。

在 XBOX Game Development Kit(GDK)与 Steam 中,成就都包含名称、说明、图标、以及是否已为当前用户解锁,并可选地包含完成成就的进度。Steam 还提供了 stats API,游戏可用它将用户进度与预设目标比较,并在超过目标时解锁成就。

事实上,XBOX Game Development Kit(GDK)提供了两套成就 API:*基于事件*(先前称为 *Achievements 2013*)与 *作品管理型*(先前称为 *Achievements 2017*)。两种系统都是可行选择,但如果你从 Steam 迁移过来,作品管理型成就/stats 会更熟悉,也更容易与游戏中已有的逻辑对齐。因此,本主题聚焦介绍该 API。有关两种 API 差异的更多信息,参见 [基于事件与作品管理型成就](/services/xbox-services/player-data/achievements/index)。以下各节介绍 Steamworks API 与 XBOX Game Development Kit(GDK)在成就方面的差异。

要使用作品管理型成就 API,你需要创建一个 XBOX 服务上下文句柄。这可以通过 [XblContextCreateHandle](/reference/live/xsapi-c/xbox_live_context_c/xbox_live_context_c_members) 函数,并配合 [XUserAddResult API](/build/core-features/common/user/xuser_howto_best_practice_signing_in)(参见 [用户身份验证与所有权](/build/steam-porting-guide/features/user-authentication-and-ownership) 主题)中的 `XUserHandle` 完成。

## 跟踪成就进度

在 Steam API 中,你可以使用 `ISteamUserStats::GetStat`/`ISteamUser::SetStat` API 跟踪用户朝某个成就的进度,并让 Steam Stats API 中存储的值成为当前进度值的真源。

而在作品管理型成就中,你的游戏是唯一真源,必须自行跟踪用户当前的进度。通过 API 调用在云端更新当前进度更像是一次“check in”,用于在 XBOX 生态的多个位置显示用户进度。关于当前进度值应存储在哪里,没有固定指导:许多游戏选择将其存储在存档文件中(也保存在云端),另一些则可能存储在自己的后端服务上。请务必让云端与游戏内部统计管理系统之间的值保持同步。

## 枚举所有成就

要使用 Steamworks API 获取用户的所有 stats,你通常会调用 `ISteamUserStats::RequestUserStats` 从服务器拉取最新 stats。相应的回调触发后,你可以按 API 名称遍历每个成就,使用 `ISteamUserStats::GetStat`/`ISteamUserStats::GetAchievement` 函数,并用返回数据初始化游戏状态。

在 XBOX Game Development Kit(GDK)中,你可以直接使用 [XblAchievementsGetAchievementsForTitleIdAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members),然后使用 [XblAchievementsResultGetAchievements](/reference/live/xsapi-c/achievements_c/achievements_c_members) 的 Out 参数中数组内每个成就对象的数据初始化游戏状态。每个成就以 [XblAchievement](/reference/live/xsapi-c/achievements_c/achievements_c_members) 结构体提供。有关其数据的更多信息,参见 [其参考主题](/reference/live/xsapi-c/achievements_c/achievements_c_members)。

### Steamworks

```cpp theme={null}
void MyGameClass::Initialize()
{
    // ...
    SteamUserStats()->RequestCurrentStats();
    STEAM_CALLBACK( MyGameClass, OnUserStatsReceived, UserStatsReceived_t );
}

void MyGameClass::OnUserStatsReceived()
{
    // Iterate over stats/achievements, and initialize any necessary game data.
    // Assume that m_achievementIds is an array of "API names" for achievements.
    for (int i = 0; i < m_achievementIds.length; i++)
    {
        const char *achievementName = SteamUserStats()->GetAchievementDisplayAttribute(m_achievementIds[i], "name");
        const char *description = SteamUserStats()->GetAchievementDisplayAttribute(m_achievementIds[i], "desc");
        bool hidden = SteamUserStats()->GetAchievementDisplayAttribute(m_achievementIds[i], "hidden");
        bool isUnlocked;
        SteamUserStats()->GetAchievement(m_achievementIds[i], &isUnlocked);
        // Do any necessary achievement initialization...
    }
}
```

### XBOX Game Development Kit(GDK)

```cpp theme={null}
auto asyncBlock = std::make_unique<XAsyncBlock>();
asyncBlock->context = nullptr;
asyncBlock->callback = [](XAsyncBlock* asyncBlock)
{
    // Take over ownership of the XAsyncBlock*
    std::unique_ptr<XAsyncBlock> asyncBlockPtr{ asyncBlock }; 

    XblAchievementsResultHandle resultHandle;
    auto hr = XblAchievementsGetAchievementsForTitleIdResult(asyncBlock, &resultHandle);

    if (SUCCEEDED(hr))
    {
        const XblAchievement* achievements = nullptr;
        size_t achievementsCount = 0;
        hr = XblAchievementsResultGetAchievements(resultHandle, &achievements, &achievementsCount);

        for (size_t i = 0; i < achievements.length; i++)
        {
            auto name = achievement[i].name;
            char description[256];
            char progress[4] = "100";
            if (achievement[i].progressState == XblAchievementProgressState::Achieved)
            {
                strcpy(description, achievement[i].unlockedDescription);
            }
            else
            {
                strcpy(description, achievement[i].lockedDescription);
                strcpy(progress, achievement[i].progression.requirements[0].currentProgressValue);
            }
            // Use this data to initialize achievements...
        }

        // When you're done with the handle, close it. This will free the achievements list 
        // from memory.
        XblAchievementsResultCloseHandle(resultHandle);
        achievements = nullptr; 
        // Instead, you couldn't close the handle and store it.  
        // If you needed to copy the handle, call XblAchievementsResultDuplicateHandle()
    }
};

// Assume m_xblContext is an XBOX services Context handle that has been initialized properly.
uint64_t xuid;
XblContextGetXboxUserId(m_xblContext, &xuid);

HRESULT hr = XblAchievementsGetAchievementsForTitleIdAsync(
    m_xblContext,
    xuid, 
    m_titleId, // Assume this is a uint32_t holding your game's title ID.
    XblAchievementType::Persistent, // achievementType: You probably want to use XblAchievementType::Persistent.
    false, //unlockedOnly: If true, returns only unlocked achievements.
    XblAchievementOrderBy::DefaultOrder, // orderBy: How to order the incoming achievements list. 
    0, // skipItems: The number of achievements to skip. 
    0, // maxItems: Set to 0 to get all achievements/disable pagination.
    asyncBlock.get()
);
if (SUCCEEDED(hr))
{
    // The call succeeded, so release the std::unique_ptr ownership of XAsyncBlock* since the callback will take over ownership.
    // If the call fails, the std::unique_ptr will keep ownership and delete the XAsyncBlock*
    asyncBlock.release();
}
```

有关作品管理型成就的更多信息,参见 [获取作品管理型成就](/services/xbox-services/player-data/achievements/index)。

## 更新成就进度与解锁成就

在 XBOX Game Development Kit(GDK)与 Steam 中,由游戏代码决定何时解锁成就。在 XBOX Game Development Kit(GDK)中,将成就的进度设为 100 即会解锁它。以下各节说明使用两种 API 更新用户指定成就进度所需的步骤。

以下是在两种 SDK 中实现的 `UpdateAchievement` 方法代码示例,它接收成就 ID / API 名称(Steam 中还需 stat API 名称)、成就要增加的量、解锁该成就的目标值,并相应地更新进度与解锁状态。

### Steamworks

```cpp theme={null}
void MyGame::UpdateAchievement(const char *achievementApiName, const char *statApiName, int diff, int target)
{
    bool isCompleted;
    int value;
    bool result = SteamUserStats()->GetAchievement(achievementApiName, &isCompleted);
    if (!result) {
        // Handle error.
    }

    result = SteamUserStats()->GetStat(statApiName, &value);
    if (!result) {
        // Handle error.
    }

    SteamUserStats()->SetStat(statApiName, value + diff);
    
    if (!isCompleted && value + diff >= target)
    {
             // Unlock the achievement.
        SteamUserStats()->SetAchievement(achievementApiName);
        SteamUserStats()->StoreStats();
    }
}

// At the next checkpoint...
if(!SteamUserStats()->StoreStats()) {
    // Handle error.
}
```

### XBOX Game Development Kit(GDK)

```cpp theme={null}
void MyGame::UpdateAchievement(const std::string& achievementId, int diff, int target)
{
    // Assume this function returns the specified achievement's progress from wherever you're storing it.
    auto previousValue = MyGetAchievementProgress(achievementId);
    // If you're storing progress elsewhere, make sure to update it there, too! Again, we're assuming 
    // that this is a function you've written to update the progress value wherever it's stored.
    MySetAchievementProgress(previousValue + diff);
    // Calculate the percent of the target that the user has reached after adding the value of diff.
    uint32_t percentComplete = (previousValue + diff) / target;

    auto async = std::make_unique<XAsyncBlock>();
    async->context = nullptr;
    async->callback = [](XAsyncBlock *async)
    {
        // Take over ownership of the XAsyncBlock*
        std::unique_ptr<XAsyncBlock> asyncBlockPtr{ async }; 

        HRESULT result = XAsyncGetStatus(async, true);

        if (SUCCEEDED(result))
        {
            // Success!
        }
        else
        {
            if (result == HTTP_E_STATUS_NOT_MODIFIED)
            {
                // The achievement was already completed or it was previously
                // set to a higher value, so it's unchanged.
            }
            else
            {
                // Handle error.
            }
        }
    };

    // Assume m_xblContext is an XBOX services Context handle that has been initialized properly.
    uint64_t xuid;
    XblContextGetXboxUserId(m_xblContext, &xuid);
    HRESULT hr = XblAchievementsUpdateAchievementAsync(
        m_xblContext,
        xuid,
        achievementId.c_str(),
        percentComplete,
        async
    );

    if (SUCCEEDED(hr))
    {
        // The call succeeded, so release the std::unique_ptr ownership of XAsyncBlock* since the callback will take over ownership.
        // If the call fails, the std::unique_ptr will keep ownership and delete the XAsyncBlock*
        asyncBlock.release();
    }
}
```

有关作品管理型成就的更多信息,参见 [更新作品管理型成就](/services/xbox-services/player-data/achievements/index)。

## 离线成就

解锁 XBOX 成就不需要网络连接。如果用户未连接到 XBOX 网络(也称为 XBOX Live),之后会自动与服务器同步解锁,无需额外代码。

## 获取其他用户的成就

要获取其他用户的成就,只需调用 [XblAchievementsGetAchievementsForTitleAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members)/[XblAchievementsGetAchievementAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members) 函数,并传入你要获取成就的用户的 XBOX 用户 ID(XUID)。这类似于在 Steam 中使用 `ISteamUser::RequestUserStats`、`ISteamUserStats::GetUserStat` 与 `ISteamUserStats::GetUserAchievement`。

## 重置成就

与 Steamworks API 的 `ISteamUserStats::ResetAllStats` 不同(它可用来重置用户所有 stats 的进度,并可选地重置成就),XBOX Game Development Kit(GDK)没有以编程方式重置成就进度或解锁状态的公开能力。以进度值 0 调用 [XblAchievementsUpdateAchievementAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members) 会保持进度值不变。不过,在测试期间你可以使用 [Player Data Reset 工具(XblPlayerDataReset.exe)](/tools/tools-services/live-player-data-reset) 来完成此操作。

## 速率限制

更新成就进度时,注意不要过于频繁地调用 XBOX Services API(XSAPI),否则可能触发游戏的速率限制,导致意外行为并影响用户体验。

对于跟踪高频更新 stats 的成就,可以考虑批量 API 调用 —— 累计一定事件数或在特定时间间隔后再向服务器同步。

你可以使用 [XBOX 服务 Trace Analyzer(XblTraceAnalyzer.exe)](/tools/tools-services/live-trace-analyzer) 调试 API 调用。

有关 XBOX 服务速率限制的更多信息,参见 [细粒度速率限制](/services/xbox-services/develop/best-practices/live-fine-grained-rate-limiting)。


## Related topics

- [Steam 移植指南概览](/zh-CN/build/steam-porting-guide/overview.md)
- [GDK 与 Steamworks 之间的概念差异](/zh-CN/build/steam-porting-guide/conceptual-differences.md)
- [Steamworks 中不存在的 GDK 功能](/zh-CN/build/steam-porting-guide/gdk-features-not-in-steamworks.md)
- [基于事件的挑战的门户配置](/zh-CN/services/xbox-services/player-data/achievements/event-based/config/live-challenges-eb-portal.md)
- [成就](/zh-CN/services/xbox-services/player-data/achievements/index.md)
