$npx -y skills add quodsoler/unreal-engine-skills --skill ue-serialization-savegamesUse 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 ful
| 1 | # UE Serialization & Save Games |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Step 1: Read Project Context |
| 8 | |
| 9 | Read `.agents/ue-project-context.md` before giving any recommendations. You need: |
| 10 | - Engine version (UE 5.0+ has `ULocalPlayerSaveGame`; earlier versions differ) |
| 11 | - Module names (the save system lives in a specific module) |
| 12 | - Target platforms (console vs. PC save paths and user indices differ) |
| 13 | - Whether multiplayer is in scope (server-authoritative vs. client-local saves) |
| 14 | |
| 15 | If the file does not exist, ask the user to run `/ue-project-context` first. |
| 16 | |
| 17 | --- |
| 18 | |
| 19 | ## Step 2: Gather Requirements |
| 20 | |
| 21 | Ask before writing code: |
| 22 | 1. **Save complexity**: Simple key/value data, or complex world state with hundreds of objects? |
| 23 | 2. **Data types**: Primitives, nested structs, asset references (soft vs. hard)? |
| 24 | 3. **Versioning needs**: Live game with future patches? Old saves must keep working? |
| 25 | 4. **Multiple save slots**: How many? Does each player/user get their own? |
| 26 | 5. **Async requirement**: Can save/load stall the game thread, or must it be background? |
| 27 | |
| 28 | --- |
| 29 | |
| 30 | ## Step 3: USaveGame Subclass |
| 31 | |
| 32 | `USaveGame` is an abstract `UObject` from `GameFramework/SaveGame.h`. Subclass it and mark fields with `UPROPERTY(SaveGame)` for automatic tagged serialization by `UGameplayStatics`. |
| 33 | |
| 34 | ```cpp |
| 35 | // MyGameSaveGame.h |
| 36 | #pragma once |
| 37 | #include "CoreMinimal.h" |
| 38 | #include "GameFramework/SaveGame.h" |
| 39 | #include "MyGameSaveGame.generated.h" |
| 40 | |
| 41 | USTRUCT(BlueprintType) |
| 42 | struct FInventoryItemData |
| 43 | { |
| 44 | GENERATED_BODY() // Required — missing GENERATED_BODY() breaks struct serialization silently |
| 45 | |
| 46 | UPROPERTY(SaveGame) FName ItemID; |
| 47 | UPROPERTY(SaveGame) int32 Quantity = 0; |
| 48 | UPROPERTY(SaveGame) bool bIsEquipped = false; |
| 49 | }; |
| 50 | |
| 51 | UCLASS(BlueprintType) |
| 52 | class MYGAME_API UMyGameSaveGame : public USaveGame |
| 53 | { |
| 54 | GENERATED_BODY() |
| 55 | public: |
| 56 | UPROPERTY(SaveGame) int32 SaveVersion = 0; // Always include a version field |
| 57 | UPROPERTY(SaveGame) float PlayerHealth = 100.f; |
| 58 | UPROPERTY(SaveGame) int32 PlayerLevel = 1; |
| 59 | UPROPERTY(SaveGame) FVector LastCheckpointLocation = FVector::ZeroVector; |
| 60 | UPROPERTY(SaveGame) FString PlayerDisplayName; |
| 61 | UPROPERTY(SaveGame) float TotalPlayTimeSeconds = 0.f; |
| 62 | UPROPERTY(SaveGame) TArray<FInventoryItemData> InventoryItems; |
| 63 | UPROPERTY(SaveGame) TMap<FName, int32> AbilityLevels; |
| 64 | // TSet<FName> is also supported in UPROPERTY(SaveGame) fields and serializes/deserializes automatically. |
| 65 | |
| 66 | // Asset references: FSoftObjectPath stores a string path — safe across saves |
| 67 | // Never use raw UObject* or hard TObjectPtr<> to content assets in save data |
| 68 | UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeaponPath; |
| 69 | }; |
| 70 | ``` |
| 71 | |
| 72 | ### Saving and Loading |
| 73 | |
| 74 | ```cpp |
| 75 | #include "Kismet/GameplayStatics.h" |
| 76 | |
| 77 | static const FString SlotName = TEXT("MainSave"); |
| 78 | static constexpr int32 UserIdx = 0; // Always 0 on PC; use GetPlatformUserIndex() on console |
| 79 | |
| 80 | // Create the object first, populate its fields, then save |
| 81 | UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())); |
| 82 | SaveGame->PlayerHealth = 75.f; |
| 83 | // Then pass SaveGame to SaveGameToSlot / AsyncSaveGameToSlot below |
| 84 | |
| 85 | // Sync save (blocks game thread — avoid in gameplay) |
| 86 | bool bSaved = UGameplayStatics::SaveGameToSlot(SaveData, SlotName, UserIdx); |
| 87 | |
| 88 | // Async save (preferred — does not block) |
| 89 | FAsyncSaveGameToSlotDelegate OnSaved; |
| 90 | OnSaved.BindUObject(this, &USaveManager::OnAsyncSaveComplete); |
| 91 | UGameplayStatics::AsyncSaveGameToSlot(SaveData, SlotName, UserIdx, OnSaved); |
| 92 | |
| 93 | // Load |
| 94 | if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIdx)) |
| 95 | { |
| 96 | UMyGameSaveGame* Save = Cast<UMyGameSaveGame>( |
| 97 | UGameplayStatics::LoadGameFromSlot(SlotName, UserIdx)); |
| 98 | } |
| 99 | |
| 100 | // Async load |
| 101 | FAsyncLoadGameFromSlotDelegate OnLoaded; |
| 102 | OnLoaded.BindUObject(this, &USaveManager::OnAsyncLoadComplete); |
| 103 | UGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIdx, OnLoaded); |
| 104 | |
| 105 | // Delete |
| 106 | UGameplayStatics::DeleteGameInSlot(SlotName, UserIdx); |
| 107 | ``` |
| 108 | |
| 109 | --- |
| 110 | |
| 111 | ## Step 4: ULocalPlayerSaveGame (UE 5.0+) |
| 112 | |
| 113 | `ULocalPlayerSaveGame` ties a save to a specific local player, tracks versioning via `GetLatestDataVersion()`, and provides `HandlePostLoad()` for migrations. |
| 114 | |
| 115 | ```cpp |
| 116 | UCLASS() |
| 117 | class MYGAME_API UMyLocalPlayerSave : public ULocalPlayerSaveGame |
| 118 | { |
| 119 | GENERATED_BODY() |
| 120 | public: |
| 121 | virtual int32 GetLatestDataVersion() const override { return 3; } |
| 122 | virtual void HandlePostLoad() override; |