---
title: "Google Play services"
description: "Use Play Games, saved games, Integrity, reviews, and updates."
seo:
  noindex: true
---

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

Complete [Android setup and sign-in](/integrations/google/setup-login) first. Create achievement and leaderboard IDs in Play Console and enable Saved Games if you use snapshots.

## Achievements and leaderboards

**Blueprint**

<Accordion>
  <AccordionItem title="Unlock an achievement">
<BlueprintViewer title="Unlock an achievement" src="/blueprints/crossplay-google-unlock-achievement.txt" />
  </AccordionItem>
  <AccordionItem title="Add achievement steps">
<BlueprintViewer title="Add achievement steps" src="/blueprints/crossplay-google-increment-achievement.txt" />
  </AccordionItem>
  <AccordionItem title="Submit a score">
<BlueprintViewer title="Submit a score" src="/blueprints/crossplay-google-submit-score.txt" />
  </AccordionItem>
  <AccordionItem title="Read top scores">
<BlueprintViewer title="Read top scores" src="/blueprints/crossplay-google-top-scores.txt" />
  </AccordionItem>
</Accordion>

**C++**

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

```cpp
#include "Nodes/Achievements/GPIK_Achievements_AsyncFunction.h"
#include "Nodes/Leaderboards/GPIK_Leaderboards_AsyncFunction.h"

void UnlockGoogleAchievement(UObject *WorldContext, const FString &AchievementId,
                             UGPIK_UnlockAchievement::FNativeCallback Completion)
{
    auto *Request = UGPIK_UnlockAchievement::UnlockAchievement(WorldContext, AchievementId);
    if (!Request)
        return;
    Request->SetNativeCallback(MoveTemp(Completion));
    Request->Activate();
}

void SubmitGoogleScore(UObject *WorldContext, const FString &LeaderboardId, int64 Score,
                       UGPIK_SubmitScore::FNativeCallback Completion)
{
    auto *Request = UGPIK_SubmitScore::SubmitScore(WorldContext, LeaderboardId, Score);
    if (!Request)
        return;
    Request->SetNativeCallback(MoveTemp(Completion));
    Request->Activate();
}

void IncrementGoogleAchievement(UObject *WorldContext, const FString &AchievementId, int32 Steps,
                                const FScriptDelegate &OnIncremented,
                                const FScriptDelegate &OnFailed)
{
    auto *Request =
        UGPIK_IncrementAchievement::IncrementAchievement(WorldContext, AchievementId, Steps);
    if (!Request)
        return;
    Request->OnSuccess.Add(OnIncremented);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
}

void ReadGoogleTopScores(UObject *WorldContext, const FString &LeaderboardId,
                         UGPIK_LoadTopScores::FNativeCallback Completion)
{
    auto *Request = UGPIK_LoadTopScores::LoadTopScores(WorldContext, LeaderboardId,
                                                       EGPIK_LeaderboardTimeSpan::AllTime,
                                                       EGPIK_LeaderboardCollection::Public, 10);
    if (!Request)
        return;
    Request->SetNativeCallback(MoveTemp(Completion));
    Request->Activate();
}
```

Unlock and submit callbacks receive `(bool bSuccess, const FString& Id, const FBetideError& Error)`. Top-score callbacks receive `(bool bSuccess, const TArray<FGPIK_LeaderboardScore>& Scores, const FBetideError& Error)`. Bind increment delegates to `UFUNCTION` handlers with `(const FGPIK_AchievementIncrementResult& Result, const FBetideError& Error)`. Use weak UObject captures when updating game objects.

Incremental achievements take a **positive number of steps**, not a percentage. Score ordering comes from the leaderboard configuration.

| Need | Node / C++ entry point |
| --- | --- |
| Current progress | **Load Achievements** / `UGPIK_LoadAchievements::LoadAchievements` |
| Increase progress | **Increment Achievement** / `UGPIK_IncrementAchievement::IncrementAchievement` |
| Top entries | **Load Top Scores** / `UGPIK_LoadTopScores::LoadTopScores` |
| Player's rank | **Load Player Score** / `UGPIK_LoadPlayerScore::LoadPlayerScore` |
| Native UI | **Show Achievements UI** / `UGPIK_AchievementsLibrary::ShowAchievementsUI` |

The Google Crossplay progression provider supports [achievements](/crossplay/achievements-stats) and [leaderboards](/crossplay/leaderboards), not generic stats.

## Saved games

**Blueprint**

<Accordion>
  <AccordionItem title="Upload a SaveGame">
<BlueprintViewer title="Upload a SaveGame" src="/blueprints/crossplay-google-save-game.txt" />
  </AccordionItem>
  <AccordionItem title="Download a SaveGame">
<BlueprintViewer title="Download a SaveGame" src="/blueprints/crossplay-google-load-game.txt" />
  </AccordionItem>
