Welcome back.
So now that we have data structures for saving data, it's time to actually save it.
So we need to figure out how to save our world state.
Now we're going to go on to our game mode base to create save and load functions.
And this is just one solution.
You don't have to save data from the game mode.
You can do it from other classes.
Some people like to create subsystems.
And that's something I encourage you to look into.
If you're interested, you can make a save game subsystem we're just going to use or a game mode base
for this.
And I'm going to put a brand new function here in our game mode base for saving the world.
It sounds kind of funny, but we're just going to call this function Save World State.
And I like this function to take in a new world by pointer called world, as this depends on what world
we're currently in when we save the world.
So let's go ahead and generate this definition.
And let's think about how we're going to save the world.
Now first of all, remember our data structures are going to identify the world by its name.
If we go to load screen save game, we'll see that we have a T array of saved maps, F save map, and
the F saved map.
Data structure has an F string called map asset name.
We need to know what the map is for a given world in our save world state function.
So how do we get that?
Well, first we can take the world and we can call get map name.
But get map name is going to have some characters prefixing it.
We call this the streaming levels prefix, and these characters must be removed if we want the true
map name.
So what we can do is make a non-const f string called world name.
And then we can remove those characters from the beginning that are automatically prepended to it.
So we can take world name dot remove from start.
This is just an f string utility function and we're going to remove those characters.
We can get those because they're called streaming levels prefix.
Once we remove those from the start we now have the actual name of the world.
This is that map asset name.
So once we've done that we can actually get our save slot data.
Now to get our save game data according to which slot we're using, we need our game instance as the
Or game instance has the load slot name and the load slot index, right.
So we can get you or a game instance.
We can call this aura GI for aura game instance, and we'll cast to aura game instance.
So you or a game instance here we're going to cast the result of get game instance.
So here we have an aura game instance.
We can check it.
It should never be null.
We can just check aura GI.
Now once we have the game instance we can check to see if we have valid save game data.
We can say if you load screen save game we'll call this save game equals get save slot data.
We can get the save slot data using the load slot name and index from aura GI.
So aura GI, the game instance has the load slot name, and aura GI also has the load slot index.
So if this pointer is valid, we now have our save game data.
Now we're ready to start saving things to our save game data.
Now the first thing I want to do is take that save game object and see if it has the current world already.
Because remember load screen save game has a t array of saved maps and each one has the map asset name.
Right.
And if we're going to take this saved maps array and add to it, we need to see if it already contains
an F saved map for the current world name.
So we're going to check that.
We're going to say if not save game.
And we're going to call that has map function passing in the world name.
If not has map, then that means we haven't saved data for this particular world yet, and we need to
add a saved map to our saved games array of saved maps.
So we're going to make a new F saved map called New saved map.
And we need to add to this.
We need to take new saved map.
First of all, we can set the map asset name because we have world name right?
And then we can take our save game object.
Take it saved maps array and add our new saved map to it.
So we're adding an F saved map to our saved maps array and the save game object for this slot name and
index.
This is all for the first time.
Now what do we do after that?
Because so far we've just added a new f saved map with an empty array actor, right?
An empty array of F save actors?
Well, we need to populate that array of saved actors with any actors in our world that we wish to save.
Now, how are we going to distinguish which actors we should save and which actors we shouldn't?
A perfectly fine way to distinguish between actors we should save and actors we shouldn't is to implement
an interface.
Now, interaction doesn't have any interfaces.
We haven't created any for saving the game or for distinguishing actors that should be saved actors.
So we should create a new interface.
Let's go ahead and compile and launch the editor.
And what I'd like to do while I'm here in the editor is quite quick.
I'd like to go to C plus plus classes or a public interaction and make a new interface class simple.
Let's scroll all the way down to unreal interface.
Click next.
We'll put this in interaction and we'll call this save interface.
We'll create the class and close the editor right back down.
That's it.
And we can check all the actors in the world to see if they implement the save interface.
Now, so far, no actors implement the save interface, but there's one that I can think of that I'd
like to have this interface.
That's my checkpoint class.
Why don't I just go over to checkpoint dot H and implement the interface here we can say public, I
save interface and of course that header is auto included right there.
So now our checkpoint implements the interface.
And we know that our checkpoints are something we can save.
So back to our A game mode base.
Let's get all the actors and filter out any that don't have that interface.
Now what I'd like to do first is I'd like to take my save game object and call the function I created
called Get Saved map with name passing in world name, because this is going to give me the F saved
map that contains that array.
And each time I save the world state, I'm going to want to empty this array.
I'm going to want to clear it on out.
So what I'll do first is I'll make a copy of it.
I'll say F saved map, saved map, and I'll get this f saved map as a copy and I'll empty the copy.
I'll say saved map dot saved actors.
Remember, it's an F saved map which contains a saved actors array.
I'm going to empty that array.
Now it may already be empty, but it may not.
So I'm going to make sure to clear it out.
I'm going to say clear it out.
We'll fill it in.
With actors.
Now I'm going to say quote and unquote actors, right.
Because we're filling it in with f saved actors, right?
Not real actors, not pointers.
We can't save pointers.
Now.
The way we're going to do this is we're going to iterate over all the actors in the world.
That's okay to do it once in a while.
Like every time we reach a checkpoint, that's totally fine.
In fact, we most likely won't even feel a glitch or anything unless there are lots of actors.
And I mean a lot of actors doing this in C plus plus should be totally fine.
But if you wanted to optimize this, the way to do it would be to store an array of actors that you'd
like to save and add actors to that array each time you change their state somehow, and then iterate
over that smaller subset of actors instead of all the actors in the world.
That being said, it's not going to be that performance intensive to just iterate over all the actors
in the world.
I'm just pointing out that you could optimize it so that you don't iterate over all of them.
So how do we iterate over all of them?
Well, we can do a for loop and we can use an iterator, an F actor iterator that can take in a world.
So we do it by saying f actor, iterator it.
Now f actor iterator needs an include.
We're going to include it and its engine utils dot h that contains the f actor iterator.
We can create one here in our for loop passing in the world, because an f actor iterator can take in
the world.
And what we'll do for this for loop is we'll say it.
Plus plus it.
This is the syntax to iterate over all the actors in the world via the f actor iterator.
Now the f actor iterator is going to contain a pointer to an actor for each iteration of this for loop.
And to get that actor, we say actor star actor equals dereferenced iterator.
And of course it's a actor, right?
There's an A in front of all a actor types.
Now this for loop will loop over all the actors, literally every single one.
And we can filter them out.
First of all, if it's pending kill, we don't want it.
So if it's not valid, we can say not is valid actor.
That's going to be a condition to not do anything.
But we'll also have an Or here and say not actor implements because we want to see if it implements
the you Save interface.
Now, if either of these is true, we can simply continue.
Continue will say skip the rest of this iteration of the for loop.
Go to the next actor and test that one.
So if we reach down beneath this line, then we've found a valid actor that implements the save interface.
Now, what we can do in this case is we can make a new f saved actor called saved actor, and we can
start to fill this saved actor struct in with valid data.
We can take saved actor dot actor name.
And if we want to get an actor name that will be distinguishable from other actors of the same type
in the world, then we can use actor get f name.
And we'll be safe to use that now, if we like, we can save the transform.
We can say saved actor dot transform.
Equals actor.
Get transform.
If you'd like to save that.
My checkpoints aren't movable and I don't want to set their transforms.
But just as an example, you could save the transform.
And now comes the part where we were talking in the last video about how you can serialize the data
in an actor into bytes.
Remember when we created the saved actor?
We created bytes, a t array of uint eights, in other words, an array of bytes.
And we said that we can serialize variables from this actor if they have the save game specifier.
Well, first of all, let's see what that looks like.
Let's go to our checkpoint and give our checkpoint a variable.
We'll make it a public variable.
Let's say bool be reached and set it to false and give it a uproperty.
Now reached as in this checkpoint has been reached already, right?
Let's say if this boolean is reached, we want to save that data.
We want it to be remembered.
So next time we go to the world, it's already glowing.
It's already deactivated.
We can't overlap with it anymore and it's lit up.
Now, to be able to serialize this variable, we give it save game as a uproperty specifier.
And yes, you can still give it other specifiers like blueprint read only for example, and edit anywhere.
Anything you like, but save game is what makes it serializable with the method we're about to use.
So now that checkpoint has a boolean with save game, let's go about serializing that data.
Now, the first thing we're going to do is create something called a memory writer.
An f memory writer.
We're going to call this memory writer.
And we're going to initialize it by passing in that byte array.
We're going to take saved actor dot bytes and pass that in.
Now what is a memory writer.
Let's right click on it and go back to its definition.
We'll go to Declaration or Usages.
And it takes us to memory writer dot H.
Now it says memory writer and memory writer 64 are implemented as derived classes rather than aliases,
so that forward declarations will work.
And it shows that f memory writer is a public t memory writer 32 t indicates a template, and f memory
writer is simply a t memory writer, using 32 as the template argument.
And if you were wondering if this was pertaining to the size, notice there's an F memory writer 64,
which is using T memory writer passing in 64 instead of 32.
Now we can right click on T memory Writer and we can go to that one.
We see that it's still here in memory writer.
And the comment says archive for storing arbitrary data to the specified memory location.
Now we have experience with archives already in.
Net serialize we've experienced the FR archive type T memory writer is a template class with an index
size, and it's based on or derived from F memory archive.
Now notice it has a static assert at compile time.
It asserts that the index size is either 32 or 64, and it says only.
These are supported 32 bit index size and 64 bit index size.
Now what's an F memory archive?
Well, if we go to the definition of this one it's an F archive.
It's just a child class of the F archive class.
It's a base class for serializing arbitrary data in memory.
Now usually structs have F's, but it turns out that the f skive and the F memory archive.
Well, it looks like they're classes.
It doesn't matter though.
They're used for serializing data.
So we can use an archive.
We can use a memory writer to archive our saved actors bytes.
Basically, what we're going to do is take this saved actor dot bytes this array.
Stick it into memory writer so that the memory writer has a reference to it.
And then we're going to use an F object and name as string proxy archive I'm going to call this archive.
And initialize this with our memory writer, and it also takes a bool called b in load.
If find fails, which says indicates whether to try and load a raft object.
If we don't find it, we can pass true in for that.
Okay, so what's this other archive we know f memory writer is a descendant of the F archive.
But what's an f object and name as string proxy archive?
Well, it's another archive, but it's designed to serialize a you object and f names if we have them.
So we can serialize an actor, for instance using f object and name as string proxy archive.
I would like to use this to serialize my actor.
And what I can do is take my archive.
And there's a boolean.
R is savegame.
We're going to set that to true because we're serializing this through a save game object.
So the archive here can be set to be compatible with a save game object.
And once we have this we can take our actor and call serialize on it.
Actors have a serialize function that takes an archive argument.
And we know that our archive is an F archive.
We can pass that in, and serialize is going to serialize the actor into the archive.
Now, once we're done with that, we can take our saved map and remember what our saved map is.
It's our f saved map that we created up here.
So it has its own saved actors array and we can add to it.
We can even add unique because we overloaded the double equals operator for the f saved actor.
And we can put our saved actor in here.
We can add that to the array uniquely.
So what does all this have to do with the saved actor?
Well, we pass in the saved actors bytes into this memory writer.
And remember, the memory writer is right here.
And it does have the capability to take in a byte array.
Now it's going to take that byte array and it's going to write to the memory in that byte array.
That byte array exists in our saved actor, which we're adding to our saved map.
And this we plan on adding to our save game object.
So once we're done with this whole for loop, we can then take that saved map we created and we can
find on our save game object which F saved map has this world name and we can replace it.
We know that the saved maps has an F saved map with this world name.
We made sure of that here.
Either it didn't have one and we added one, or it does have one, and we're going to replace it.
We're going to say for f saved map reference, we'll call this map to replace in save game saved maps.
And we'll say if.
Map to replace dot map.
Asset name equals world name.
If that's the case, we're going to take map to replace and set it to saved map, because we've just
now filled that in with fresh new data, and we're going to clear out the old data.
We can do this because we're using a reference here, a non-const reference.
So we're going to replace that data right here.
Now that our save game object has fresh new data, we can actually save the game.
So after this for loop we can use you gameplay statics save game to slot.
Passing in the save game and the slot name and slot index, which we know we have thanks to our game
instance, so we can copy our game instance, load, slot name and index and we can paste them in right
there.
So now we have a function to save all the actors in the world, at least those that have implemented
the save interface, which are just the beacons for now.
But we have the ability to serialize their data.
So now we know that we're serializing any variables that contain the save game Uproperty specifier.
The question now is how do we load that data?
Well, we'll be doing that in the next video.
So stay tuned.
We'll create a load world state function and save and load our data.
I'll see you soon.