SystemsSeptember 12, 202615 min read

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 configure

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

Get early access to UnrealPilot →