</Accordion>

**C++**

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

bool SaveGoogleGame(UObject *WorldContext, USaveGame *SaveGame, const FScriptDelegate &OnSaved,
                    const FScriptDelegate &OnFailed)
{
    const TArray<uint8> Data = UGPIK_CloudSaveLibrary::SaveGameObjectToData(SaveGame);
    if (Data.IsEmpty())
        return false;
    auto *Request =
        UGPIK_SaveGame::SaveGame(WorldContext, TEXT("PlayerSave"), Data, TEXT("Player progress"));
    if (!Request)
        return false;
    Request->OnSuccess.Add(OnSaved);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
    return true;
}

void LoadGoogleGame(UObject *WorldContext, const FScriptDelegate &OnLoaded,
                    const FScriptDelegate &OnFailed)
{
    auto *Request = UGPIK_LoadGame::LoadGame(WorldContext, TEXT("PlayerSave"));
    if (!Request)
        return;
    Request->OnSuccess.Add(OnLoaded);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
}

USaveGame *DeserializeGoogleSave(const FGPIK_SavedGame &SavedGame)
{
    return UGPIK_CloudSaveLibrary::DataToSaveGameObject(SavedGame.Data);
}
```

Save handlers use `(const FString& SnapshotName, const FBetideError& Error)`; load handlers use `(const FGPIK_SavedGame& SavedGame, const FBetideError& Error)`. Bind them as `UFUNCTION` delegates before activation. `SaveGoogleGame` returning `true` means the request started. On load success, deserialize, check for `nullptr`, and cast to your SaveGame class; retain it in a `UPROPERTY` if needed beyond the callback.

Pass your populated SaveGame object to upload. Cast the downloaded object to your SaveGame class before applying its fields. Use a stable slot name and version your payload. **Load Saved Games List** lists snapshot metadata; **Load Game from Cloud** reads the bytes. In C++, use `UGPIK_LoadSavedGamesList::LoadSavedGamesList` and `UGPIK_LoadGame::LoadGame`.

The wrapper requests Google's **most recently modified** conflict policy. It exposes no manual conflict-selection callback; do not rely on it to merge simultaneous progress. These snapshots stay in Play Games and are separate from [Crossplay cloud saves](/crossplay/cloud-saves).

## Integrity, reviews, and updates

These Android services use separate modules and do not require a Play Games achievement or leaderboard setup.

**Blueprint**

<Accordion>
  <AccordionItem title="Request a review">
<BlueprintViewer title="Request a review" src="/blueprints/crossplay-google-review.txt" />
  </AccordionItem>
  <AccordionItem title="Start a flexible update">
<BlueprintViewer title="Start a flexible update" src="/blueprints/crossplay-google-start-update.txt" />
  </AccordionItem>
  <AccordionItem title="Install a downloaded update">
<BlueprintViewer title="Install a downloaded update" src="/blueprints/crossplay-google-complete-update.txt" />
  </AccordionItem>
</Accordion>

**C++**

Add `PlayInAppReviews`, `PlayInAppUpdates`, `BetideCore`, and `Engine` for these examples.

<Accordion>
  <AccordionItem title="Request a review">

`GoogleReviewGameInstance.h`:

```cpp
#pragma once
#include "Engine/GameInstance.h"
#include "InAppReviewsManager.h"
#include "PlayInAppReviews.h"
#include "GoogleReviewGameInstance.generated.h"

UCLASS()
class UGoogleReviewGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    void RequestPlayerReview()
    {
        if (auto *Manager = GetSubsystem<UInAppReviewsManager>())
        {
            FReviewOperationCompletedDelegate Completion;
            Completion.BindDynamic(this, &ThisClass::OnPrepared);
            Manager->RequestReviewFlow(Completion);
        }
    }

  private:
    UFUNCTION()
    void OnPrepared(EInAppReviewErrorCode Code, const FBetideError &Error)
    {
        if (Error.IsError())
        {
            UE_LOG(LogPlayInAppReviews, Warning, TEXT("%s"), *Error.Message);
            return;
        }
        if (auto *Manager = GetSubsystem<UInAppReviewsManager>())
        {
            FReviewOperationCompletedDelegate Completion;
            Completion.BindDynamic(this, &ThisClass::OnCompleted);
            Manager->LaunchReviewFlow(Completion);
        }
    }
    UFUNCTION()
    void OnCompleted(EInAppReviewErrorCode Code, const FBetideError &Error)
    {
        if (Error.IsError())
            UE_LOG(LogPlayInAppReviews, Warning, TEXT("%s"), *Error.Message);
        else
            UE_LOG(LogPlayInAppReviews, Log, TEXT("Review flow completed"));
    }
};
```

  </AccordionItem>
  <AccordionItem title="Start and complete a flexible update">

`GoogleUpdateGameInstance.h`:

```cpp
#pragma once
#include "Engine/GameInstance.h"
#include "InAppUpdatesManager.h"
#include "PlayInAppUpdates.h"
#include "GoogleUpdateGameInstance.generated.h"

