UE5 Save Game System: Complete Implementation Guide
From basic SaveGame objects to production-ready systems with async operations, versioning, and cloud sync. Everything you need to know.
Understanding Unreal's Save System
Unreal Engine provides a straightforward save system built on the USaveGame class. At its core, saving is just serializing a UObject to disk and loading is deserializing it back. But production save systems need much more: async operations, multiple slots, versioning, validation, and graceful error handling.
This guide covers the complete picture—from basic implementation to the patterns used in shipped games.
Basic SaveGame Setup
Creating the SaveGame Class
In C++, create a class inheriting from USaveGame:
// MySaveGame.h
#pragma once
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
UCLASS()
class UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame)
FString PlayerName;
UPROPERTY(SaveGame)
int32 PlayerLevel;
UPROPERTY(SaveGame)
FVector LastCheckpointLocation;
UPROPERTY(SaveGame)
TArray<FInventoryItem> InventoryItems;
UPROPERTY(SaveGame)
TMap<FName, bool> CompletedQuests;
};In Blueprint, right-click in Content Browser → Blueprint Class → search for SaveGame → create your save game Blueprint. Add variables with "SaveGame" flag enabled (check the advanced properties).
Synchronous Save and Load
The simplest approach—fine for small saves during non-critical moments:
// Saving
UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(
UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())
);
// Populate data
SaveGameInstance->PlayerName = CurrentPlayerName;
SaveGameInstance->PlayerLevel = CurrentLevel;
SaveGameInstance->LastCheckpointLocation = GetActorLocation();
// Write to disk
UGameplayStatics::SaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0);
// Loading
UMySaveGame* LoadedGame = Cast<UMySaveGame>(
UGameplayStatics::LoadGameFromSlot(TEXT("Slot1"), 0)
);
if (LoadedGame)
{
// Restore state
CurrentPlayerName = LoadedGame->PlayerName;
CurrentLevel = LoadedGame->PlayerLevel;
}This blocks the game thread. For small saves (a few KB), the hitch is imperceptible. For larger saves or when saving during gameplay, use async.
Async Save and Load
Async operations prevent hitches. UE5 provides delegates for completion callbacks.
Async Saving
void UMySaveManager::AsyncSaveGame(UMySaveGame* SaveGame, const FString& SlotName)
{
FAsyncSaveGameToSlotDelegate SaveDelegate;
SaveDelegate.BindUObject(this, &UMySaveManager::OnSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot(
SaveGame,
SlotName,
0,
SaveDelegate
);
}
void UMySaveManager::OnSaveComplete(const FString& SlotName, int32 UserIndex, bool bSuccess)
{
if (bSuccess)
{
UE_LOG(LogSave, Log, TEXT("Save complete: %s"), *SlotName);
}
else
{
UE_LOG(LogSave, Error, TEXT("Save failed: %s"), *SlotName);
// Handle failure - retry, notify user, etc.
}
}Async Loading
void UMySaveManager::AsyncLoadGame(const FString& SlotName)
{
FAsyncLoadGameFromSlotDelegate LoadDelegate;
LoadDelegate.BindUObject(this, &UMySaveManager::OnLoadComplete);
UGameplayStatics::AsyncLoadGameFromSlot(
SlotName,
0,
LoadDelegate
);
}
void UMySaveManager::OnLoadComplete(
const FString& SlotName,
int32 UserIndex,
USaveGame* LoadedGame
)
{
UMySaveGame* MySave = Cast<UMySaveGame>(LoadedGame);
if (MySave)
{
ApplySaveData(MySave);
}
else
{
HandleLoadFailure(SlotName);
}
}Blueprint Async Nodes
In Blueprint, use the "Async Save Game to Slot" and "Async Load Game from Slot" nodes. They have completion pins that fire when the operation finishes.
Data Serialization Patterns
What Serializes Automatically
Properties marked with UPROPERTY(SaveGame) serialize automatically:
- Primitive types (int, float, bool, FString, FName, FText)
- Structs containing serializable types
- Arrays and Maps of serializable types
- Soft object references (TSoftObjectPtr)
- Class references (TSubclassOf)
What Doesn't Serialize
- Raw pointers (UObject*, AActor*)
- Hard object references (point to runtime objects)
- Delegates
- Transient properties
Handling Object References
You can't save pointers to actors—they won't exist when you load. Instead, save data needed to recreate or find them:
// Instead of:
UPROPERTY()
AActor* TrackedEnemy; // Won't work
// Use:
UPROPERTY(SaveGame)
FGuid TrackedEnemyGuid; // Save the GUID
// Or for spawned actors:
UPROPERTY(SaveGame)
TSubclassOf<AActor> EnemyClass; // What to spawn
UPROPERTY(SaveGame)
FTransform EnemyTransform; // Where to spawn
UPROPERTY(SaveGame)
FEnemySaveData EnemyState; // How to configureCustom Struct Serialization
For complex data, define custom structs:
USTRUCT(BlueprintType)
struct FActorSaveData
{
GENERATED_BODY()
UPROPERTY(SaveGame)
TSubclassOf<AActor> ActorClass;
UPROPERTY(SaveGame)
FTransform Transform;
UPROPERTY(SaveGame)
FGuid UniqueId;
UPROPERTY(SaveGame)
TMap<FName, FString> Properties;
};The Properties map allows flexible key-value storage for actor-specific data without modifying the struct for each actor type.
Save Slot Management
Multiple Save Slots
Most games need multiple save slots. Create a manager:
UCLASS()
class USaveSlotManager : public UObject
{
GENERATED_BODY()
public:
// Get metadata for all slots
TArray<FSaveSlotInfo> GetAllSaveSlots();
// Check if slot exists
bool DoesSaveExist(const FString& SlotName);
// Delete a save
bool DeleteSave(const FString& SlotName);
// Get next available slot name
FString GetNextAvailableSlot();
// Copy a save to another slot
bool CopySave(const FString& SourceSlot, const FString& DestSlot);
};Save Metadata
Display save information in the UI without loading full save data:
USTRUCT(BlueprintType)
struct FSaveSlotInfo
{
GENERATED_BODY()
UPROPERTY()
FString SlotName;
UPROPERTY()
FDateTime SaveTime;
UPROPERTY()
FString LevelName;
UPROPERTY()
int32 PlaytimeMinutes;
UPROPERTY()
UTexture2D* Screenshot;
};Store metadata in a separate, smaller save file that loads quickly for the save/load menu.
Save Versioning
Games update. Save format changes. You need versioning to handle old saves.
Version Number Approach
UCLASS()
class UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
static const int32 CURRENT_VERSION = 3;
UPROPERTY(SaveGame)
int32 SaveVersion;
// Called after loading to migrate old saves
void MigrateFromOldVersion();
};Migration Logic
void UMySaveGame::MigrateFromOldVersion()
{
// Version 1 -> 2: Added inventory weight system
if (SaveVersion < 2)
{
for (auto& Item : InventoryItems)
{
Item.Weight = GetDefaultWeight(Item.ItemId);
}
}
// Version 2 -> 3: Renamed quest IDs
if (SaveVersion < 3)
{
TMap<FName, bool> MigratedQuests;
for (auto& Quest : CompletedQuests)
{
FName NewId = MigrateQuestId(Quest.Key);
MigratedQuests.Add(NewId, Quest.Value);
}
CompletedQuests = MoveTemp(MigratedQuests);
}
SaveVersion = CURRENT_VERSION;
}Handling Incompatible Saves
Sometimes migration isn't possible. Define a minimum supported version:
static const int32 MIN_SUPPORTED_VERSION = 2;
bool UMySaveGame::IsCompatible()
{
return SaveVersion >= MIN_SUPPORTED_VERSION;
}Autosave Implementation
Trigger Points
Autosave at logical points, not on timers alone:
- Checkpoint reached
- Level transition
- After significant progress (quest complete, boss killed)
- Before dangerous encounters
- On game pause (before player leaves)
Autosave Manager
UCLASS()
class UAutosaveManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
void RequestAutosave(EAutosaveReason Reason);
private:
// Prevent rapid autosaves
float MinTimeBetweenAutosaves = 30.0f;
float LastAutosaveTime = 0.0f;
// Rotating autosave slots
int32 CurrentAutosaveSlot = 0;
int32 MaxAutosaveSlots = 3;
void DoAutosave();
};UI Feedback
Always show when autosaving—players get anxious if they don't know their progress is saved. Display an icon or notification during save operations.
Error Handling and Recovery
Save Validation
Validate saves on load:
bool UMySaveGame::Validate()
{
// Check for corruption indicators
if (PlayerLevel < 0 || PlayerLevel > MAX_LEVEL)
return false;
if (PlaytimeMinutes < 0)
return false;
// Verify required data exists
if (CurrentLevelName.IsEmpty())
return false;
return true;
}Backup Saves
Keep backup copies in case the primary save corrupts:
void SaveWithBackup(UMySaveGame* SaveGame, const FString& SlotName)
{
// Move current save to backup
FString BackupSlot = SlotName + "_backup";
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
// Load then save to backup slot
USaveGame* Current = UGameplayStatics::LoadGameFromSlot(SlotName, 0);
UGameplayStatics::SaveGameToSlot(Current, BackupSlot, 0);
}
// Save new data
UGameplayStatics::SaveGameToSlot(SaveGame, SlotName, 0);
}Recovery Flow
UMySaveGame* LoadWithRecovery(const FString& SlotName)
{
// Try primary save
UMySaveGame* Save = Cast<UMySaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, 0)
);
if (Save && Save->Validate())
return Save;
// Try backup
UE_LOG(LogSave, Warning, TEXT("Primary save invalid, trying backup"));
FString BackupSlot = SlotName + "_backup";
Save = Cast<UMySaveGame>(
UGameplayStatics::LoadGameFromSlot(BackupSlot, 0)
);
if (Save && Save->Validate())
return Save;
// No valid save found
UE_LOG(LogSave, Error, TEXT("No valid save found for slot: %s"), *SlotName);
return nullptr;
}Platform Considerations
Console Save Systems
Consoles have platform-specific save requirements:
- PlayStation: Uses Save Data system with size limits per title
- Xbox: Connected Storage API for cloud sync
- Switch: Strict save data size limits
Unreal abstracts most of this, but you need to handle platform-specific UI (save data management screens) and respect size limits.
Cloud Saves
PC platforms often sync saves to cloud (Steam Cloud, Epic Cloud). Enable this in your platform settings. The save/load API remains the same—the platform handles sync.
Save Size Optimization
Large saves cause issues on all platforms. Minimize size:
- Don't save data that can be derived (recalculate on load)
- Use indices into data tables instead of full item data
- Compress arrays of similar data
- Store deltas from defaults instead of full state
Testing Save Systems
Test Cases
- Save and immediate load
- Save, quit, restart, load
- Load saves from previous versions
- Handle corrupted save files
- Save during level transition
- Multiple rapid saves
- Save with full/near-full storage
- Cloud sync conflicts
Debug Commands
// Console commands for testing
UFUNCTION(Exec)
void DebugSaveGame();
UFUNCTION(Exec)
void DebugLoadGame(const FString& SlotName);
UFUNCTION(Exec)
void DebugCorruptSave(const FString& SlotName);
UFUNCTION(Exec)
void DebugListSaves();Summary
A production save system needs:
- Async operations to avoid hitches
- Versioning for backward compatibility
- Validation to detect corruption
- Backups for recovery
- Clear UI feedback
- Platform-appropriate behavior
Start simple—synchronous saves work fine for prototyping. Add async operations, versioning, and backup systems as you approach release. The patterns here scale from indie games to AAA titles.