Save player data
Write, read, and reconcile a player's saved data.
Configure Storage providers in Betide → Crossplay, then sign in. The write examples store UTF-8 text. Serialize Unreal save objects yourself before uploading.
Read a save
Loading the interactive viewer.
Game module dependencies: UCIKCore, BetideCore.
#include "Engine/GameInstance.h"
#include "Storage/UCIKStorageSubsystem.h"
#include "UCIKCore.h"
void ReadCloudSave(UGameInstance &GameInstance)
{
auto *Service = GameInstance.GetSubsystem<UUCIKStorageSubsystem>();
if (!Service)
return;
Service->ReadUserFile(TEXT("save.bin"),
FBetideCallback<FUCIK_UserFile>::CreateWeakLambda(
&GameInstance, [](const TBetideResult<FUCIK_UserFile> &Result) {
if (!Result.bOk)
{
UE_LOG(LogUCIK, Warning, TEXT("%s"),
*Result.Error.GetLogString());
return;
}
UE_LOG(LogUCIK, Log, TEXT("Save bytes: %d; checksum: %s"),
Result.GetValue().Data.Num(), *Result.GetValue().Checksum);
}));
}Keep the returned Checksum with the local save. A read uses the first configured provider; it does not merge different saves across stores.
Write without overwriting a changed save
Loading the interactive viewer.
Game module dependencies: UCIKCore, UCIKNodes, BetideCore.
#include "Engine/GameInstance.h"
#include "Helpers/UCIK_HelperLibraries.h"
#include "Storage/UCIKStorageSubsystem.h"
#include "UCIKCore.h"
void WriteCloudSave(UGameInstance &GameInstance, const FString &SaveText,
const FString &PreviousChecksum)
{
auto *Service = GameInstance.GetSubsystem<UUCIKStorageSubsystem>();
if (!Service)
return;
const FUCIK_UserFile File =
UUCIK_UserFileLibrary::MakeUserFileFromString(TEXT("save.bin"), SaveText);
Service->WriteUserFileWithConflictCheck(
File, PreviousChecksum,
FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
&GameInstance, [](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
if (!Result.bOk)
{
UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
return;
}
for (const auto &Pair : Result.GetValue())
{
UE_LOG(LogUCIK, Log, TEXT("%s: %s"), *Pair.Key.ToString(),
Pair.Value ? TEXT("written") : TEXT("not written"));
}
}));
}An empty expected checksum means “create only if absent.” Bind On Save Conflict to offer a choice between the local and remote save. Do not silently retry a conflict with an unconditional write.
Every write provider must pass the read/check step before uploads begin. Another device can write between the check and upload. If an upload fails, providers can be left with different copies. Read back before the next edit.
Files and quota
List cloud saves
Loading the interactive viewer.
Read storage quota
Loading the interactive viewer.
Delete a cloud save
Loading the interactive viewer.
Loading the interactive viewer.
Game module dependencies: UCIKCore, UCIKNodes, BetideCore.
#include "Engine/GameInstance.h"
#include "Helpers/UCIK_HelperLibraries.h"
#include "Storage/UCIKStorageSubsystem.h"
#include "UCIKCore.h"
namespace CloudSaveExamples
{
UUCIKStorageSubsystem *Service(UGameInstance &Game)
{
auto *Storage = Game.GetSubsystem<UUCIKStorageSubsystem>();
if (!Storage)
UE_LOG(LogUCIK, Warning, TEXT("Storage subsystem is unavailable"));
return Storage;
}
void ListCloudSaves(UGameInstance &Game)
{
auto *Storage = Service(Game);
if (!Storage)
return;
Storage->EnumerateUserFiles(FBetideCallback<TArray<FUCIK_UserFileMetadata>>::CreateWeakLambda(
&Game, [](const TBetideResult<TArray<FUCIK_UserFileMetadata>> &Result) {
if (!Result.bOk)
{
UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
return;
}
for (const FUCIK_UserFileMetadata &File : Result.GetValue())
UE_LOG(LogUCIK, Log, TEXT("%s: %lld bytes"), *File.Name, File.SizeBytes);
}));
}
void ReadStorageQuota(UGameInstance &Game)
{
auto *Storage = Service(Game);
if (!Storage)
return;
Storage->GetStorageQuota(FBetideCallback<TArray<FUCIK_StorageQuota>>::CreateWeakLambda(
&Game, [](const TBetideResult<TArray<FUCIK_StorageQuota>> &Result) {
if (!Result.bOk)
{
UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
return;
}
for (const FUCIK_StorageQuota &Quota : Result.GetValue())
UE_LOG(LogUCIK, Log,
TEXT("%s: available %lld; total %lld bytes (-1 means unknown)"),
*Quota.ProviderId.ToString(), Quota.AvailableBytes, Quota.TotalBytes);
}));
}
FBetideCallback<TMap<FBetideProviderId, bool>> ReportProviders(UGameInstance &Game, FString Operation)
{
return FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
&Game, [Operation =
MoveTemp(Operation)](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
if (!Result.bOk)
{
UE_LOG(LogUCIK, Warning, TEXT("%s: %s"), *Operation, *Result.Error.GetLogString());
return;
}
for (const auto &Pair : Result.GetValue())
UE_LOG(LogUCIK, Log, TEXT("%s on %s: %s"), *Operation, *Pair.Key.ToString(),
Pair.Value ? TEXT("completed") : TEXT("not completed"));
});
}
void DeleteConfirmedSave(UGameInstance &Game, const FString &FileName)
{
if (auto *Storage = Service(Game))
Storage->DeleteUserFile(FileName, ReportProviders(Game, TEXT("Delete save")));
}
void OverwriteConfirmedCloudSave(UGameInstance &Game, const FString &SaveText)
{
auto *Storage = Service(Game);
if (!Storage)
return;
const FUCIK_UserFile File =
UUCIK_UserFileLibrary::MakeUserFileFromString(TEXT("save.bin"), SaveText);
Storage->WriteUserFile(File, ReportProviders(Game, TEXT("Overwrite save")));
}
} // namespace CloudSaveExamplesOnly call delete or overwrite after the player chooses it. Refresh the file list after completion. Listing uses the first provider that supports enumeration.
Multiple providers and conflicting devices
The conflict check reads every selected write provider before writing. A failed read, failed absence check, or changed checksum stops all writes. Checking and writing are not atomic: another device can still write between them, and a later upload failure can leave only some providers updated.
A successful write result does not guarantee every provider succeeded. C++ exposes per-provider results; the Blueprint write/delete nodes expose only overall success and error. Use C++ when you need per-provider write receipts. Quota -1 means unknown; EOS storage does not expose quota through this API. A zero modification time means unavailable, not an empty save.
Use provider-specific Game Center saved games and Google Play snapshots through their own APIs.