As noted at the top of this section, our actual game and the state of all our dynamic actors is done in three major steps:
- Add an interface to all actors that need to save. This involves a few changes to our moving platform, which we'll try to keep straightforward.
- Serialize all our actors' desired variables to an FArchive by tagging our UPROPERTIES.
- Write this to a file we can then serialize everything back out from.
For very simple saving (such as player stats and the current level), be sure to check out the USaveGame document link in the Further reading section at the end of the chapter. Now, on to our relatively complex version.
First we need an interface that we'll add to all of our actors that we care about saving, this is the first time we need to make a C++ class outside the editor.
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Serialization/ObjectAndNameAsStringProxyArchive.h" #include "Inventory/MasteringInventory.h" #include "SavedActorInterface.generated.h" /** * */ USTRUCT() struct FActorSavedData { GENERATED_USTRUCT_BODY() FString MyClass; FTransform MyTransform; FVector MyVelocity; FName MyName; TArray<uint8> MyData; friend FArchive& operator<<(FArchive& Ar, FActorSavedData& SavedData) { Ar << SavedData.MyClass; Ar << SavedData.MyTransform; Ar << SavedData.MyVelocity; Ar << SavedData.MyName; Ar << SavedData.MyData; return Ar; } }; USTRUCT() struct FInventoryItemData { GENERATED_USTRUCT_BODY() FString WeaponClass; int WeaponPower; int Ammo; FString TextureClass; friend FArchive& operator<<(FArchive& Ar, FInventoryItemData& InvItemData) { Ar << InvItemData.WeaponClass; Ar << InvItemData.WeaponPower; Ar << InvItemData.Ammo; Ar << InvItemData.TextureClass; return Ar; } }; USTRUCT() struct FInventorySaveData { GENERATED_USTRUCT_BODY() FString CurrentWeapon; int CurrentWeaponPower = -1; TArray<FInventoryItemData> WeaponsArray; friend FArchive& operator<<(FArchive& Ar, FInventorySaveData& InvData) { Ar << InvData.CurrentWeapon; Ar << InvData.CurrentWeaponPower; Ar << InvData.WeaponsArray; return Ar; } }; USTRUCT() struct FGameSavedData { GENERATED_USTRUCT_BODY() FDateTime Timestamp; FName MapName; FInventorySaveData InventoryData; TArray<FActorSavedData> SavedActors; friend FArchive& operator<<(FArchive& Ar, FGameSavedData& GameData) { Ar << GameData.MapName; Ar << GameData.Timestamp; Ar << GameData.InventoryData; Ar << GameData.SavedActors; return Ar; } }; struct FSaveGameArchive : public FObjectAndNameAsStringProxyArchive { FSaveGameArchive(FArchive& InInnerArchive) : FObjectAndNameAsStringProxyArchive(InInnerArchive, true) { ArIsSaveGame = true; } }; UINTERFACE(BlueprintType) class USavedActorInterface : public UInterface { GENERATED_UINTERFACE_BODY() }; class ISavedActorInterface { GENERATED_IINTERFACE_BODY() public: UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Load-Save") void ActorLoaded(); };
And what is nice about BlueprintNativeEvent is that we can fire these from C++, but have them executed in blueprint. The class we have some new work to do for is our moving platform, which again, exists and is defined solely in blueprint. Making the interface BlueprintType means we can add this easily to our platform blueprint-only class. So, heading to that class, here are the steps we need to get it saving properly to the archive. Open the moving platform class, click Class Settings at the top main menu bar, and on the right, you'll see Implemented Interfaces, and we can click Add and select Saved Actor Interface to add this functionality on the blueprint side. Once we compile the blueprint, we can add an event then for when the actor is loaded. To properly set it in the right state, we need to click on its two variables in the My Blueprint tab on the left, and in their Details tab, click the down arrow to expose the rest of its options and check the box for SaveGame for both the GoingHome and StartPosition blueprint variables. Now, when we serialize a platform to an archive, these will be saved and loaded and while ideally we would "lasso" select a set of nodes and right-click and select Collapse to Function, we can't do this here because asynchronous nodes such as MoveComponentTo have to stay in the Event Graph layer. But let's add an event for the interface's Actor Loaded and then just copy paste some of the movement nodes, making sure that if the platform needs to be moving it goes the right way (based on the Going Home variable). There's no harm telling a platform to go where it already is, so we'll set it on the case it has Going Home set to move to its Start Position. Also fixed slightly is the on-actor-overlap event from before. It will go to Start Position + 300 in Z, rather than its current position. So that fixes arguably our hardest case, the blueprint-only class of the group. Let's add the interface to our other classes, and give them a general save functionality as well as a couple of specific ones (such as our MasteringCharacter).
MyData will consist of all the UPROPERTY items we tag with SaveGame. Right now, the only one of these we would really need to add is the player's inventory; but because that has class references and an array of structs that also directly reference a texture and class, we'll custom handle inventory.
If we had other basic types (such as the blueprint variables on our moving platform), simply add UPROPERTY(SaveGame) to their definition and they automatically serialize in and out with the actor data. To make inventory load and save properly, we need a couple of new structs and its own serialization to and from them, which we will demonstrate in the next section. Since it is not an actor class, it is a little annoying to put its structs in the same place as those actor saving ones, but this still seems at this level of complexity to be the best place. So now, how do we use this menu, some new UI, and a lot of saving and loading code to save everything that can change in our levels at any moment and load right back? Let's dive into that now!