UCLASS()
class UGoogleUpdateGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    void StartFlexibleUpdate()
    {
        if (auto *Manager = GetSubsystem<UInAppUpdatesManager>())
        {
            FRequestInfoOperationCompletedDelegate Completion;
            Completion.BindDynamic(this, &ThisClass::OnStartInfo);
            Manager->RequestInfo(Completion);
        }
    }
    void InstallDownloadedUpdate()
    {
        if (auto *Manager = GetSubsystem<UInAppUpdatesManager>())
        {
            FRequestInfoOperationCompletedDelegate Completion;
            Completion.BindDynamic(this, &ThisClass::OnInstallInfo);
            Manager->RequestInfo(Completion);
        }
    }

  private:
    UFUNCTION()
    void OnStartInfo(EAppUpdateErrorCode Code, UAppUpdateInfo *Info, const FBetideError &Error)
    {
        if (!CheckInfo(Info, Error))
            return;
        auto *Options =
            UAppUpdateOptions::CreateAppUpdateOptions(EAppUpdateType::AppUpdate_TYPE_FLEXIBLE);
        if (!Options || !Info->IsUpdateTypeAllowed(Options))
        {
            UE_LOG(LogPlayInAppUpdates, Log, TEXT("Flexible update is not available"));
            return;
        }
        if (auto *Manager = GetSubsystem<UInAppUpdatesManager>())
        {
            FUpdateOperationCompletedDelegate Completion;
            Completion.BindDynamic(this, &ThisClass::OnStarted);
            Manager->StartUpdate(Info, Options, Completion);
        }
    }
    UFUNCTION()
    void OnInstallInfo(EAppUpdateErrorCode Code, UAppUpdateInfo *Info, const FBetideError &Error)
    {
        if (!CheckInfo(Info, Error))
            return;
        const EAppUpdateStatus Status = Info->GetStatus();
        if (Status != EAppUpdateStatus::AppUpdate_DOWNLOADED)
        {
            UE_LOG(LogPlayInAppUpdates, Log, TEXT("Update status: %s"),
                   *UEnum::GetValueAsString(Status));
            return;
        }
        if (auto *Manager = GetSubsystem<UInAppUpdatesManager>())
        {
            FUpdateOperationCompletedDelegate Completion;
            Completion.BindDynamic(this, &ThisClass::OnInstalled);
            Manager->CompleteUpdate(Completion);
        }
    }
    UFUNCTION()
    void OnStarted(EAppUpdateErrorCode Code, const FBetideError &Error)
    {
        if (Error.IsError())
            UE_LOG(LogPlayInAppUpdates, Warning, TEXT("%s"), *Error.Message);
        else
            UE_LOG(LogPlayInAppUpdates, Log,
                   TEXT("Update flow completed; check download status before installing"));
    }
    UFUNCTION()
    void OnInstalled(EAppUpdateErrorCode Code, const FBetideError &Error)
    {
        if (Error.IsError())
            UE_LOG(LogPlayInAppUpdates, Warning, TEXT("%s"), *Error.Message);
        else
            UE_LOG(LogPlayInAppUpdates, Log, TEXT("Install request completed"));
    }
    static bool CheckInfo(UAppUpdateInfo *Info, const FBetideError &Error)
    {
        if (Error.IsError())
        {
            UE_LOG(LogPlayInAppUpdates, Warning, TEXT("%s"), *Error.Message);
            return false;
        }
        if (!Info)
        {
            UE_LOG(LogPlayInAppUpdates, Warning,
                   TEXT("Update info unavailable; use an Android Play build"));
            return false;
        }
        return true;
    }
};
```

  </AccordionItem>
</Accordion>

**Integrity:** the current `Request` implementation ignores non-empty `RequestHash` values. Action-bound token verification needs that plugin fix first. Keep server-side hash checks; do not accept an unbound token as proof of a protected action.

<BlueprintViewer title="Action-bound integrity token (blocked)" nodeNames={["Prepare Integrity Token", "Request"]} />


**Reviews:** request the review flow, then launch it after the request succeeds. Completion does not prove that a review dialog appeared or that the player rated the game; Google applies display quotas. See [review requirements](https://developer.android.com/guide/playcore/in-app-review).

**Updates:** request current update info, check that your chosen update type is allowed, then start it with **Create App Update Options**. The install example checks status once and completes only when it is **Downloaded**. Call it when the player chooses to restart, after starting the update in the same app session; the manager retains state from **Start Update**. Do not overlap info requests. Test through a supported Google Play installation path, as described in [Google's update testing guide](https://developer.android.com/guide/playcore/in-app-updates/test).
