In this post we’re going to see how you can save and load your data in UE4 using the provided API from Epic. We will start by saving and loading primitive data and then move on to save complex data (ie custom classes etc.). For this post, I’m using the 4.15 version of the engine, so in case you’re following this post with a different version you might have to change your code a bit to match the API of your version.

The save data process is split into the following parts:

Serialize our data using an Archive, meaning we will convert our data into a format that can be stored – in this case, we’ll convert the data into binary. Store the binary data into the hard disk

The load data process is split into the following parts:

Store the contents of the hard disk into the memory Deserialize the data

In UE4, the code snippets for serialization and deserialization will be same. This means that we’re going to write a function that saves and loads data (I think that this idea is originated from Rama). By having a single code snippet for these two tasks, we end up having less code and therefore the debug process is easier. However, I would not recommend this approach in general. Two different functionalities should be split to avoid confusion in the long run.

For this post, I’m using the Third Person C++ Template so I can access the player’s class code. I suggest you do the same since all the code provided below is going to be in that class.

Creating the data intented for save and load

For the first part of this post, we’re going to save and load some primitive types. Here are the data we’re going to use:

Data for save and load protected: UPROPERTY(EditAnywhere) float Health; UPROPERTY(EditAnywhere) int32 CurrentAmmo; UPROPERTY(EditAnywhere) FVector RandomLocation; 1 2 3 4 5 6 7 8 9 10 protected : UPROPERTY ( EditAnywhere ) float Health ; UPROPERTY ( EditAnywhere ) int32 CurrentAmmo ; UPROPERTY ( EditAnywhere ) FVector RandomLocation ;

Save-Load function

First, let’s create the function that saves and loads the data:

void SaveLoadData ( FArchive & Ar , float & HealthToSaveOrLoad , int32 & CurrentAmmoToSaveOrLoad , FVector & PlayerLocationToSaveOrLoad ) ;

Here is the implementation of the SaveLoadData function:

