Save and Load Game Data in Unreal Engine 5 Using C++
Saving and loading data is critical to any video game and in this post I go over the basic techniques of how to do this in C++.
Software Versions: Unreal Engine 5.5.0 | Rider 2024.3
Project Name: MyProject
This post will cover a basic example of saving actors in Unreal Engine 5 using the UPROPERTY
SaveGame
specifier. To keep things simple, player state will not be included and only one save slot titled SaveSlot01
will be used. Player state and multiple/dynamic save slots will have to be covered in a future post. I wouldn't have been able to accomplish this or understand C++ save logic without Tom Looman's save system post or Dream on a Stick's saves and concepts post save system posts, check them out for more details.
First, we need a new USaveGame
object, I called it MySaveGame
. It's good practice to add a USTRUCT
to organize the saved data and use the struct to create a TArray
of the saved data. The actor's name will be used as the unique identifier, this will suffice most casual cases. The TArray<uint8> ByteData;
is where the SaveGame
specifier properties will be saved. For this example I won't be utilizing the actor transform data, but it's great when you need to save and set an actor's location/rotation/scale. The .cpp
will be remain empty.
MySaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
USTRUCT()
struct FActorSaveData
{
GENERATED_BODY()
public:
UPROPERTY()
FName ActorName;
UPROPERTY()
FTransform ActorTransform;
UPROPERTY()
TArray<uint8> ByteData;
};
UCLASS()
class MYPROJECT_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY()
TArray<FActorSaveData> SavedActors;
};
There a variety of techniques to save data, however, using a subsystem with a save game interface appears to be the most modern approach. Let's create the interface.
Create a new interface, I called my SaveGameInterface
. This interface has two BlueprintNativeEvents
called onLoadGame
and OnResetData
, add and adjust per your project's requirements. These two functions will be called and implemented for each actor that inherits the SaveGameInterface
. The .cpp
file will remain empty.
SaveGameInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SaveGameInterface.generated.h"
// This class does not need to be modified.
UINTERFACE()
class USaveGameInterface : public UInterface
{
GENERATED_BODY()
};
class MYPROJECT_API ISaveGameInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
// Load game data from save file
UFUNCTION(BlueprintNativeEvent)
void OnLoadGame();
// Rest and save data to original state
UFUNCTION(BlueprintNativeEvent)
void OnResetData();
};
Now let's attack the Unreal subsystem. The subsystem will contain all the saving and loading logic needed for this example. Subsystems were added late in UE4's lifecycle and are considered great to use for save game logic. Subsystems automatically load with the game instance so their readily available. Visit Epic's official subsystem documentation for more information, I'll paste the first couple of sentences below directly from Epic's docs.
Subsystems in Unreal Engine (UE) are automatically instanced classes with managed lifetimes. These classes provide easy to use extension points, where the programmers can get Blueprint and Python exposure right away while avoiding the complexity of modifying or overriding engine classes.
Create a new class inheriting the UGameInstanceSubsystem
. The header file will contain void
functions for saving and loading along with a SaveGame
and a SlotName
variable property that we can reference through the .cpp
file. Furthermore, in this example, to keep things simple, I'm utilizing the Initialize
function to set default values on load so I don't crash the editor.
SavGameSubsystem.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "SaveGameSubsystem.generated.h"
class UMySaveGame;
UCLASS()
class MYPROJECT_API USaveGameSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UPROPERTY()
FString SaveSlotName;
UPROPERTY()
TObjectPtr<UMySaveGame> CurrentSaveGame;
UFUNCTION(BlueprintCallable)
void SaveGame();
UFUNCTION(BlueprintCallable)
void LoadGame();
UFUNCTION(BlueprintCallable)
void ResetData();
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
};
USaveGameSubsystem
's .cpp
file will start with the Initialize
function. Here, I'm simply setting the slot name and setting CurrentSaveGame
variable with UGameplayStatics::LoadGameFromSlot
or UGameplayStatics::CreateSaveGameObject
, typically this would be dynamic, but I wanted to keep things simple.
#include "SaveGameSubsystem.h"
#include "MySaveGame.h"
#include "SaveGameInterface.h"
#include "Kismet/GameplayStatics.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
void USaveGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
SaveSlotName = TEXT("SaveGame01");
if (UGameplayStatics::DoesSaveGameExist(SaveSlotName, 0))
{
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
}
else
{
CurrentSaveGame = CastChecked<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
}
}
Next we can look at the SaveGame
function. The function starts by cleaning out the SaveActors
array then it looks in the game world and grabs every actor that inherits the USaveGameInterface
class (we'll create the actors later in the post). The function then loops through the actors saving each actor's data our pre-defined struct. The most interesting part is how we handle the ByteData
, we first need to use a FMemoryWriter
then pass it through a FObjectAndNameAsStringProxyArchive
, set the the ArIsSaveGame
value to true to get the properties with the SaveGame
specifier, then the data is serialized and added to the SavedActors
TArray
. Once the for
loop is completed we use UGameplayStatics::SaveGameToSlot
to save the file. We are not using AsyncSaveGameToSlot
for this simple example, I'll have to address async saving and loading in a future post.
void USaveGameSubsystem::SaveGame()
{
CurrentSaveGame->SavedActors.Empty();
TArray<AActor*> SaveGameActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveGameInterface::StaticClass(), SaveGameActors);
for (AActor* Actor : SaveGameActors)
{
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetFName();
FMemoryWriter MyMemoryWriter(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MyMemoryWriter, true);
Ar.ArIsSaveGame = true;
Actor->Serialize(Ar);
CurrentSaveGame->SavedActors.Add(ActorData);
}
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SaveSlotName, 0);
}
Loading a game file is very similar to saving a file, but rather than using FMemoryWriter
, we use FMemoryReader
. We get the save file then loop through all the the world's actors that inherit our USaveGameInterface
. We use a nested for
loop to loop through our saved actors and match them to the world actor using our unique identifier that is their name. From inside the if
statement we can set values as necessary and use FMemoryReader
to read the saved data and pass it back to the actor with Actor->Serialize(Ar)
. Now, since the actor has been updated with the loaded data, we can now call an interface function so actor react to the new data loaded. We can call ISaveGameInterface::Execute_OnLoadGame(Actor)
and as long as the actor has OnLoadGame_Implementation
defined then it'll react to the call.
void USaveGameSubsystem::LoadGame()
{
if (UGameplayStatics::DoesSaveGameExist(SaveSlotName, 0))
{
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
TArray<AActor*> SaveGameActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveGameInterface::StaticClass(), SaveGameActors);
for (AActor* Actor : SaveGameActors)
{
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
{
if (ActorData.ActorName == Actor->GetFName())
{
FMemoryReader MyMemoryReader(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MyMemoryReader, true);
Ar.ArIsSaveGame = true;
Actor->Serialize(Ar);
ISaveGameInterface::Execute_OnLoadGame(Actor);
break;
}
}
}
}
}
I also added a ResetData
function that empties the SavedActors
TArray
and executes an interface command to let the actors react run their reset function.
void USaveGameSubsystem::ResetData()
{
TArray<AActor*> SaveGameActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveGameInterface::StaticClass(), SaveGameActors);
for (AActor* Actor : SaveGameActors)
{
ISaveGameInterface::Execute_OnResetData(Actor);
}
CurrentSaveGame->SavedActors.Empty();
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SaveSlotName, 0);
}
Below is the final .cpp
file for the SaveGameSubsystem
class.
SaveGameSubsystem.cpp
#include "SaveGameSubsystem.h"
#include "MySaveGame.h"
#include "SaveGameInterface.h"
#include "Kismet/GameplayStatics.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
void USaveGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
SaveSlotName = TEXT("SaveGame01");
if (UGameplayStatics::DoesSaveGameExist(SaveSlotName, 0))
{
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
}
else
{
CurrentSaveGame = CastChecked<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
}
}
void USaveGameSubsystem::SaveGame()
{
CurrentSaveGame->SavedActors.Empty();
TArray<AActor*> SaveGameActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveGameInterface::StaticClass(), SaveGameActors);
for (AActor* Actor : SaveGameActors)
{
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetFName();
FMemoryWriter MyMemoryWriter(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MyMemoryWriter, true);
Ar.ArIsSaveGame = true;
Actor->Serialize(Ar);
CurrentSaveGame->SavedActors.Add(ActorData);
}
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SaveSlotName, 0);
}
void USaveGameSubsystem::LoadGame()
{
if (UGameplayStatics::DoesSaveGameExist(SaveSlotName, 0))
{
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
TArray<AActor*> SaveGameActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveGameInterface::StaticClass(), SaveGameActors);
for (AActor* Actor : SaveGameActors)
{
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
{
if (ActorData.ActorName == Actor->GetFName())
{
FMemoryReader MyMemoryReader(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MyMemoryReader, true);
Ar.ArIsSaveGame = true;
Actor->Serialize(Ar);
ISaveGameInterface::Execute_OnLoadGame(Actor);
break;
}
}
}
}
}
void USaveGameSubsystem::ResetData()
{
TArray<AActor*> SaveGameActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveGameInterface::StaticClass(), SaveGameActors);
for (AActor* Actor : SaveGameActors)
{
ISaveGameInterface::Execute_OnResetData(Actor);
}
CurrentSaveGame->SavedActors.Empty();
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SaveSlotName, 0);
}
I create two actors to test the save system. I created a collectable coin and flag pole that has a flag move to the top on overlap. Both actors have a bActive
UPROPERTY
that uses the SaveGame
specifier.
// Actor's header file
...
UPROPERTY(EditAnywhere, SaveGame)
bool bActive;
...
The SaveGame
specifier allows the archive struct above to use the ArIsSaveGame
value to indicate which of the actor's variables need to be saved.
Each actor inherits the SaveGameInterface
we created earlier so we can find it when we search the world for savable actors. The actors use virtual overrides for interface implementation functions so they can react accordingly to OnLoadGame
and OnResetData
calls.
#include "SaveGameInterface.h"
#include "SaveGameCoin.generated.h"
...
UCLASS()
class MYPROJECT_API ASaveGameCoin : public AActor, public ISaveGameInterface
...
public:
...
// Interface implementations
virtual void OnLoadGame_Implementation() override;
virtual void OnResetData_Implementation() override;
SaveGameCoin.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SaveGameInterface.h"
#include "SaveGameCoin.generated.h"
class USphereComponent;
UCLASS()
class MYPROJECT_API ASaveGameCoin : public AActor, public ISaveGameInterface
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASaveGameCoin();
// Interface implementations
virtual void OnLoadGame_Implementation() override;
virtual void OnResetData_Implementation() override;
UPROPERTY()
int32 UUID;
UPROPERTY(EditAnywhere)
USceneComponent* Root;
UPROPERTY(EditAnywhere)
USphereComponent* Sphere;
UPROPERTY(EditAnywhere)
UStaticMeshComponent* Mesh;
UPROPERTY(EditAnywhere, SaveGame)
bool bActive = true;
UFUNCTION()
void Activate();
UFUNCTION()
void Deactivate();
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
SaveGameCoin.cpp
#include "SaveGameCoin.h"
#include "Components/SphereComponent.h"
// Sets default values
ASaveGameCoin::ASaveGameCoin()
{
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Root->bVisualizeComponent = true;
RootComponent = Root;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Mesh->SetupAttachment(RootComponent);
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
Sphere->SetupAttachment(RootComponent);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &ASaveGameCoin::OnBeginOverlap);
}
void ASaveGameCoin::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor != nullptr)
{
Deactivate();
}
}
void ASaveGameCoin::OnLoadGame_Implementation()
{
if (!bActive)
{
Deactivate();
}
}
void ASaveGameCoin::OnResetData_Implementation()
{
Activate();
}
void ASaveGameCoin::Activate()
{
bActive = true;
Mesh->SetHiddenInGame(false);
Sphere->OnComponentBeginOverlap.AddUniqueDynamic(this, &ASaveGameCoin::OnBeginOverlap);
}
void ASaveGameCoin::Deactivate()
{
bActive = false;
Mesh->SetHiddenInGame(true);
Sphere->OnComponentBeginOverlap.RemoveAll(this);
}
SaveGameFlagPole.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SaveGameInterface.h"
#include "SaveGameFlagPole.generated.h"
class USphereComponent;
UCLASS()
class MYPROJECT_API ASaveGameFlagPole : public AActor, public ISaveGameInterface
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
public:
// Sets default values for this actor's properties
ASaveGameFlagPole();
// Interface implementations
virtual void OnLoadGame_Implementation() override;
virtual void OnResetData_Implementation() override;
UPROPERTY(EditAnywhere)
USceneComponent* Root;
UPROPERTY(EditAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(EditAnywhere)
UStaticMeshComponent* FlagMesh;
UPROPERTY(EditAnywhere)
USphereComponent* Sphere;
UPROPERTY(EditAnywhere, SaveGame)
bool bActive;
UPROPERTY(EditAnywhere)
FVector FlagOriginLocation;
UPROPERTY(EditAnywhere)
FVector TargetLocation;
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
SaveGameFlagPole.cpp
#include "SaveGameFlagPole.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
ASaveGameFlagPole::ASaveGameFlagPole()
{
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Root->bVisualizeComponent = true;
RootComponent = Root;
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
BaseMesh->SetupAttachment(RootComponent);
FlagMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("FlagMesh"));
FlagMesh->SetupAttachment(RootComponent);
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
Sphere->SetupAttachment(RootComponent);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &ASaveGameFlagPole::OnBeginOverlap);
}
// Called when the game starts
void ASaveGameFlagPole::BeginPlay()
{
Super::BeginPlay();
FlagOriginLocation = FlagMesh->GetRelativeLocation();
}
void ASaveGameFlagPole::OnLoadGame_Implementation()
{
if (bActive)
{
FlagMesh->SetRelativeLocation(TargetLocation);
}
}
void ASaveGameFlagPole::OnResetData_Implementation()
{
bActive = false;
FlagMesh->SetRelativeLocation(FlagOriginLocation);
}
void ASaveGameFlagPole::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor != nullptr && OtherActor != this)
{
bActive = true;
FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.Linkage = 0;
LatentInfo.UUID = FGuid::NewGuid().A;
UKismetSystemLibrary::MoveComponentTo(
FlagMesh,
TargetLocation,
FRotator(0.f, 0.f, 0.f),
true,
true,
3.f,
true,
EMoveComponentAction::Move,
LatentInfo);
}
}
Typically saving and loading a game is done through a UI, but here I'm using sphere components to keep things isolated and reduce any additional complexity.
SaveGameTriggerSphere
has three boolean
values indicating whether to run LoadGame
, SaveGame
, or ResetData
on overlap. The super interesting part of this actor is that since the engine automatically adds subsystems to the game instance, it's immediately available and accessible. All we have to is run USaveGameSubsystem* SaveSubsystem = GetGameInstance()->GetSubsystem<USaveGameSubsystem>();
to get the subsystem then we just need to do SaveSubsystem->SaveGame();
.
USaveGameSubsystem* SaveSubsystem = GetGameInstance()->GetSubsystem<USaveGameSubsystem>();
SaveSubsystem->SaveGame();
SaveGameTriggerSphere.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SaveGameTriggerSphere.generated.h"
class USphereComponent;
class UTextRenderComponent;
UCLASS()
class MYPROJECT_API ASaveGameTriggerSphere : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASaveGameTriggerSphere();
UPROPERTY(EditAnywhere)
USceneComponent* Root;
UPROPERTY(EditAnywhere)
UStaticMeshComponent* Mesh;
UPROPERTY(EditAnywhere)
UTextRenderComponent* Text;
UPROPERTY(EditAnywhere)
USphereComponent* Sphere;
UPROPERTY(EditAnywhere)
bool bSaveGame;
UPROPERTY(EditAnywhere)
bool bLoadGame;
UPROPERTY(EditAnywhere)
bool bResetData;
UFUNCTION()
void LoadGame();
UFUNCTION()
void ResetData();
UFUNCTION()
void SaveGame();
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
SaveGameTriggerSphere.cpp
#include "SaveGameTriggerSphere.h"
#include "Components/SphereComponent.h"
#include "Components/TextRenderComponent.h"
#include "SaveGameSubsystem.h"
// Sets default values
ASaveGameTriggerSphere::ASaveGameTriggerSphere()
{
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Root->bVisualizeComponent = true;
RootComponent = Root;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(RootComponent);
Text = CreateDefaultSubobject<UTextRenderComponent>(TEXT("Text"));
Text->SetHorizontalAlignment(EHorizTextAligment::EHTA_Center);
Text->SetupAttachment(RootComponent);
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
Sphere->SetHiddenInGame(false);
Sphere->SetupAttachment(RootComponent);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &ASaveGameTriggerSphere::OnBeginOverlap);
}
void ASaveGameTriggerSphere::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor != nullptr && OtherActor != this)
{
if (bLoadGame)
{
LoadGame();
return;
}
if (bSaveGame)
{
SaveGame();
return;
}
if (bResetData) ResetData();
}
}
void ASaveGameTriggerSphere::LoadGame()
{
USaveGameSubsystem* SaveSubsystem = GetGameInstance()->GetSubsystem<USaveGameSubsystem>();
SaveSubsystem->LoadGame();
}
void ASaveGameTriggerSphere::ResetData()
{
USaveGameSubsystem* SaveSubsystem = GetGameInstance()->GetSubsystem<USaveGameSubsystem>();
SaveSubsystem->ResetData();
}
void ASaveGameTriggerSphere::SaveGame()
{
USaveGameSubsystem* SaveSubsystem = GetGameInstance()->GetSubsystem<USaveGameSubsystem>();
SaveSubsystem->SaveGame();
}
With this we have completed the C++ programming for this example. We can now create Blueprint actors from the SaveGameCoin
, SaveGameFlagPole
, and SaveGameTriggerSphere
classes. Below are some example screenshots of the final result.
For a better understanding view the source code, clone the project, and visit the gym to get a complete experience of how this all works together. Saving game data in Unreal took me awhile to understand and conceptualize. I hope this helps you in your journey.
Harrison McGuire