| name | save-and-load |
| description | Persist and restore game data in Unreal C++ using the SaveGame system — define a USaveGame subclass with UPROPERTY members, create/save/load/delete named slots via UGameplayStatics (SaveGameToSlot, LoadGameFromSlot, AsyncSaveGameToSlot, AsyncLoadGameFromSlot, DoesSaveGameExist, DeleteGameInSlot), and serialize actor state into a byte buffer with FMemoryWriter/FMemoryReader and FObjectAndNameAsStringProxyArchive. Covers the SaveGame UPROPERTY specifier and ArIsSaveGame archive flag for selective actor serialization, the ULocalPlayerSaveGame subclass for per-user saves, save versioning and migration, ISaveGameSystem platform abstraction, and design rules for what belongs in a save. Use when implementing save/load, persisting progress, inventory, or settings, handling multiple save slots or user profiles, serializing dynamic actor state, migrating old saves, or troubleshooting missing fields and null returns on load. |
| metadata | {"engine-version":"5.8","category":"systems"} |
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
#pragma once
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
UCLASS()
class MYGAME_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY() int32 SaveVersion = 1;
UPROPERTY() FString PlayerName;
UPROPERTY() int32 PlayerLevel = 1;
UPROPERTY() FTransform LastCheckpoint;
UPROPERTY() TArray<FName> UnlockedAbilities;
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"
UMySaveGame* Save = Cast<UMySaveGame>(
UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
if (Save)
{
Save->PlayerLevel = CurrentLevel;
Save->PlayerName = PlayerName;
UGameplayStatics::SaveGameToSlot(Save, TEXT("Slot0"), 0);
}
if (UGameplayStatics::DoesSaveGameExist(TEXT("Slot0"), 0))
{
UMySaveGame* Loaded = Cast<UMySaveGame>(
UGameplayStatics::LoadGameFromSlot(TEXT("Slot0"), 0));
if (Loaded) { ApplySaveData(Loaded); }
}
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:
FAsyncSaveGameToSlotDelegate OnSaved;
OnSaved.BindUObject(this, &AMyGameMode::HandleSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot(Save, TEXT("Slot0"), 0, OnSaved);
FAsyncLoadGameFromSlotDelegate OnLoaded;
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.
UPROPERTY(SaveGame) bool bOpened = false;
UPROPERTY(SaveGame) int32 RemainingUses = 3;
UPROPERTY() float SomeTransientValue;
TArray<uint8> ActorBytes;
{
FMemoryWriter MemWriter(ActorBytes, true);
FObjectAndNameAsStringProxyArchive Ar(MemWriter, false);
Ar.ArIsSaveGame = true;
SomeActor->Serialize(Ar);
}
MySaveGame->DynamicActorData = ActorBytes;
{
FMemoryReader MemReader(MySaveGame->DynamicActorData, true);
FObjectAndNameAsStringProxyArchive Ar(MemReader, 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:
UMyPlayerSave* PS = Cast<UMyPlayerSave>(
ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
UMyPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot")));
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)
{
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.