Skip to main content

💾 Lesson 7.3: Saving & Loading with SaveGame

Players expect to close the game and return to exactly where they left off. Unreal's SaveGame system makes this a few clean calls: define what to save as a USaveGame subclass, then write and read it to named slots. And it all rides on the reflection system you already understand.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define a USaveGame subclass holding the data to persist
  • Create, write, and read a save with UGameplayStatics
  • Check for and delete existing saves; use multiple slots
  • Explain what serializes and why UPROPERTY matters again
  • Structure a save/load flow that gathers and restores game state

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

The USaveGame Object

A save is a UObject whose UPROPERTYs are the saved data. Subclass USaveGame and add exactly what you want persisted.

// MySaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"

UCLASS()
class MYPROJECT_API UMySaveGame : public USaveGame
{
    GENERATED_BODY()

public:
    // Every UPROPERTY here is written to disk and read back.
    UPROPERTY(VisibleAnywhere, Category = "Save")
    int32 PlayerLevel = 1;

    UPROPERTY(VisibleAnywhere, Category = "Save")
    float PlayerHealth = 100.0f;

    UPROPERTY(VisibleAnywhere, Category = "Save")
    FVector PlayerLocation = FVector::ZeroVector;

    UPROPERTY(VisibleAnywhere, Category = "Save")
    TArray<FName> InventoryItems;

    UPROPERTY(VisibleAnywhere, Category = "Save")
    FString SlotDisplayName;
};

📖 The reflection system, one more payoff

Saving works by serializing the object's UPROPERTYs — the same reflection data from Lesson 2.2 that powers the editor and networking. This is the fourth payoff promised in that lesson's diagram (editor, Blueprint, serialization, networking). A member without UPROPERTY simply won't be saved — the reflection system can't see it, so it isn't written.

Writing a Save

Create the object, fill it, and write it to a named slot with UGameplayStatics.

#include "Kismet/GameplayStatics.h"
#include "MySaveGame.h"

void AMyGameMode::SaveGame(const FString& SlotName)
{
    // Create a fresh save object of our class.
    UMySaveGame* Save = Cast<UMySaveGame>(
        UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
    if (!Save) { return; }

    // Gather the state we want to persist.
    if (APawn* Player = UGameplayStatics::GetPlayerPawn(this, 0))
    {
        Save->PlayerLocation = Player->GetActorLocation();
        if (UHealthComponent* HC = Player->FindComponentByClass<UHealthComponent>())
        {
            Save->PlayerHealth = HC->GetCurrentHealth();
        }
    }
    Save->PlayerLevel = CurrentLevel;

    // Write to disk. UserIndex is 0 for single-player.
    const bool bOK = UGameplayStatics::SaveGameToSlot(Save, SlotName, /*UserIndex=*/0);
    UE_LOG(LogTemp, Log, TEXT("Save '%s' %s"), *SlotName, bOK ? TEXT("OK") : TEXT("FAILED"));
}

✅ Gather, then write

The pattern is always: create the save object → gather live state into it → SaveGameToSlot. Notice the state-gathering reuses everything — FindComponentByClass (3.5), the health component (3.3). A save object is just a snapshot bag; your job is filling it from the running game.

Reading a Save

Loading mirrors saving: read the object from the slot, then apply its data back to the game.

void AMyGameMode::LoadGame(const FString& SlotName)
{
    // Nothing to load if the slot doesn't exist.
    if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0)) { return; }

    UMySaveGame* Save = Cast<UMySaveGame>(
        UGameplayStatics::LoadGameFromSlot(SlotName, 0));
    if (!Save) { return; }

    // Apply the loaded data back to the live game.
    CurrentLevel = Save->PlayerLevel;
    if (APawn* Player = UGameplayStatics::GetPlayerPawn(this, 0))
    {
        Player->SetActorLocation(Save->PlayerLocation);
        if (UHealthComponent* HC = Player->FindComponentByClass<UHealthComponent>())
        {
            HC->SetHealth(Save->PlayerHealth);   // the guarded setter from Lesson 6.2
        }
    }
}

⚠️ Always guard the load

