> ## 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.

# 통계와 업적

> Steam 업적과 통계를 XBOX GDK 타이틀 관리 API로 포팅해 게임 내 진행을 추적하고 XBOX 서비스로 체크인을 동기화하세요.

XBOX Game Development Kit(GDK)과 Steam 모두에서 업적은 이름, 설명, 아이콘, 현재 사용자에 대해 잠금 해제되었는지 여부, 그리고 선택적으로 완료까지의 진행도와 같은 정보를 가집니다. Steam은 또한 사용자의 진행을 미리 결정된 목표와 비교하고 이 목표를 초과할 때 업적을 잠금 해제하는 데 게임에서 사용되는 통계 API를 가지고 있습니다.

실제로 XBOX Game Development Kit(GDK)에는 두 개의 업적 API가 있습니다: *이벤트 기반*(이전 명칭 *Achievements 2013*)과 *타이틀 관리*(이전 명칭 *Achievements 2017*). 두 시스템 모두 실행 가능한 옵션이지만, Steam에서 넘어왔다면 타이틀 관리 업적/통계가 훨씬 익숙할 것이며 게임에 이미 존재할 가능성이 높은 로직과 일치할 것입니다. 이런 이유로 이 항목에서는 그 API를 설명합니다. 두 API의 차이에 대한 자세한 내용은 [이벤트 기반 vs. 타이틀 관리 업적](/services/xbox-services/player-data/achievements/index)을 참조하세요. 다음 섹션은 Steamworks API와 XBOX Game Development Kit(GDK) 간 업적의 차이를 설명합니다.

타이틀 관리 업적 API를 사용하려면 XBOX 서비스 Context 핸들을 생성해야 합니다. [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)의 `XUserHandle`([사용자 인증과 소유권](/build/steam-porting-guide/features/user-authentication-and-ownership) 항목 참조)을 사용해 이를 수행할 수 있습니다.

## 업적 진행 추적

Steam API에서는 `ISteamUserStats::GetStat`/`ISteamUser::SetStat` API를 사용해 사용자의 업적 진행을 추적할 수 있으며, Steam Stats API에 저장된 값이 현재 진행 값의 원본 소스가 됩니다.

타이틀 관리 업적에서는 게임이 유일한 원본 소스이며 사용자의 현재 진행을 자체적으로 추적해야 합니다. API 호출로 클라우드의 현재 진행을 업데이트하는 것은 일종의 “체크인”으로 수행되며, XBOX 생태계의 일부 표면에서 사용자의 진행을 보여주는 데 사용됩니다. 현재 진행 값이 어디에 존재해야 하는지에 대한 구체적인 지침은 없지만, 많은 게임이 세이브 파일(및 클라우드)에 저장하도록 선택하고, 다른 게임은 자체 백엔드 서비스에 저장할 수 있습니다. 클라우드와 게임의 내부 스탯 관리 시스템 간 값을 서로 동기화된 상태로 유지하세요.

## 모든 업적 열거

Steamworks API로 사용자의 모든 통계를 얻으려면 일반적으로 `ISteamUserStats::RequestUserStats`를 호출해 서버에서 최신 통계를 가져옵니다. 해당 콜백이 실행된 후, `ISteamUserStats::GetStat`/`ISteamUserStats::GetAchievement` 함수를 API 이름으로 사용해 각 업적을 반복하고 반환된 데이터를 사용해 게임 상태를 초기화할 수 있습니다.

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()
{
    // 통계/업적을 반복하며 필요한 게임 데이터를 초기화합니다.
    // m_achievementIds가 업적의 "API 이름" 배열이라고 가정합니다.
    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);
        // 필요한 업적 초기화 수행...
    }
}
```

### XBOX Game Development Kit(GDK)

```cpp theme={null}
auto asyncBlock = std::make_unique<XAsyncBlock>();
asyncBlock->context = nullptr;
asyncBlock->callback = [](XAsyncBlock* asyncBlock)
{
    // 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);
            }
            // 이 데이터로 업적을 초기화합니다...
        }

        // 핸들 사용이 끝나면 닫습니다. 이는 메모리에서 업적 목록을
        // 해제합니다.
        XblAchievementsResultCloseHandle(resultHandle);
        achievements = nullptr; 
        // 대신 핸들을 닫지 않고 저장할 수도 있습니다.  
        // 핸들을 복사해야 한다면 XblAchievementsResultDuplicateHandle()을 호출하세요.
    }
};

