When I was looking for pointers on how to do saving and loading, I was frustrated at the lack of depth in the tutorials that I found. I couldn’t find anything that went beyond explaining PlayerPrefs and serialisation. I mean, yes, duh, I have to read and write the data — but how should I structure it? How should I find it? What’s the best way to put it together again? What did you see?!
Thus, having recently finished a fully armed and operational saving and loading system in my own game, Exon, here is a complete run-down of how it works and why. Although I would expect saving and loading to be personal to the architecture of a particular game, I feel like I’ve ended up with a very flexible and generic system, so hopefully this dump will give future people a decent idea of how to approach this most important of features. Plus some reassurance — it’s actually not that scary!
Locked and Loaded
It starts with a base class. Every component that can be saved and loaded inherits from a base class which comes with two unique identifiers: a Prefab ID for matching objects to their templates, and an Object ID that represents an instance. It also has a marker that indicates whether the instance was pre-placed in a scene or not, plus three abstract base methods that Save, Restore and Relink.
(Yes, this could be an interface, but as we’ll see later, I am a monster.)
Although all the identifiers could be added by hand, the scalability of a system like this hangs on automation. Luckily Unity provides mechanisms to hook into bits of the editor, so I was able to write two scripts to keep on top of all the IDs:
- An import post-processor script that assigns the unique Prefab ID to every saveable/loadable prefab as it is saved for the first time (i.e. the Prefab ID is null/empty; I also have a reset script that blanks the IDs causing them to be regenerated, just in case). I have put all my loadable prefabs into the Resources folder because I am a monster — part of the ID-assignment script also saves a map of these prefab IDs to the file paths required to invoke Resources.Load on them, but I’m sure this could be extended to use asset bundles instead.
- A scene pre-saving script that assigns Object IDs to every instance of a saveable component in the scene, and marks them as having been pre-placed. This means objects that already existed can have their properties reinstated during the loading process (or they can be destroyed).
As far as “unique” identifiers goes, I am incrementing an integer every time I assign an ID, and I store the current top in a text file. As long as I don’t go over however many billions of numbers fit in an integer, this should be fine! (I am deliberately not using file paths because those can get long and confusing, but the asset post-processor hook provides information about moved and renamed assets too so there’s no reason why it couldn’t work.)
Play Time Stuff
There are only two elements required during play time to keep this system moving, which is good because it means all the performance-heavy junk is reserved for save and load time — behind a wait screen where we can afford to let the game stutter a bit.
- Assign new Object IDs to things that are instantiated. They’ll come to life with their Prefab IDs, but will need to be given Object IDs so that instances can be linked to each other.
- Note the Object IDs of things that have died. If something was spawned during play time and then destroyed later, it can be ignored — that’s as if it never existed anyway. However, if an object was pre-placed, you need to know that it died so you can blat it when reloading.
When I finally save a level, I do a very dirty FindAllObjectsOfType on the base class and then iterate over all of those objects to perform the actual saving.
(For my current setup this is actually blazing fast, but it’s naturally going to scale with the number of objects you have floating around. Apparently I don’t actually have that many, it seems I have opted for fewer complex entities rather than splurging on simple ones. If it is slow, I don’t think you can make this asynchronous as it relies on getting a snapshot of game state — if things could change during the process they might get dangerous. Even though this all occurs over a single frame, I pause my game by setting time scale to 0 just to be sure.)
The Save method returns an instance of a plain C# class that is serialisable, comprised of lists of key/value pairs of primitive types. It contains separate lists for different types; one for integers, one for strings, etc. This means my saved data class is probably very memory-inefficient, but it is extremely flexible.
Each component’s Save method converts its private state into flat values that are named and plopped into the serialisable class. Because the component knows its own values and their types it can put them straight into the appropriate bucket. You might be able to make this generic rather than having a bank of explicitly typed lists, but generics can get painful when you’re contorting like this so I opted for the simplest option.
Reference variables have to be saved differently. This is where the unique Object IDs come in — these can be saved as strings into the string bucket as flat data.
It is also critical that, for spawned objects, the ID of the prefab that was used to create them goes in too. This means we’ll be able to reinstate the object directly from its saved data without resorting to any level-specific look-ups.
Yes, this is the kicker — I had to write a specific save method for every single saveable component. Since Exon is now more than five years old and rather complex, this topped out at about 50 different classes. Yes, it was painful, but if you want to save and load your game, you’re going to have to suck this up. You can make trade-offs here, though — components that look nice but have no gameplay impact, like the wreckage of my dead mechs, can probably be ignored. I can also rely on UI state and other more flexible top-level gubbins to reassert themselves when given the restored level data.
(In the end I did save wrecks because I’m daft. On the other hand, I decided that I didn’t need to save my melee attack system because I didn’t want to open the can of worms of trying to serialise and then reassert deeper animation states from Unity’s dark heart. Have I mentioned that my melee attack system is heavily animation-based? Yes, as I said, I’m a monster.)
You probably could automate this somehow with reflection or whatnot, but I felt that doing it by hand was ultimately the safest way — it also gave me a decent excuse to chop up and simplify some older chunks of logic along the way (and straight-up delete some obsolete classes). I also suspect that just knuckling down and battering through this was much faster than writing a whole other automagic system would have been.
There is only one saveable component on each prefab. I’ve given these “primary” components the responsibility of also saving the data of any sub-components (e.g. my Unit is a primary component that also manages the saved data of its UnitController and BotNavigator and so on). This is important so that each prefab can only have one ID, so there is no confusion between components when loading. (This caused me some major headaches during development, when my primary component selector was a touch ambiguous — be careful!)
Once every component has been turned into a flat, serialisable data structure, these are packed into a further class that represents the level as a whole. There is one important distinction at this point: the level data is split into data for preplaced objects and data for objects that were spawned at run-time. This means that, once the scene has loaded, we will know what already exists and needs to be updated versus what needs to be completely regenerated.
Once that level file is ready, it can be serialised to disk as JSON. JSON leaves my files open to hacking, but it’s a singleplayer game so I’m not all that fussed. Besides, right now, I need the ability to easily match file data to the live game for debugging purposes.
So with a big pile of JSON on disk that’s associated with some level, it’s time to reverse the process. Load your scene as you would normally do, then once it’s ready…
The first step is reinstating all the flat, primitive data for objects that were pre-placed and quietly deleting anything the player had already eliminated. I walk over the list of data blobs for pre-placed objects, and then find the objects to which they should be applied. This is a straight reversal of the save method: everywhere a value was added to the saved data before, it’s read out here in the Restore method (except reference values, which need to come later).
The second step is to regenerate all the objects that were spawned during run-time. We can now iterate through the list of saved objects and use their prefab IDs to load up the prefabs and instantiate the appropriate objects, immediately populating them with their primitive values as for the preplaced objects.
The next step is what I call the “relinking” step. This is how we reinstate all the in-memory references. We have to do this after all objects have been recreated so that we don’t trip up trying to link things that don’t yet exist.
As every object is loaded, I plop it into a dictionary that maps the Object ID to the new primary component — so when a component needs to relink to another, it looks up the ID stored in its own data and finds the reborn version of the thing that was on the other side. I have absolutely no type safety around this, because I trust a component that has saved its own data knows how to load that data again (e.g. a private field that was a Unit reference is guaranteed to resolve back to a Unit reference, unless some IDs don’t match in which case it’s all guffed anyway).
As with saving, this required me to manually write Restore and Relink methods for every single component that has a Save method.
I don’t think this system is particularly version independent. If you rename a property, then any older saved data might no longer be compatible — however, if it’s a primitive value that can recover gracefully by going back to a sensible default, it might be fine (I’m not sure I’ll advertise this feature as it could lead to a whole world of Interesting bugs, but it might avoid total losses for people if I gaff later on). If you change the prefab ID of something, old saves will definitely die (but if you knew the IDs on both sides, they could be recovered). I have not addressed this problem yet because I haven’t released anything to give a version number, but it can’t be much more difficult than putting a metadata block in which can be checked when loading.
Anyway, that’s the important bits of the system — there are loads of minutiae surrounding it, but I suspect that’s where we drift from the general approach to the specifics of my engine. I may also have forgotten bits because although the top-level process is simple there are a thousand little niggles on the way down. Either way, if you’d like to know more, ask me anything!