Saving Games Smartly
- Jul 20, 2024
- 5 min read
A cornerstone to games is the ability to save and load them. This is true whether you use a simple checkpoint system or a full blown full world saver that saves everything down to the current animations playing.
Saving a game can be a daunting task if you're new to the idea though. Consider all of the things that make up your game. Everything from ending defaults like component transforms, mesh properties, and materials, to your own game variables and properties in the classes you have made. This is a lot of stuff that needs to be saved somehow and reset when reloading the saved game.
The popular way
A seemingly popular way to handle savegames is by making a struct that can hold something's data. And then you do this for everything that has data. Every inventory item, every quest, every character, weapon, etc etc. Each of these containing a specialized struct that can copy and hold their data.
Surely you already see how daunting this is. You start by making a class, you make properties on it, you put logic in it to make it work. Then you have to go make a special struct that can hold all of those properties. Now you have to go add even more code to this class to populate the struct of data and also to take that struct of data and populate itself.
This would be fine if it were the end. But now you have to do this for everything in the game. And on top of that, you have to maintain this for everything in the game. Add a new property? Extend the struct and extend the code setting and reading from it. It is a lot of upkeep, and I for one really hate upkeep.
The better way
There is a way to do what was outlined above, without doing any of what was outline above. Pretty much everything in Unreal is made from UObjects. Even editor classes are made from UObjects. And there is a very good reason for this. Serialize. AKA UObject::Serialize.
Lets start at Archives, because Serialize is a function that takes in an Archive. There are two different major types of Archives. One is a Reader, one is a Writer. A Writer will read data from a UObject and put it into an array of bytes. A Reader will take an array of bytes and use it to populate a UObject's properties.
The general idea here is that you use one or the other of these archives to achieve your goal. The goals are either to save an object's properties when saving, or populate an object's properties when loading.
You can extend an Archive struct and make it only save properties that are marked SaveGame. Or you can save an entire object's properties regardless of their SaveGame mark. One is of course smaller, but sometimes you need the other because you can't always save just savegame stuff sometimes in classes you can't affect.
Property serialization
A property is basically a name and a value. No matter what it is. Pointers, floats, integer, strings. All of these are a property name and a property value. This basic fact is what allows the Archives to work. They simply save what amounts to a string of proeprties and their values.
An example of this might be the following. Excluding their UPROPERTY macros.
float MyFloat = 0.5f;
int32 MyInteger = 37;
I don't remember the specific delimiter. But an archive might save these such as...
MyFloat=0.5;MyInteger=37;
It saves them via Name=Value. And this is a really neat note because this also works for pointers!
Pointers
Lets move on to another special savegame feature a lot of people do not know about. Saving pointers. Pointers are memory locations. So the idea of saving them is strange at first. How are you possibly going to reload a memory spot? Except that Unreal doesn't do this. Unreal saves pointers to UObjects via their names.
Every object has a special name that identifies it. And it is usually appended to their outer's name. Lets make a quick example of an InventoryComponent on a Character, inside of a map named TheReach.
TheReach.OurCharacter32.InventoryComponent
Note the 32. It's special because the map and component aren't numbered. The component could be but this is a default component. The 32 represents this as the 33rd actor that has been spawned this session in TheReach. This is a special name. There cannot be a second OurCharacter32 in the same map. There cannot be a second InventoryComponent on OurCharacter32.
This is exactly how networking shares pointers as well. After all you don't send a memory location over the network expecting Player2 to have identical memory to Player1. Instead you send pointers as a string, which is the name path you can use to identify it.
This bit of information is neat for one major reason. You can save pointers in a Savegame.
Saving and restoring pointers
So saving pointers is easy to follow once you understand that they save as names. But as some of you already understand, there is an issue with the names.
Lets go through the simple logic steps. When you start a brand new game. You spawn forty SuperCoolWeapons. You have SuperCoolWeapon0 to SuperCoolWeapon39. Now you destroy the first thirty five of these weapons. So SuperCoolWeapon0 to 34 are destroyed. You save your game, and in it are SuperCoolWeapon35 to 39.
You load this game and spawn five SuperCoolWeapons. You have SuperCoolWeapon0 to SuperCoolWeapon4. Cause the counter was reset when reloading the game. You can take the five saved weapons and put their data in these five new ones. This will work for the data these weapons held. But any pointers pointing to these weapons will not work and will be null. This is due to their name change.
Combatting this is a simple change of logic. We already know we're saving an array of bytes and a class which they belong to. So simply save a name along with this data as well. Now you have the ability to create an object of a class, with the correct name it was saved with, and the bytes of data saved from it to restore.
The main trick once you realize this is very simple. You should have an array of structs in your savegame that is basically what is outlined above. ObjectType, ObjectName, ObjectData.
As I noted above, you can find any object via it's name. This is important because this allows you to find an object if it exists like something loaded with the map or something statically initiated from code before the save is loaded, or even Subsystems. So you do a simple loop of either finding the object or spawning a new one with that name and class.
At this point everything you saved should be back in the game but without it's saved data. Now, you make a second loop and restore everything. Due to the objects being the same name as they were when saved, every pointer saved restores correctly.
The take away
You can create an extremely simple savegame system that will save pretty much any game type using the stuff outlined above. A basic understanding of Unreal's core architecture and these fundamentals outlined above makes it very trivial to create a system which can save entire maps of data. Whether you simply save a couple objects like a ProfileSubsystem and a checkpoint, or you want to save thousands of objects at a time and restore them. It's all possible and easy to manage.
I will probably write more articles on the specifics above such as how to use Serialize later. But feel free to catch me in Unreal Source if you want to chat about the topic or have any questions!



Comments