// m_xblContext가 올바르게 초기화된 XBOX 서비스 Context 핸들이라고 가정합니다.
uint64_t xuid;
XblContextGetXboxUserId(m_xblContext, &xuid);

HRESULT hr = XblAchievementsGetAchievementsForTitleIdAsync(
    m_xblContext,
    xuid, 
    m_titleId, // 게임의 title ID를 담고 있는 uint32_t라고 가정합니다.
    XblAchievementType::Persistent, // achievementType: 아마도 XblAchievementType::Persistent를 사용하고 싶을 것입니다.
    false, //unlockedOnly: true인 경우 잠금 해제된 업적만 반환합니다.
    XblAchievementOrderBy::DefaultOrder, // orderBy: 들어오는 업적 목록의 순서 지정 방법. 
    0, // skipItems: 건너뛸 업적 수. 
    0, // maxItems: 모든 업적을 얻으려면/페이지네이션을 비활성화하려면 0으로 설정합니다.
    asyncBlock.get()
);
if (SUCCEEDED(hr))
{
    // 호출이 성공했으므로 콜백이 소유권을 인수하도록 std::unique_ptr의 XAsyncBlock* 소유권을 해제합니다.
    // 호출이 실패하면 std::unique_ptr가 소유권을 유지하고 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의 스탯 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) {
        // 오류 처리.
    }

    result = SteamUserStats()->GetStat(statApiName, &value);
    if (!result) {
        // 오류 처리.
    }

    SteamUserStats()->SetStat(statApiName, value + diff);
    
    if (!isCompleted && value + diff >= target)
    {
             // 업적 잠금 해제.
        SteamUserStats()->SetAchievement(achievementApiName);
        SteamUserStats()->StoreStats();
    }
}

