SuperAPI AI Directory

save-and-load

Coding & development

View repository

Updated 11-06 16:19

About this skill

Save & load

Persistence uses a USaveGame object: put the data to persist in UPROPERTY members, then write the object to a named slot and read it back. The slot API lives in UGameplayStatics; the actual file I/O is delegated to ISaveGameSystem, which is swapped per platform.

When to use this skill

  • Saving and restoring player progress, inventory, world flags, or per-player settings.
  • Checking whether a save slot exists; deleting or enumerating slots; multiple slot/profile flows.
  • Serializing runtime actor state (dynamic objects) into a byte array inside a SaveGame object.
  • Migrating save data after adding or removing fields.
  • Understanding what goes in a USaveGame vs. config (UGameUserSettings/.ini).

Mental model

USaveGame subclass        — the data bag; UPROPERTY members are what gets serialized
UGameplayStatics          — the entry point: create / save / load / delete / check slots
ISaveGameSystem           — the platform adapter; FGenericSaveGameSystem writes .sav files
FArchive (ArIsSaveGame)   — serialization flag; UPROPERTY(SaveGame) fields filtered by this
FObjectAndNameAsStringProxyArchive — proxy wrapping FMemoryWriter/Reader for actor state

SaveGameToSlot writes all non-transient UPROPERTYs on the USaveGame object — the SaveGame specifier on the USaveGame itself does nothing; it matters when you use the archive pattern to selectively serialize actor properties (see references/serializing-actor-state.md).

Define a SaveGame

// MySaveGame.h
#pragma once
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"

UCLASS()
class MYGAME_API UMySaveGame : public USaveGame
{
    GENERATED_BODY()
public:
    UPROPERTY() int32 SaveVersion = 1;      // increment when schema changes
    UPROPERTY() FString PlayerName;
    UPROPERTY() int32 PlayerLevel = 1;
    UPROPERTY() FTransform LastCheckpoint;
    UPROPERTY() TArray<FName> UnlockedAbilities;

    // Serialized actor-state blobs (see actor serialization pattern below)
    UPROPERTY() TArray<uint8> DynamicActorData;
};

Rules for members:

  • Must be UPROPERTY — bare fields are invisible to the serializer.
  • Prefer plain data: ints, floats, strings, names, enums, structs, arrays of the above.
  • Never store live UObject* or actor pointers — they are meaningless on reload. Store stable identifiers (slot index, FName, FSoftObjectPath), resolve back to live objects on load.
  • FTransform, FVector, FRotator, and custom USTRUCTs are fine as long as all their members are UPROPERTY.

Save & load — synchronous

#include "Kismet/GameplayStatics.h"

