---
title: "Game Center services"
description: "Use Game Center achievements, leaderboards, and saved games."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Authenticate with [Game Center](/integrations/apple/setup-login) before using these services. Create achievement and leaderboard IDs in App Store Connect; use those exact IDs in your game.

## Achievements and scores

**Blueprint**

<Accordion>
  <AccordionItem title="Report achievement progress">
<BlueprintViewer title="Report achievement progress" src="/blueprints/crossplay-apple-achievement.txt" />
  </AccordionItem>
  <AccordionItem title="Submit a score">
<BlueprintViewer title="Submit a score" src="/blueprints/crossplay-apple-submit-score.txt" />
  </AccordionItem>
  <AccordionItem title="Read top scores">
<BlueprintViewer title="Read top scores" src="/blueprints/crossplay-apple-leaderboard.txt" />
  </AccordionItem>
</Accordion>

**C++**

Game module dependencies: `GameCenterKit`, `BetideCore`.

```cpp
#include "Nodes/Achievements/GCIK_Achievements.h"
#include "Nodes/Leaderboards/GCIK_Leaderboards.h"

void ReportAppleAchievement(UObject *WorldContext, const FString &AchievementId, double Percent,
                            TFunction<void(const FBetideError &)> Completion)
{
    auto *Request =
        UGCIK_ReportAchievement::ReportAchievement(WorldContext, AchievementId, Percent, true);
    if (!Request)
        return;
    Request->OnSuccessCallback = Completion;
    Request->OnFailureCallback = MoveTemp(Completion);
    Request->Activate();
}

void SubmitAppleScore(UObject *WorldContext, const FString &LeaderboardId, int64 Score,
                      TFunction<void(const FBetideError &)> Completion)
{
    auto *Request = UGCIK_SubmitScore::SubmitScore(WorldContext, {LeaderboardId}, Score);
    if (!Request)
        return;
    Request->OnSuccessCallback = Completion;
    Request->OnFailureCallback = MoveTemp(Completion);
    Request->Activate();
}
```

```cpp
void ReadAppleTopScores(
    UObject *WorldContext, const FString &LeaderboardId,
    TFunction<void(const TArray<FGCIK_LeaderboardEntry> &, int64, const FBetideError &)> Completion)
{
    auto *Request = UGCIK_LoadLeaderboardEntries::LoadLeaderboardEntries(
        WorldContext, LeaderboardId, EGCIK_LeaderboardPlayerScope::Global,
        EGCIK_LeaderboardTimeScope::AllTime, 1, 10);
    if (!Request)
        return;
    Request->OnSuccessCallback = Completion;
    Request->OnFailureCallback = MoveTemp(Completion);
    Request->Activate();
}
```

Use weak UObject captures in completion handlers. `UGCIK_LeaderboardsLibrary::ShowLeaderboardUI(LeaderboardId)` opens Apple's leaderboard UI.

Achievement progress is **0–100 percent**; 100 unlocks it. Leaderboard scores use the board's configured ordering and format. Entry ranges start at **1**.

| Need | Node / C++ entry point |
| --- | --- |
| Player progress | **Load Achievements** / `UGCIK_LoadAchievements::LoadAchievements` |
| Titles and descriptions | **Load Achievement Descriptions** / `UGCIK_LoadAchievementDescriptions::LoadAchievementDescriptions` |
| Ranked entries | **Load Leaderboard Entries** / `UGCIK_LoadLeaderboardEntries::LoadLeaderboardEntries` |
| Entries around the player | **Load Player Centered Entries** / `UGCIK_LoadPlayerCenteredEntries::LoadPlayerCenteredEntries` |
| Previous recurring board | **Load Previous Leaderboard Occurrence** / `UGCIK_LoadPreviousOccurrence::LoadPreviousOccurrence` |

For shared gameplay calls across stores, use [Crossplay achievements](/crossplay/achievements-stats) and [leaderboards](/crossplay/leaderboards). Apple's provider supports achievements and leaderboards; it does not provide the generic Crossplay stats service.

## Saved games

Game Center saves use iCloud and stay within Apple's ecosystem. [Configure iCloud for the signed app](https://developer.apple.com/documentation/gamekit/saving-the-player-s-game-data-to-an-icloud-account) and test with an iCloud account. They are separate from [Crossplay cloud saves](/crossplay/cloud-saves).

**Blueprint**

<Accordion>
  <AccordionItem title="Save player data">
<BlueprintViewer title="Save player data" src="/blueprints/crossplay-apple-save-data.txt" />
  </AccordionItem>
  <AccordionItem title="List saved games">
<BlueprintViewer title="List saved games" src="/blueprints/crossplay-apple-list-saves.txt" />
  </AccordionItem>
  <AccordionItem title="Load saved bytes">
<BlueprintViewer title="Load saved bytes" src="/blueprints/crossplay-apple-load-save.txt" />
  </AccordionItem>
  <AccordionItem title="Resolve chosen save versions">
<BlueprintViewer title="Resolve chosen save versions" src="/blueprints/crossplay-apple-resolve-saves.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#include "Nodes/SavedGames/GCIK_SavedGames.h"

void SaveAppleGame(UObject *WorldContext, const FString &Slot, const TArray<uint8> &Data,
                   const FScriptDelegate &OnSaved, const FScriptDelegate &OnFailed)
{
    auto *Request = UGCIK_SaveGameData::SaveGameData(WorldContext, Slot, Data);
    if (!Request)
        return;
    Request->OnSuccess.Add(OnSaved);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
}
```

```cpp
void ListAppleSaves(UObject *WorldContext, const FScriptDelegate &OnLoaded,
                    const FScriptDelegate &OnFailed)
{
    auto *Request = UGCIK_LoadAllSavedGames::LoadAllSavedGames(WorldContext);
    if (!Request)
        return;
    Request->OnSuccess.Add(OnLoaded);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
}

void LoadAppleSave(UObject *WorldContext, const FString &Slot, const FScriptDelegate &OnLoaded,
                   const FScriptDelegate &OnFailed)
{
    auto *Request = UGCIK_LoadSavedGameData::LoadSavedGameData(WorldContext, Slot);
    if (!Request)
        return;
    Request->OnSuccess.Add(OnLoaded);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
}

void ResolveAppleSaves(UObject *WorldContext, const TArray<FGCIK_SavedGame> &Conflicts,
                       const TArray<uint8> &MergedData, const FScriptDelegate &OnResolved,
                       const FScriptDelegate &OnFailed)
{
    auto *Request = UGCIK_ResolveConflicts::ResolveConflicts(WorldContext, Conflicts, MergedData);
    if (!Request)
        return;
    Request->OnSuccess.Add(OnResolved);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
}
```

Bind the delegates to `UFUNCTION` handlers with `(const FGCIK_SavedGame&, const FBetideError&)`. The load-data handler adds `const TArray<uint8>& Data` between the record and error. The list handler receives `(const TArray<FGCIK_SavedGame>&, const FBetideError&)`; conflict resolution uses the save handler signature.

Keep a version in your save payload so later releases can read older saves. **Load All Saved Games** returns metadata; **Load Saved Game Data** reads a named save.

For conflicts, load the list first: this caches the native references required by **Resolve Saved Game Conflicts**. Pass the conflicting records and your chosen merged bytes. The wrapper does not merge game state for you. Its load node selects the first save with a matching name, so it cannot read each conflicting version by device or date. Only resolve with bytes your game has already chosen; do not assume this is a complete merge API. In C++, use `UGCIK_ResolveConflicts::ResolveConflicts`; **Delete Saved Game** is `UGCIK_DeleteSavedGame::DeleteSavedGame`.