LoadGameFromSlot returns nullptr if the slot is missing or corrupt, so check DoesSaveGameExist first and Cast defensively. Loading a save whose class changed (you added/removed fields) is usually tolerated — missing fields default, extra saved fields are ignored — but a version mismatch can surprise you, so consider a version int32 in your save for migrations.

Slots, Existence & Deletion

Saves are addressed by a slot name (a string) plus a user index. Different slot names give you multiple save files — quicksave, autosave, manual slots.

// Multiple named slots:
SaveGame(TEXT("AutoSave"));
SaveGame(TEXT("QuickSave"));
SaveGame(TEXT("Slot_01"));

// Does a slot exist? (e.g. to enable a "Continue" button)
const bool bHasSave = UGameplayStatics::DoesSaveGameExist(TEXT("AutoSave"), 0);

// Delete a slot:
UGameplayStatics::DeleteGameInSlot(TEXT("Slot_01"), 0);
graph LR Game["Running game"] -->|"gather → SaveGameToSlot('Slot_01')"| Disk["Slot_01 on disk"] Disk -->|"LoadGameFromSlot('Slot_01') → apply"| Game
Figure 1: The round trip. Gather state into a USaveGame, write it to a named slot; later read it back and apply. Each slot name is an independent save file.

✅ Long saves can go async

For large saves, the synchronous calls can hitch a frame. Unreal also offers async variants (AsyncSaveGameToSlot / AsyncLoadGameFromSlot) that take a delegate called on completion — pairing nicely with the async concepts in Module 11. For small saves the synchronous calls are fine.

What Serializes

The rules mirror what crosses the Blueprint boundary (Lesson 6.3), because both ride reflection:

Saves cleanly ✅Needs care ⚠️
Primitives, FString/FName/FTextRaw UObject* — saves a reference, not the object's data
FVector, structs of savable fieldsActor pointers — usually don't survive a fresh session
TArray/TMap of savable typesNon-UPROPERTY members — never saved
TSubclassOf, enumsLive gameplay object graphs — save IDs, re-spawn on load

📖 Save data, not live objects

Don't try to save pointers to live actors — on load, those actors don't exist yet. Instead save the data needed to recreate them: an enemy's FName id and transform, then re-spawn it on load using the DataTable lookup from Lesson 7.1. Think of a save as a recipe to rebuild the world, not a photograph of live memory.

Hands-on Exercise & Quiz

🏋️ Exercise: A checkpoint system

Objective: Implement a minimal autosave.

  1. Define UCheckpointSave : public USaveGame with the player's location, health, and an int32 CheckpointIndex.
  2. On a checkpoint trigger (overlap, Lesson 5.1), gather state and SaveGameToSlot(TEXT("Checkpoint")).
  3. On game start, if DoesSaveGameExist(TEXT("Checkpoint")), load and apply it.
  4. Why do you save the player's location rather than a pointer to the player actor?
✅ Answer

Because the player actor from the previous session no longer exists when you load — a saved pointer would be meaningless. Saving the location (plain data) lets you place the freshly-spawned player pawn back where they were. Save the data to reconstruct state, never live object references.

🎯 Quick Quiz

Question 1: What determines which data gets saved in a USaveGame?

Question 2: Before LoadGameFromSlot, you should:

Question 3: To persist enemies present in a level, you should save:

Summary

🎉 Key Takeaways

  • A save is a USaveGame subclass whose UPROPERTY members are the persisted data — saving rides the reflection system.
  • Write: CreateSaveGameObject → gather state → SaveGameToSlot(Save, SlotName, UserIndex).
  • Read: DoesSaveGameExistLoadGameFromSlotCast → apply data back (guard every step).
  • Slots are named strings; use DoesSaveGameExist/DeleteGameInSlot; async variants exist for large saves.
  • Save data to rebuild the world (ids, transforms), not live object pointers.

📚 Additional Resources

🚀 What's Next?

Data, config, saves — all persisted. The module's finale is about runtime global systems: Subsystems, Unreal's clean, lifetime-managed replacement for singletons — the right home for a save manager, an inventory service, or a music director.

🎉 Lesson complete!

Progress persists. Now let's give your systems a proper home.