// 다음 체크포인트에서...
if(!SteamUserStats()->StoreStats()) {
    // 오류 처리.
}
```

### XBOX Game Development Kit(GDK)

```cpp theme={null}
void MyGame::UpdateAchievement(const std::string& achievementId, int diff, int target)
{
    // 이 함수가 저장 위치에서 지정된 업적의 진행을 반환한다고 가정합니다.
    auto previousValue = MyGetAchievementProgress(achievementId);
    // 다른 곳에 진행을 저장한다면 그곳에서도 업데이트하세요. 다시 말해, 
    // 저장 위치의 진행 값을 업데이트하기 위해 작성한 함수라고 가정합니다.
    MySetAchievementProgress(previousValue + diff);
    // diff 값을 더한 후 사용자가 도달한 목표의 백분율을 계산합니다.
    uint32_t percentComplete = (previousValue + diff) / target;

    auto async = std::make_unique<XAsyncBlock>();
    async->context = nullptr;
    async->callback = [](XAsyncBlock *async)
    {
        // XAsyncBlock*의 소유권 인수
        std::unique_ptr<XAsyncBlock> asyncBlockPtr{ async }; 

        HRESULT result = XAsyncGetStatus(async, true);

        if (SUCCEEDED(result))
        {
            // 성공!
        }
        else
        {
            if (result == HTTP_E_STATUS_NOT_MODIFIED)
            {
                // 업적이 이미 완료되었거나 이전에 더 높은 값으로
                // 설정되어 있어 변경되지 않았습니다.
            }
            else
            {
                // 오류 처리.
            }
        }
    };

    // m_xblContext가 올바르게 초기화된 XBOX 서비스 Context 핸들이라고 가정합니다.
    uint64_t xuid;
    XblContextGetXboxUserId(m_xblContext, &xuid);
    HRESULT hr = XblAchievementsUpdateAchievementAsync(
        m_xblContext,
        xuid,
        achievementId.c_str(),
        percentComplete,
        async
    );

    if (SUCCEEDED(hr))
    {
        // 호출이 성공했으므로 콜백이 소유권을 인수하도록 std::unique_ptr의 XAsyncBlock* 소유권을 해제합니다.
        // 호출이 실패하면 std::unique_ptr가 소유권을 유지하고 XAsyncBlock*을 삭제합니다.
        asyncBlock.release();
    }
}
```

타이틀 관리 업적에 대한 자세한 내용은 [타이틀 관리 업적 업데이트](/services/xbox-services/player-data/achievements/index)를 참조하세요.

## 오프라인 업적

XBOX 업적 잠금 해제는 네트워크 연결이 필요하지 않습니다. 사용자가 XBOX 네트워크(XBOX Live로도 알려짐)에 연결되어 있지 않다면 추가 코드 없이 나중에 서버와 자동으로 잠금 해제됩니다.

## 다른 사용자의 업적 가져오기

다른 사용자의 업적을 가져오려면, 업적을 가져올 사용자의 XBOX User ID(XUID)를 가지고 [XblAchievementsGetAchievementsForTitleAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members)/[XblAchievementsGetAchievementAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members) 함수를 호출하기만 하면 됩니다. 이는 Steam에서 `ISteamUser::RequestUserStats`, `ISteamUserStats::GetUserStat`, `ISteamUserStats::GetUserAchievement` 함수를 사용하는 것과 유사합니다.

## 업적 재설정

사용자의 모든 통계에 대한 진행을 재설정하는 데 사용할 수 있고, 지정된 경우 업적도 재설정할 수 있는 Steamworks API의 `ISteamUserStats::ResetAllStats`와 달리, XBOX Game Development Kit(GDK)에는 프로그래밍 방식으로 업적 진행이나 잠금 해제 상태를 재설정하는 문서화된 기능이 없습니다. [XblAchievementsUpdateAchievementAsync](/reference/live/xsapi-c/achievements_c/achievements_c_members)를 진행 값 0으로 호출해도 진행 값은 변경되지 않습니다. 그러나 테스트할 때 이를 수행하기 위해 [Player Data Reset 도구 (XblPlayerDataReset.exe)](/tools/tools-services/live-player-data-reset)를 사용할 수 있습니다.

## 속도 제한

업적 진행을 업데이트할 때 XBOX Services API(XSAPI)를 너무 자주 호출하지 않도록 주의하세요. 그렇지 않으면 게임이 속도 제한에 걸릴 수 있습니다. 이는 예기치 못한 동작과 사용자에게 좋지 않은 경험을 초래할 수 있습니다.

자주 업데이트되는 통계를 추적하는 업적의 경우, API 호출을 특정 개수의 이벤트가 발생한 후 발행하거나 특정 시간 간격으로 서버에서 업데이트하도록 배치화해 보세요.

[XBOX services 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

- [Steamworks에 없는 GDK 기능](/ko/build/steam-porting-guide/gdk-features-not-in-steamworks.md)
- [Steam 포팅 가이드 개요](/ko/build/steam-porting-guide/overview.md)
- [GDK와 Steamworks의 개념적 차이](/ko/build/steam-porting-guide/conceptual-differences.md)
- [이벤트 기반 통계 대 타이틀 관리 통계](/ko/services/xbox-services/player-data/stats-leaderboards/live-stats-eb-vs-tm.md)
- [Using resettable statistics and leaderboards](/ko/services/playfab/community/leaderboards/tournaments-leaderboards/using-resettable-statistics-and-leaderboards.md)
