For a game idea that I was thinking of I needed a saving/loading system that would store all actors that are in a world. I thought of how games like in Roller Coaster Tycoon or the Sims would do this. I quickly came to the conclusion that it would be best to store all data from all objects in a world so you are able to recreate the world in the same state. For this issue I found an easy solution that is both extendable and dynamic.
The solution
I came up with this approach after reading a blog post from user “ ” at the gamedev.net forum about complex saving and loading techniques in UE4. You can create an interface that can be implemented by an actor to mark actors saveable. The save script gets the actors that implement this interface from the world, serializes their data and stores the data into an actor record. This actor record is a struct that stores the fundamental data that is used to instantiate the actor. These actor records are all saved in an array which is stored in the overarching save game object that is serialized and stored on the user’s disk. When the actors are restored, they are injected with the serialized data. An event is fired from the interface after the injection which allows you to put the actor back in its correct state by using the variables retrieved from the binary data.
Saving Actors
Making actors saveable
To mark objects in the world as saveable, I created an implementable interface in C++ that can be implemented in Blueprint or C++. The interface I created implements 2 events that will trigger when data is saved or loaded so behaviour can be programmed accordingly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#pragma once #include "SaveableActorInterface.generated.h" UINTERFACE(BlueprintType) class USaveableActorInterface : public UInterface { GENERATED_UINTERFACE_BODY() }; class ISaveableActorInterface { GENERATED_IINTERFACE_BODY() public: UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Saveable Actor") void ActorSaveDataLoaded(); UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Saveable Actor") void ActorSaveDataSaved(); }; |
1 2 3 4 5 6 7 8 |
#include "ActorSaveTest.h" #include "SaveableActorInterface.h" USaveableActorInterface::USaveableActorInterface(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { //empty } |
To allow for variables to be serialized, you can mark them with the “SaveGame” tag in the UProperty macro. Or in Blueprint, tick the “SaveGame” box under the variable’s advanced settings. Only variables marked like this will be serialized when the serialize function is called on the actor.
1 2 |
UPROPERTY(SaveGame) int VariableToSave; |
Saving actor records
I created a struct that holds basic information that is required to spawn an instance of the actor. This struct also holds the serialized data that make up all the saved variables of the actor. I also overloaded the << operator so you can easily archive the record in a binary array. If you want to know more about saving binary data, go and read this tutorial from user “Rama” at the Unreal tutorials page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
USTRUCT() struct FActorSaveData { GENERATED_USTRUCT_BODY() FString ActorClass; FName ActorName; FTransform ActorTransform; TArray<uint8> ActorData; friend FArchive& operator<<(FArchive& Ar, FActorSaveData& ActorData) { Ar << ActorData.ActorClass; Ar << ActorData.ActorName; Ar << ActorData.ActorTransform; Ar << ActorData.ActorData; return Ar; } }; |
Next to the actor records I also create an overarching struct that acts as a save game. The save game contains the array with all the actor records and other relative game data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
USTRUCT() struct FSaveGameData { GENERATED_USTRUCT_BODY() FName GameID; FDateTime Timestamp; TArray<FActorSaveData> SavedActors; friend FArchive& operator<<(FArchive& Ar, FSaveGameData& GameData) { Ar << GameData.GameID; Ar << GameData.Timestamp; Ar << GameData.SavedActors; return Ar; } }; |
Iterating the saveable actors
To get a list of all actors in the world that implement the interface, you can use a function in the UGameplayStatistics class. We can use the resulting list to iterate over all the saveable actors and convert them into actor records.
1 2 3 4 5 6 7 |
TArray<AActor*> Actors; UGameplayStatics::GetAllActorsWithInterface(GetWorld(), USaveableActorInterface::StaticClass(), Actors); for (auto Actor : Actors) { //Saving each actor to record } |
Serializing the data
To serialize the actor data to a binary array which can be stored in the actor’s record, we create a struct that inherits from FObjectAndNameAsStringProxyArchive. This class will serialize your objects and prefix the binary data with a string. We do this to ensure that the data won’t become currupted if fields are added or removed.
1 2 3 4 5 6 7 8 |
struct FSaveGameArchive : public FObjectAndNameAsStringProxyArchive { FSaveGameArchive(FArchive& InInnerArchive) : FObjectAndNameAsStringProxyArchive(InInnerArchive, true) { ArIsSaveGame = true; } }; |
Now we create a memory writer that writes the bytestream to the actor record’s ActorData array. We use the memory writer in the archive struct from above and pass the archive to the actor’s Serialize function. Afterwards the record is added to an array of records that will later be added to the final save game. If everything is done, the saved event is executed on the actor. Now we can repeat this for all other to-save actors.
1 2 3 4 5 6 7 8 9 10 11 |
FActorSaveData ActorRecord; ActorRecord.ActorName = FName(*Actor->GetName()); ActorRecord.ActorClass = Actor->GetClass()->GetPathName(); ActorRecord.ActorTransform = Actor->GetTransform(); FMemoryWriter MemoryWriter(ActorRecord.ActorData, true); FSaveGameArchive Ar(MemoryWriter); Actor->Serialize(Ar); SavedActors.Add(ActorRecord); ISaveableActorInterface::Execute_ActorSaveDataSaved(Actor); |
After all the actor records are saved, we can create the final save game.
1 2 3 4 5 6 7 8 9 |
FSaveGameData SaveGameData; SaveGameData.GameID = "1234"; SaveGameData.Timestamp = FDateTime::Now(); SaveGameData.SavedActors = SavedActors; FBufferArchive BinaryData; BinaryData << SaveGameData; |
Saving data to file
After we have created the final save file and serialized it, we can store the byte array to a file.
1 2 3 4 5 6 7 8 9 10 11 |
if (FFileHelper::SaveArrayToFile(BinaryData, *FString("TestSave.sav"))) { UE_LOG(LogTemp, Warning, TEXT("Save Success! %s"), FPlatformProcess::BaseDir()); } else { UE_LOG(LogTemp, Warning, TEXT("Save Failed!")); } BinaryData.FlushCache(); BinaryData.Empty(); |
Loading Actors
Loading the data from file
To load the binary data
1 2 3 4 5 6 7 8 9 10 |
TArray<uint8> BinaryData; if (!FFileHelper::LoadFileToArray(BinaryData, *FString("TestSave.sav"))) { UE_LOG(LogTemp, Warning, TEXT("Load Failed!")); return; } else { UE_LOG(LogTemp, Warning, TEXT("Load Succeeded!")); } |
Extracting data from binary
1 2 3 4 5 6 7 8 9 |
FMemoryReader FromBinary = FMemoryReader(BinaryData, true); FromBinary.Seek(0); FSaveGameData SaveGameData; FromBinary << SaveGameData; FromBinary.FlushCache(); BinaryData.Empty(); FromBinary.Close(); |
Spawning the actor
Now we iterate over all the actor records in the save file and instantiate them accordingly. During the process we also inject the data and execute the loaded event on the actor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
for (FActorSaveData ActorRecord : SaveGameData.SavedActors) { FVector SpawnPos = ActorRecord.ActorTransform.GetLocation(); FRotator SpawnRot = ActorRecord.ActorTransform.Rotator(); FActorSpawnParameters SpawnParams; SpawnParams.Name = ActorRecord.ActorName; UClass* SpawnClass = FindObject<UClass>(ANY_PACKAGE, *ActorRecord.ActorClass); if (SpawnClass) { AActor* NewActor = GWorld->SpawnActor(SpawnClass, &SpawnPos, &SpawnRot, SpawnParams); FMemoryReader MemoryReader(ActorRecord.ActorData, true); FSaveGameArchive Ar(MemoryReader); NewActor->Serialize(Ar); NewActor->SetActorTransform(ActorRecord.ActorTransform); ISaveableActorInterface::Execute_ActorSaveDataLoaded(NewActor); } } |
References:
Evanger, J. (2017, January 17). Omplex Saving and Loading Techniques in Unreal Engine 4. Retrieved January 19, 2017, from https://www.gamedev.net/topic/685514-complex-saving-and-loading-techniques-in-unreal-engine-4/
Rama. (n.d.). Interfaces in C. Retrieved January 19, 2017, from https://wiki.unrealengine.com/Interfaces_in_C%2B%2B
W, J. (2014, December 23). UE4ActorSaveLoad. Retrieved January 19, 2017, from https://github.com/shinaka/UE4ActorSaveLoad
Rama. (n.d.). Save System, Read & Write Any Data to Compressed Binary Files. Retrieved January 19, 2017, from https://wiki.unrealengine.com/Save_System,_Read_%26_Write_Any_Data_to_Compressed_Binary_Files
How would this be implemented using blueprints?
HI Allan, this solution requires you to code some C++ and cannot be achieved using blueprints.
Oh ok :(
At the top when you said “I created an implementable interface in C++ that can be implemented in *Blueprint* or C++.” did you mean using C++ nodes in blueprints? If so, do you add the C++ code inside nodes and where do they go?
Hi Allan, you can achieve this by using attributes in C++, you can take a look at this guide: https://wiki.unrealengine.com/Blueprints,_Creating_C%2B%2B_Functions_as_new_Blueprint_Nodes
Working great for me except one thing, UE is unable to serialize asset references like TAssetPtr and similar. How would one approach to modify this to use FArchiveUObject instead FArchive as that is error message I do get to use FArchiveUObject instead. It might be trivial but I’m not sure how to do it exactly.
Hi Peter! You should be able to just replace FArchive by FArchiveUObject. They are essentially the same but the UObject version implements the code for serializing TAssetPtr etc.
I did try it and it does not work way I expected… :) Maybe I’m just way to new to C++ to get full grasp of what is going on there. Essentially I would benefit from being able to store that so if you can provide example based on your post code that would be awesome. For now I did solve it in crude way that defy using interfaces by overriding Serialize method on actor and inside converting that info to string path and manually injecting it to archive. I’m not fully happy with that solution as it does defy interface use. If you would have time to extend article/snippet with version that does support TAssetPtr I would be very thankful to you. I’m using your example to store user created track layout and restore it afterwards. Would like to apply the same principle to player vehicle but I need it to be elegant as I do delayed spawn base class and pass in data asset so it can configure itself properly… Thank you.
Nice post! It’s always great to see more people writing about this!