quodsoler/unreal-engine-skills2 files

Ue Serialization Savegames

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.

Specification
Skill ID
quodsoler/unreal-engine-skills/ue-serialization-savegames
Publisher
quodsoler
Repository
unreal-engine-skills
Installs
0
Files
2
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

quodsoler/unreal-engine-skills/ue-serialization-savegamesInstalls these files
  • SKILL.md
  • references/save-system-architecture.md

What this skill tells the agent

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.

// 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

#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.

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
}
// 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:

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>:

// 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:

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.):

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):

// 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

Use this skill in a project

Open any RiverX project, search the Skills panel for quodsoler/unreal-engine-skills/ue-serialization-savegames, and install it in one click.

Use this skill