Claude
Skills
Sign in
Back

ue-serialization-savegames

Included with Lifetime
$97 forever

Use when implementing save/load systems, player progress persistence, or data serialization in Unreal Engine. Triggers on: save game, USaveGame, FArchive, serialization, SaveGameToSlot, config, persist data, save file, load game. See references/save-system-architecture.md for full slot management and multi-user patterns.

General

What this skill does


# UE Serialization & Save Games

You are an expert in Unreal Engine's serialization and save game systems. You implement save/load pipelines using `USaveGame`, `FArchive`, config files, and versioning so player progress persists correctly across sessions and game updates.

---

## Step 1: Read Project Context

Read `.agents/ue-project-context.md` before giving any recommendations. You need:
- Engine version (UE 5.0+ has `ULocalPlayerSaveGame`; earlier versions differ)
- Module names (the save system lives in a specific module)
- Target platforms (console vs. PC save paths and user indices differ)
- Whether multiplayer is in scope (server-authoritative vs. client-local saves)

If the file does not exist, ask the user to run `/ue-project-context` first.

---

## Step 2: Gather Requirements

Ask before writing code:
1. **Save complexity**: Simple key/value data, or complex world state with hundreds of objects?
2. **Data types**: Primitives, nested structs, asset references (soft vs. hard)?
3. **Versioning needs**: Live game with future patches? Old saves must keep working?
4. **Multiple save slots**: How many? Does each player/user get their own?
5. **Async requirement**: Can save/load stall the game thread, or must it be background?

---

## Step 3: USaveGame Subclass

`USaveGame` is an abstract `UObject` from `GameFramework/SaveGame.h`. Subclass it and mark fields with `UPROPERTY(SaveGame)` for automatic tagged serialization by `UGameplayStatics`.

```cpp
// MyGameSaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MyGameSaveGame.generated.h"

USTRUCT(BlueprintType)
struct FInventoryItemData
{
    GENERATED_BODY() // Required — missing GENERATED_BODY() breaks struct serialization silently

    UPROPERTY(SaveGame) FName  ItemID;
    UPROPERTY(SaveGame) int32  Quantity = 0;
    UPROPERTY(SaveGame) bool   bIsEquipped = false;
};

UCLASS(BlueprintType)
class MYGAME_API UMyGameSaveGame : public USaveGame
{
    GENERATED_BODY()
public:
    UPROPERTY(SaveGame) int32   SaveVersion = 0;      // Always include a version field
    UPROPERTY(SaveGame) float   PlayerHealth = 100.f;
    UPROPERTY(SaveGame) int32   PlayerLevel = 1;
    UPROPERTY(SaveGame) FVector LastCheckpointLocation = FVector::ZeroVector;
    UPROPERTY(SaveGame) FString PlayerDisplayName;
    UPROPERTY(SaveGame) float   TotalPlayTimeSeconds = 0.f;
    UPROPERTY(SaveGame) TArray<FInventoryItemData>   InventoryItems;
    UPROPERTY(SaveGame) TMap<FName, int32>            AbilityLevels;
    // TSet<FName> is also supported in UPROPERTY(SaveGame) fields and serializes/deserializes automatically.

    // Asset references: FSoftObjectPath stores a string path — safe across saves
    // Never use raw UObject* or hard TObjectPtr<> to content assets in save data
    UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeaponPath;
};
```

### Saving and Loading

```cpp
#include "Kismet/GameplayStatics.h"

static const FString SlotName  = TEXT("MainSave");
static constexpr int32 UserIdx = 0; // Always 0 on PC; use GetPlatformUserIndex() on console

// Create the object first, populate its fields, then save
UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
SaveGame->PlayerHealth = 75.f;
// Then pass SaveGame to SaveGameToSlot / AsyncSaveGameToSlot below

// Sync save (blocks game thread — avoid in gameplay)
bool bSaved = UGameplayStatics::SaveGameToSlot(SaveData, SlotName, UserIdx);

// Async save (preferred — does not block)
FAsyncSaveGameToSlotDelegate OnSaved;
OnSaved.BindUObject(this, &USaveManager::OnAsyncSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot(SaveData, SlotName, UserIdx, OnSaved);

// Load
if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIdx))
{
    UMyGameSaveGame* Save = Cast<UMyGameSaveGame>(
        UGameplayStatics::LoadGameFromSlot(SlotName, UserIdx));
}

// Async load
FAsyncLoadGameFromSlotDelegate OnLoaded;
OnLoaded.BindUObject(this, &USaveManager::OnAsyncLoadComplete);
UGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIdx, OnLoaded);

// Delete
UGameplayStatics::DeleteGameInSlot(SlotName, UserIdx);
```