Save and Load data void ATP_ThirdPersonCharacter::SaveLoadData(FArchive& Ar, float& HealthToSaveOrLoad, int32& CurrentAmmoToSaveOrLoad, FVector& PlayerLocationToSaveOrLoad) { //Save or load values Ar << HealthToSaveOrLoad; Ar << CurrentAmmoToSaveOrLoad; Ar << PlayerLocationToSaveOrLoad; } 1 2 3 4 5 6 7 8 9 void ATP_ThirdPersonCharacter :: SaveLoadData ( FArchive & Ar , float & HealthToSaveOrLoad , int32 & CurrentAmmoToSaveOrLoad , FVector & PlayerLocationToSaveOrLoad ) { //Save or load values Ar << HealthToSaveOrLoad ; Ar << CurrentAmmoToSaveOrLoad ; Ar << PlayerLocationToSaveOrLoad ; }

This function will save and load data, depending on the use case. The reason that SaveLoadData function can do both is because the “<<” operator has two different meanings. Specifically, the operator in this case could mean that:

We’re storing data from a variable (ie HealthToSaveOrLoad) into a binary format

We’re converting binary data to a variable

Saving and loading primitive types

Since we have created the required function for save and load, provide the following functions to use in Blueprint code:

SaveData and LoadData declarations protected: UFUNCTION(BlueprintCallable, Category = SaveLoad) bool SaveData(); UFUNCTION(BlueprintCallable, Category = SaveLoad) bool LoadData(); 1 2 3 4 5 6 7 protected : UFUNCTION ( BlueprintCallable , Category = SaveLoad ) bool SaveData ( ) ; UFUNCTION ( BlueprintCallable , Category = SaveLoad ) bool LoadData ( ) ;

Before we implement the logic for these functions, define a name for the file we’re going to store into your character’s source file:

#define SAVEDATAFILENAME "SampleSavedData"

We’re going to use that file for save and load. If we attemp to save and this file already exists, it’s going to be overwritten. In case you don’t want that, you can store your data in a different location by using a FString type.

Let’s provide the save and load implementations:

Save and load logic bool ATP_ThirdPersonCharacter::SaveData() { //Save the data to binary FBufferArchive ToBinary; SaveLoadData(ToBinary, Health, CurrentAmmo, RandomLocation); //No data were saved if (ToBinary.Num() <= 0) return false; //Save binaries to disk bool result = FFileHelper::SaveArrayToFile(ToBinary, TEXT(SAVEDATAFILENAME)); //Empty the buffer's contents ToBinary.FlushCache(); ToBinary.Empty(); return result; } bool ATP_ThirdPersonCharacter::LoadData() { TArray<uint8> BinaryArray; //load disk data to binary array if (!FFileHelper::LoadFileToArray(BinaryArray, TEXT(SAVEDATAFILENAME))) return false; if (BinaryArray.Num() <= 0) return false; //Memory reader is the archive that we're going to use in order to read the loaded data FMemoryReader FromBinary = FMemoryReader(BinaryArray, true); FromBinary.Seek(0); SaveLoadData(FromBinary, Health, CurrentAmmo, RandomLocation); //Empty the buffer's contents FromBinary.FlushCache(); BinaryArray.Empty(); //Close the stream FromBinary.Close(); return true; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 bool ATP_ThirdPersonCharacter :: SaveData ( ) { //Save the data to binary FBufferArchive ToBinary ; SaveLoadData ( ToBinary , Health , CurrentAmmo , RandomLocation ) ; //No data were saved if ( ToBinary . Num ( ) <= 0 ) return false ; //Save binaries to disk bool result = FFileHelper :: SaveArrayToFile ( ToBinary , TEXT ( SAVEDATAFILENAME ) ) ; //Empty the buffer's contents ToBinary . FlushCache ( ) ; ToBinary . Empty ( ) ; return result ; } bool ATP_ThirdPersonCharacter :: LoadData ( ) { TArray < uint8 > BinaryArray ; //load disk data to binary array if ( ! FFileHelper :: LoadFileToArray ( BinaryArray , TEXT ( SAVEDATAFILENAME ) ) ) return false ; if ( BinaryArray . Num ( ) <= 0 ) return false ; //Memory reader is the archive that we're going to use in order to read the loaded data FMemoryReader FromBinary = FMemoryReader ( BinaryArray , true ) ; FromBinary . Seek ( 0 ) ; SaveLoadData ( FromBinary , Health , CurrentAmmo , RandomLocation ) ; //Empty the buffer's contents FromBinary . FlushCache ( ) ; BinaryArray . Empty ( ) ; //Close the stream FromBinary . Close ( ) ; return true ; }

In line 25 of the above snippet you will notice that we’re passing a TArray<uint8> into the LoadFileToArray function, however, in line 11, we’re passing a FBufferArchive to SaveArrayToFile function. The reason that both work, is because FBufferArchive inherits from FMemoryWriter and TArray<uint8>. For more information about that, check out this header file in UE4’s github repo. If the link doesn’t work for you, you have to connect your GitHub account to your Epic account so you can gain access to the source code of the engine.

Since we’re loading binary data and we’re attempting to deserialize them into actual values, we have to be sure that we’re deserializing values in the same order we serialized them. This is the core concept of having the same function for both save and load. In the end of this section, I have uploaded two screenshots that showcase the result of a different deserialization order.

After you’re done with that, provide the following logic in your character’s BP to test your code:

To test the save and load, provide some random values from within the Blueprint and then save these using the Save Data function. Then, stop your game, reset all values to 0 and attemp to load your data. Your data will be loaded just fine!

If we deserialize the data in a different order than we serialized them, we might end up with strange results like the following screenshots suggest:

Both deserializations come from the same serialized file!

Saving and Loading complex types

In the previous section we saved primitive types that already exist in the engine’s API. Fortunately, it’s quite easy to save and load complex types (for example custom classes). To do that, we have to extend the << operator of the FArchive class, in order to support our custom class.

Navigate to your classs header file and declare the following friend function:

friend FArchive & operator << ( FArchive & Ar , CustomClass & CustomClassRef ) ;

Then, in your source file provide the following implementation:



FArchive& operator<<(FArchive& Ar, CustomClass& CustomClassRef) { Ar << CustomClassRef.SomeValue; return Ar; } 1 2 3 4 5 6 FArchive & operator << ( FArchive & Ar , CustomClass & CustomClassRef ) { Ar << CustomClassRef . SomeValue ; return Ar ; }

Related

A friend function is a function that is able to access the private members of class. By using the friend keyword in this case we’re “extending” the FArchive class in order to support our custom class for serialization and deserialization.