💾 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
USaveGamesubclass 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
UPROPERTYmatters 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);
✅ 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/FText | Raw UObject* — saves a reference, not the object's data |
FVector, structs of savable fields | Actor pointers — usually don't survive a fresh session |
TArray/TMap of savable types | Non-UPROPERTY members — never saved |
TSubclassOf, enums | Live 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.
- Define
UCheckpointSave : public USaveGamewith the player's location, health, and anint32 CheckpointIndex. - On a checkpoint trigger (overlap, Lesson 5.1), gather state and
SaveGameToSlot(TEXT("Checkpoint")). - On game start, if
DoesSaveGameExist(TEXT("Checkpoint")), load and apply it. - 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
USaveGamesubclass whoseUPROPERTYmembers are the persisted data — saving rides the reflection system. - Write:
CreateSaveGameObject→ gather state →SaveGameToSlot(Save, SlotName, UserIndex). - Read:
DoesSaveGameExist→LoadGameFromSlot→Cast→ 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.