---

## Step 4: ULocalPlayerSaveGame (UE 5.0+)

`ULocalPlayerSaveGame` ties a save to a specific local player, tracks versioning via `GetLatestDataVersion()`, and provides `HandlePostLoad()` for migrations.

```cpp
UCLASS()
class MYGAME_API UMyLocalPlayerSave : public ULocalPlayerSaveGame
{
    GENERATED_BODY()
public:
    virtual int32 GetLatestDataVersion() const override { return 3; }
    virtual void  HandlePostLoad() override;

    UPROPERTY(SaveGame) TMap<FName, int32> UnlockedAbilities;
};

void UMyLocalPlayerSave::HandlePostLoad()
{
    Super::HandlePostLoad();
    const int32 Ver = GetSavedDataVersion(); // version when last saved

    if (Ver < 2) { UnlockedAbilities.Add(TEXT("Dash"), 1); }
    // Ver < 3 migrations go here
}
```

```cpp
// Load or create (sync)
UMyLocalPlayerSave* Save = ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
    UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"));

// Load or create (async)
ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
    UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"),
    FOnLocalPlayerSaveGameLoadedNative::CreateUObject(this, &AMyPC::OnSaveLoaded));

// Save back
Save->AsyncSaveGameToSlotForLocalPlayer(); // async (preferred)
Save->SaveGameToSlotForLocalPlayer();      // sync
```

---

## Step 5: FArchive and Custom Serialization

`FArchive` (from `Serialization/Archive.h`) is the base for all UE serialization. Key API:

```cpp
Ar.IsLoading()    // true when deserializing — same operator<< handles both directions
Ar.IsSaving()     // true when serializing to output
Ar.IsError()      // true after any read/write failure — always check before continuing
Ar.Tell()         // current position (int64); -1 if not seekable
Ar.CustomVer(Key) // returns the registered version number for a FGuid key
```

### FMemoryWriter and FMemoryReader

`FMemoryWriter`/`FMemoryReader` (from `Serialization/MemoryWriter.h` / `MemoryReader.h`) serialize to/from `TArray<uint8>`:

```cpp
// Serialize to bytes
TArray<uint8> OutBytes;
FMemoryWriter Writer(OutBytes, /*bIsPersistent=*/true);
int32 Version = 2;
Writer << Version;          // Serialize version header first — always
Writer << SomeData;
checkf(!Writer.IsError(), TEXT("Serialization failed"));

// Deserialize from bytes
FMemoryReader Reader(OutBytes, /*bIsPersistent=*/true);
int32 LoadedVersion = 0;
Reader << LoadedVersion;
if (LoadedVersion < 1 || Reader.IsError()) { /* corrupt data */ return; }
Reader << SomeData;
```

### FBufferArchive

`FBufferArchive` (from `Serialization/BufferArchive.h`) combines `FMemoryWriter` + `TArray<uint8>` — the object *is* the output buffer:

```cpp
FBufferArchive Buffer(/*bIsPersistent=*/true);
int32 Magic = 0x53415645; // 'SAVE'
Buffer << Magic;
Buffer << MyStruct;        // requires operator<< overload
TArray<uint8> Bytes = MoveTemp(Buffer); // FBufferArchive IS a TArray<uint8>
```

### Custom operator<< for Structs

Define `operator<<` to make a struct serializable via any `FArchive` (required when passing it to `FBufferArchive`, `FMemoryWriter`, etc.):

```cpp
FArchive& operator<<(FArchive& Ar, FMyCustomData& Data)
{
    Ar << Data.Name << Data.Value << Data.Timestamp;
    return Ar;
}
```

### Compressed Archives

For large saves, use `FArchiveSaveCompressedProxy` / `FArchiveLoadCompressedProxy` (from `Serialization/ArchiveSaveCompressedProxy.h`):

```cpp
// Compress
TArray<uint8> Compressed;
FArchiveSaveCompressedProxy Comp(Compressed, NAME_Zlib);
Comp.Serialize(RawData.GetData(), RawData.Num());
Comp.Flush();

// Decompress
FArchiveLoadCompressedProxy Decomp(Compressed, NAME_Zlib);
TArray<uint8> Raw;
Raw.SetNum(KnownUncompressedSize);
Decomp.Serialize(Raw.GetData(), Raw.Num());
```

### Custom Serialize() on UObject

Override `Serialize(FArchive& Ar)` for precise binary l

Related in General