So, I’ve been fairly quiet recently because 1) developing saving and loading systems has been a long invisible slog and 2) I’ve been replaying Morrowind (oops).
But with all the data that comprises a “campaign” in Exon being successfully written to disc, it was finally time to start trying to load it. Since I’ve told you the story of decomposing the world into text files in more than enough detail, it’s time to begin the story of reconstructing it…
Load the Data
When I say “all the data” being written, I was of course being a little economical with the truth, as when I started my first steps into reading it back in I immediately discovered several properties that I’d missed. It’s going to be a long road, isn’t it?
As with all the previous steps in this process, the ultimate plan is deceptively simple, but is fraught with so many tiny little nuances that it’s a mammoth task.
1. Clear the Decks
As the game is going to function over the course of many different levels, I need to do a good deal of cleaning-up before I even start loading a saved game. There are several persistent campaign-level objects that hang around with faction and character data, plus the player’s character itself, that need to be blatted so that nothing can leak through.
In order to ensure everything is torn down in the right order, I’m transitioning to an empty scene before actually doing anything specific — this should ensure that any level logic stops talking up to the campaign elements before those are destroyed. Then I need to unhook the UI from the player character so it doesn’t throw a hissy-fit when the hero is erased from existence. So many things that I call once to initialise the game, written years ago and barely touched since, suddenly have to be remembered and unhooked!
2. Load Campaign Data
Then it’s time to bring back the global campaign data. The faction and character data is the easiest stuff to reload because it’s already just numbers and strings — it’s raw data rather than inter-object relationships. The same goes for persistent quest variables.
This also needs to be loaded before I actually send the player back to the level they were in, because that level will need to refer to this global data in its own initialisation flow.
3. Load the Hero
Loading the hero is where things get interesting. The player’s mech exists outside of time, so it automatically persists between levels. By loading it here, I am ensuring that returning to the level will behave exactly the same if you’re reloading it as when you’re entering it for the first time — in both cases, with a hero already in existence.
The thing about the hero is that it’s not just easily-serialisable primitive data. It’s composed of a network of components — the Unit itself talks has an AmmunitionStockpile and a ManaPool and six Abilities and more, and its visual appearance is defined by a set of UnitChunks which have each been painted with a particular Material…
My plan to handle this first involved assigning every prefab a unique identifier. When I save the hero’s data, all of its objects go in with the relevant identifiers, so I have an instruction set of which source objects were used to assemble the unit plus all the internal values that had accrued during their time in the game world.
However, Unity only allows hot-loading of objects that are inside a Resources folder and only by their file path, not by some arbitrary identifier. (There are lots of dire warnings about using the Resources folder and I well understand them — it takes a heavy toll on the frame-rate to be directly spinning up the hard drive like this. However, I am not loading Resources at “run-time” per se; I am bringing them up behind a loading screen, which is already chugging for many other initialisation reasons. Basically, I can afford the performance hit at this time in the game’s lifecycle.)
In order to map between those identifiers and the actual objects they refer to, I have plumped for reinventing Warcraft III‘s concept of a Listfile. I had already made a load of editor scripts that automatically assign prefab identifiers, so I extended this to also save that identifier and the file path of its object into into my new listfiles.
Thus when I spin through the saved data for the hero, I can take each identifier to the listfile to get the actual object which can then be safely bring to life. Rinse and repeat for all the chunks, their paintwork and the items the hero was carrying.
4. Load the Level
With the prerequisites in place, it’s time to load the level. Since the campaign data is up, any components that care can immediately know if the level has been visited before, and thus whether to initialise as normal or to wait for restoration. (This is most pertinent to my “preplaced unit” markers — on first visit, they should spawn a fresh unit, but on subsequent visits, those units will be restored independently unless they have already been killed.)
Once the scene is up and running, it’s first a matter of first spinning through every object that’s there and refreshing it with the saved data, then spinning through every object that wasn’t there and recreating it. (I make this sound simple but it’s really not — every component has matching Save and Restore methods that are specific to them so there’s a whole heap of code underlying this tidy little top-level loop.)
5. Relink Objects
There are two kinds of data in the world — flat values like amounts of health and timers, and links between two live objects. Objects are always going to be recreated in non-deterministic order, so while loading the level initially it is not possible to link objects to each other — as one side of any given relationship might not exist yet.
Thus there are two steps to loading a particular object; Restore and Relink. The first brings it back to life with all its flat values, the second searches the database of restored objects to turn all those meaningless identifiers back into real in-memory references.
6. Fix All the Broken Bits
As I said, the overall plan is simple but the number of weird little nuances and edge cases is monstrous, so I’ll likely be fiddling with it for weeks before it actually successfully loads a game. No rest for the wicked!