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

Unreal Engine 5 Quinn standing next to save and load platforms with coins and test flag poles behind her

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

Unreal Engine 5 Quinn standing on the Save Game platform
Quinn in the Gym: Quinn ran around the area and collected some coins and raised two flag poles then saved the game.
Unreal Engine 5 Quinn standing in front of a new game instance of the save section of the gym
Quinn in the Gym: Stopping the editor play and then beginning a PIE session, the actors are restored to their original settings. All coins are active and all flag poles are down.
Unreal Engine 5 Quinn standing on the Load Game platform indicating the saved data has been restored
Quinn in the Gym: After running onto the Load Game platform the save data is restored, the coins deactivated and the flags are raised.
Unreal Engine 5 Quinn standing on the Reset Data platform
Quinn in the Gym: To reset the save file Quinn can run onto the Reset Data platform to activate all coins and lower all flags.

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.

Loading...