// ---- Save ----
UMySaveGame* Save = Cast<UMySaveGame>(
    UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
if (Save)
{
    Save->PlayerLevel = CurrentLevel;
    Save->PlayerName  = PlayerName;
    UGameplayStatics::SaveGameToSlot(Save, TEXT("Slot0"), /*UserIndex*/ 0);
}

// ---- Load ----
if (UGameplayStatics::DoesSaveGameExist(TEXT("Slot0"), 0))
{
    UMySaveGame* Loaded = Cast<UMySaveGame>(
        UGameplayStatics::LoadGameFromSlot(TEXT("Slot0"), 0));
    if (Loaded) { ApplySaveData(Loaded); }
}

// ---- Delete ----
UGameplayStatics::DeleteGameInSlot(TEXT("Slot0"), 0);

UserIndex differentiates per-user/profile saves; on PC it is typically 0. Slots are arbitrary strings — use a consistent naming convention ("Profile_0", "AutoSave", etc.).

Save & load — async (prefer for gameplay saves)

Synchronous save/load blocks the game thread. For any save triggered during active gameplay use the async variants, which serialize on the game thread but offload the file write/read to a worker:

// ---- Async save ----
FAsyncSaveGameToSlotDelegate OnSaved;
// Callback signature: void (const FString& Slot, int32 User, bool bSuccess)
OnSaved.BindUObject(this, &AMyGameMode::HandleSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot(Save, TEXT("Slot0"), 0, OnSaved);

// ---- Async load ----
FAsyncLoadGameFromSlotDelegate OnLoaded;
// Callback signature: void (const FString& Slot, int32 User, USaveGame* Loaded)
OnLoaded.BindUObject(this, &AMyGameMode::HandleLoadComplete);
UGameplayStatics::AsyncLoadGameFromSlot(TEXT("Slot0"), 0, OnLoaded);

Both delegates are declared in Kismet/GameplayStatics.h:43-47.

Serializing actor state (UPROPERTY(SaveGame) + FMemoryWriter)

For dynamic world objects (destructibles, chests, NPCs), capture actor state into a byte array stored inside your USaveGame. Mark the actor properties to persist with UPROPERTY(SaveGame), then use FObjectAndNameAsStringProxyArchive (with ArIsSaveGame = true) to filter them. Only properties marked UPROPERTY(SaveGame) are written when ArIsSaveGame is set.

// On an actor class:
UPROPERTY(SaveGame) bool bOpened = false;
UPROPERTY(SaveGame) int32 RemainingUses = 3;
UPROPERTY()         float SomeTransientValue; // NOT saved (no SaveGame specifier)

// ---- Serialize actor → byte array ----
TArray<uint8> ActorBytes;
{
    FMemoryWriter MemWriter(ActorBytes, /*bIsPersistent*/ true);
    FObjectAndNameAsStringProxyArchive Ar(MemWriter, /*bInLoadIfFindFails*/ false);
    Ar.ArIsSaveGame = true;          // restricts serialization to UPROPERTY(SaveGame) fields
    SomeActor->Serialize(Ar);
}
MySaveGame->DynamicActorData = ActorBytes; // store in the USaveGame

// ---- Restore actor ← byte array ----
{
    FMemoryReader MemReader(MySaveGame->DynamicActorData, /*bIsPersistent*/ true);
    FObjectAndNameAsStringProxyArchive Ar(MemReader, /*bInLoadIfFindFails*/ true);
    Ar.ArIsSaveGame = true;
    SomeActor->Serialize(Ar);
}

See references/serializing-actor-state.md for the full multi-actor pattern, spawn/restore loop, and gotchas.

Per-player saves — ULocalPlayerSaveGame (UE 5.3+)

ULocalPlayerSaveGame (also in SaveGame.h) extends USaveGame with built-in versioning, HandlePostLoad/HandlePreSave/HandlePostSave hooks, and synchronous/async helpers tied to a specific ULocalPlayer. It is the recommended base for per-user saves when your game supports multiple local players or needs structured versioning:

// Sync load-or-create for a specific player controller
UMyPlayerSave* PS = Cast<UMyPlayerSave>(
    ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
        UMyPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot")));

// Async variant
ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
    UMyPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot"),
    FOnLocalPlayerSaveGameLoaded::CreateUObject(this, &AMyHUD::OnPlayerSaveLoaded));

Declared in Runtime/Engine/Classes/GameFramework/SaveGame.h:47-226.

Versioning & migration

  • Add UPROPERTY() int32 SaveVersion = 1; from day one. Increment when the schema changes.
  • Adding a UPROPERTY is backward-compatible (missing fields load as their C++ default).
  • Renaming or removing a field is a breaking change — handle it in load logic:
void UMySaveGame::PostLoad()
{
    Super::PostLoad();
    if (SaveVersion < 2)
    {
        // e.g. migrate OldField → NewField
        NewField = OldField_Deprecated;
        SaveVersion = 2;
    }
}
  • For ULocalPlayerSaveGame, override GetLatestDataVersion() and do fixup in HandlePostLoad.
  • Always null-check the loaded object — a corrupt or mismatched save returns nullptr.

What to save (design)

  • Persist authoritative game state: progress, stats, unlocks, world-object states, settings.
  • Do not serialize the entire live level — capture the minimum data to reconstruct the world on load (spawn actors from a list of records, restore their state from byte blobs).
  • Cross-level/session data (e.g. unlocked chapters) belongs in GameInstance or a subsystem during a session; persist it to a USaveGame slot at save points.
  • Game and graphics settings often fit better in UGameUserSettings / config .ini files (project-structure) — reserve USaveGame for gameplay state.

Gotchas

  • Non-UPROPERTY fields are invisible — the serializer cannot see bare C++ fields.
  • UPROPERTY(SaveGame) does nothing on the SaveGame object itself — it gates selective actor serialization via ArIsSaveGame. On the USaveGame subclass every non-transient UPROPERTY is written regardless of specifier.
  • Storing live pointers — actor/object pointers are meaningless after a reload; store IDs or soft paths and re-resolve them in BeginPlay.
  • Sync save on large data during active gameplay — causes a visible hitch; use async.
  • No version field — painful migrations; add one at project start.
  • Cast without null-check after LoadGameFromSlot — crashes on missing or corrupt saves.
  • DoesSaveGameExist not checked before load — not strictly required (load returns null on missing), but checking first lets you distinguish "no save" from "corrupt save".
  • Mismatched slot name / UserIndex — save and load must use the exact same pair.
  • UPROPERTY(Transient) fields — explicitly excluded from all serialization; use this for cache/derived data you recompute on load.

Version notes

  • ULocalPlayerSaveGame arrived in UE 5.3 (it lives in SaveGame.h, not its own header); UAsyncActionHandleSaveGame is much older (UE 4.x). Earlier code uses only USaveGame + the UGameplayStatics free functions.
  • SaveGameToMemory / LoadGameFromMemory / SaveDataToSlot / LoadDataFromSlot are available as of UE 5.3 for in-memory and two-phase save flows.
  • The ISaveGameSystem platform layer is stable across UE5; on PC it writes .sav files to <Project>/Saved/SaveGames/. Console platforms swap in platform-specific implementations.

References & source material

Engine source (UE 5.8, under Engine/Source/):

  • Runtime/Engine/Classes/GameFramework/SaveGame.h — USaveGame (abstract, Blueprintable); ULocalPlayerSaveGame with versioning hooks:47-226.
  • Runtime/Engine/Classes/Kismet/GameplayStatics.h — CreateSaveGameObject:1124, SaveGameToMemory:1134, SaveDataToSlot:1143, AsyncSaveGameToSlot:1155, SaveGameToSlot:1167, DoesSaveGameExist:1175, LoadGameFromMemory:1182, LoadDataFromSlot:1191, AsyncLoadGameFromSlot:1202, LoadGameFromSlot:1211, DeleteGameInSlot:1231. Delegates FAsyncSaveGameToSlotDelegate / FAsyncLoadGameFromSlotDelegate:43-47.
  • Runtime/Engine/Public/SaveGameSystem.h — ISaveGameSystem interface: SaveGame, LoadGame, DeleteGame, DoesSaveGameExist, async variants; FGenericSaveGameSystem (writes Saved/SaveGames/<Name>.sav).
  • Runtime/CoreUObject/Public/UObject/ObjectMacros.h — CPF_SaveGame flag:458; SaveGame specifier keyword:1194 — gates serialization when ArIsSaveGame is set.
  • Runtime/Core/Public/Serialization/Archive.h — ArIsSaveGame bitfield:942; IsSaveGame() accessor:659-662.
  • Runtime/CoreUObject/Public/Serialization/ObjectAndNameAsStringProxyArchive.h — FObjectAndNameAsStringProxyArchive: serializes UObject* and FName as strings; wrap around FMemoryWriter/FMemoryReader for actor state capture.
  • Runtime/Core/Public/Serialization/MemoryWriter.h — FMemoryWriter (32-bit index):100-106.
  • Runtime/Core/Public/Serialization/MemoryReader.h — FMemoryReader:16-69.

Official docs (UE 5.8, verified live):

Deep-dive references in this skill:

Related skills: subsystems, gameplay-framework, data-driven-design, core-types-and-containers.