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
USaveGamevs. 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 customUSTRUCTs are fine as long as all their members areUPROPERTY.
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
UPROPERTYis 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, overrideGetLatestDataVersion()and do fixup inHandlePostLoad. - 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
GameInstanceor a subsystem during a session; persist it to aUSaveGameslot at save points. - Game and graphics settings often fit better in
UGameUserSettings/ config.inifiles (ue-project-structure) — reserveUSaveGamefor gameplay state.
Gotchas
- Non-
UPROPERTYfields are invisible — the serializer cannot see bare C++ fields. UPROPERTY(SaveGame)does nothing on the SaveGame object itself — it gates selective actor serialization viaArIsSaveGame. On theUSaveGamesubclass every non-transientUPROPERTYis 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. DoesSaveGameExistnot 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
ULocalPlayerSaveGamearrived in UE 5.3 (it lives inSaveGame.h, not its own header);UAsyncActionHandleSaveGameis much older (UE 4.x). Earlier code uses onlyUSaveGame+ theUGameplayStaticsfree functions.SaveGameToMemory/LoadGameFromMemory/SaveDataToSlot/LoadDataFromSlotare available as of UE 5.3 for in-memory and two-phase save flows.- The
ISaveGameSystemplatform layer is stable across UE5; on PC it writes.savfiles 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);ULocalPlayerSaveGamewith 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. DelegatesFAsyncSaveGameToSlotDelegate/FAsyncLoadGameFromSlotDelegate:43-47.Runtime/Engine/Public/SaveGameSystem.h—ISaveGameSysteminterface:SaveGame,LoadGame,DeleteGame,DoesSaveGameExist, async variants;FGenericSaveGameSystem(writesSaved/SaveGames/<Name>.sav).Runtime/CoreUObject/Public/UObject/ObjectMacros.h—CPF_SaveGameflag:458;SaveGamespecifier keyword:1194 — gates serialization whenArIsSaveGameis set.Runtime/Core/Public/Serialization/Archive.h—ArIsSaveGamebitfield:942;IsSaveGame()accessor:659-662.Runtime/CoreUObject/Public/Serialization/ObjectAndNameAsStringProxyArchive.h—FObjectAndNameAsStringProxyArchive: serializesUObject*andFNameas strings; wrap aroundFMemoryWriter/FMemoryReaderfor 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):
- Saving and Loading Your Game — https://dev.epicgames.com/documentation/unreal-engine/saving-and-loading-your-game-in-unreal-engine
Deep-dive references in this skill:
- references/savegame-objects-and-slots.md — slot API internals, ISaveGameSystem platform layer, file locations, binary save/load helpers.
- references/serializing-actor-state.md — full multi-actor capture/restore pattern, UPROPERTY(SaveGame) mechanics, FArchive internals.
- references/versioning-and-migration.md — version strategy, migration patterns, ULocalPlayerSaveGame versioning, platform considerations.
Related skills: ue-subsystems, ue-gameplay-framework, ue-data-driven-design, ue-core-types-and-containers.