When I decided that procedural generation was too much bother for not enough gain and switched over to hand-crafting scenarios, I figured that was the worst of my development headaches gone. After all, the more data that is static, the less you have to worry about while the game is actually turned on.
Then I realised that you’re still going to die a reasonable amount of times during the game. Then I realised that although the levels will be individually smallish, they’ll still be quite open.
What does death plus openness equal? Why, my dear, it means I must let you save and load your game, because fuck losing all that progress (to death or dinner).
Saving and Loading
I have to admit that the question of saving and loading data is one that’s blindsided me somewhat. I’ve been happily working on my “vertical slice” scenario, a chunky little singleplayer adventure that will be composed of a hub area, a tutorial, a proper mission and maybe a deathmatch arena. Subconsciously, I expected each component scene to be small enough that I’d just save the checkpoints and be easily able to regenerate enough game state from basic parameters that I could get away with it.
Then I was working on the dialogue system and made a little side quest and… oh. Oh.
Yes, if you finished a side quest, cleaned out an area, found a cool new weapon and then lost that progress while meandering through a reasonably-sized open platter, yes, that would be shit. It’s exactly the problem that marred the otherwise fairly delightful Urban Chaos, which is a game from the late 90s and therefore had a technological excuse. I do not have any excuses. (Except that I am a single man working alone, but since when did that matter on the scales of truth and justice? Reliable saving and loading is just the Right Thing To Do.)
I think I’ve probably mentioned it before, but I do suspect that the procedural permadeath revolution in indie games is ultimately a gambit that allows developers to avoid making genuine saving and loading systems. If your character is a mere level with a couple of attributes on the side, it’s easy to save them in the abstract; and if the level is brand new every time and lacking in any narrative depth then it’s easier to handwave the replaying of it. After all, in a good procedural game, replaying is kind of the point.
While that scratches a certain kind of itch, my heart has always belonged to the giant singlplayer clunkers where everything remains where I moved it, no matter how far I progress through the game. In hindsight, it was inevitable that I’d have to solve this problem — I just wasn’t quite ready for it to pounce at crimbo time. Supposed to be a holiday, right?
I guess the problem is that saving and loading isn’t like any other other technical challenges I’ve tackled up to this point. Most things have been solvable by just jumping in and battering at them until they look right — systems like AI, for example, are self-contained enough that you can build them up by adding features as you go.
Saving and loading, though, requires careful consideration — it must be tailor made to how I specifically have built my engine so far. This is not something one can pick off the shelf and have it “just work”. Which is annoying, because it means there are basically no tutorials floating around on the subject — most articles tell you to make Serializable classes and then wave you on your way. Well, yes, of course I need to serialise the data… but how will I stitch it all back together again?
After a few weeks of thought while replaying Baldur’s Gate, I believe I have the scope of the problem:
- I will need to disambiguate between pre-placed objects and objects created at run-time. These need to be saved and loaded in different ways: pre-placed objects need to be “updated” after the level has loaded, while run-time objects need to be brought back into existence.
- In order to recreate spawned objects, I’ll need to be able to link them back to their templates. A dynamic object needs to know where it came from, plus what few extra numbers make it unique. A projectile, for example, should have position, orientation, velocity and lifespan, while the rest of the data from its template are likely to be unchanged.
- Assuming that I can know what objects are pre-placed, these need to be indexed so that on loading the level, each object can then look up what modified properties it should have instead.
- Assuming that I can know what objects were generated mid-game, they need to be loaded from disk, potentially from levels that don’t have the faintest idea what they are (e.g. the player character and their equipment is carried through from previous levels).
That’s a fair morass of pain right there, and that’s likely not even all of it. Joy!
The other issue is that, whatever answer I come up with for these problems, it has to be one that can sustain a single man working alone in his bedroom, who is prey to mood swings and architectural blunders. While I can stomach save-game data not being compatible between different versions of the game, I cannot stomach the entire thing melting subtly for some players because I forgot to increment an identifier one day. It needs to be completely automatic.
Luckily, Unity does offer some assistance with all of its (barely-documented) editor scripting hooks.
The first problem of distinguishing pre-placed objects from run-time objects, for example, appears to be solvable by hooking into the EditorSceneManager.sceneSaving event. This fires as you hit Save but before the scene is actually written to disk, allowing you to twiddle with things. I’ve slotted a PreplacedObjectId property into relevant objects that is set by this mechanism, and nothing else ever, so I should be able to rely on this as a signifier of something having been placed by me at design time versus some object at run-time by its presence or absence. As long as I do actually save my changes, which feels like a fairly reliable hook.
Indexing prefabs is also deceptively easy. Creating a subclass of AssetPostprocessor gives you a hook that fires when any file is imported by Unity. Just like with my pre-placed objects, I can use this to detect prefabs (whose paths end in .prefab) and assign them PrefabId values — I can then use this to map objects in the actual game world to the templates that spawned them. (To ensure the id values are unique, I’m just incrementing an integer each time and keeping track of it in a spare text file. Assuming I don’t import two billion files, I should be fine.)
So now I’ve got a fairly safe indexing system, how can I use that to write game state that can then be reloaded safely?
… er… I haven’t quite thought that far ahead yet… so… umm… if you’ve got any ideas… I’m thinking Resources.Load() with a mapping from id to paths?
9 thoughts on “Blog 755: Save The Data”
Have you figured out anything by now? If not, I can share how I’m running saving in my project with a pretty similar approach.
LikeLiked by 1 person
Well, I’ve worked out that using OnPostprocessAllAssets doesn’t work in practice — you end up with an infinite loop spiral of import-edit-save-import…, so that one’s out the window. 😦
I’m likely going to have to fall back to pressing a button that sweeps all assets, but that’s slow and annoying and makes me sad.
So yes, I’d love to hear how you’re doing it!
Why do you need to sweep all assets to index them, isn’t it enough to assign ids at creation? For the saving itself, we could maybe meet up on discord/’insert other platform’ to discuss it?
So far, I haven’t been able to find an editor event that reliably detects prefab creation. For example, if I duplicate an asset to then modify it, I need its id to be refreshed, but I haven’t been able to isolate this sort of case. The same goes for creating a prefab by dragging something out of the scene… So if you know how to detect these cases, I’m all ears.
As for discords, I guess the publically accessible ones I’m on are the Hive Workshop (https://discordapp.com/invite/6dnuydb, though I only really lurk there) and WC3C (https://discordapp.com/invite/u4yJbje) servers.
I don’t know about any event you could hook into for prefab creation, however, strictly for handling this inside the folders, you could try EditorApplication.projectWindowItemOnGUI with EditorApplication.ProjectWindowItemCallback. These are run for each object in the ProjectView, you could check the object instances against a database and assign ids when no corresponding object is found.
So it turns out there is no infinite loop with using OnPostprocessAllAssets, I was just a bad programmer and I feel bad. We’re back on track!
Nice! How far are you with saving/loading itself?
Hahahaha… I believe I have got everything identified now, so phase 2 is dismantling my character data and quest variables so they go somewhere easy to write. Then it’ll be on to writing data for preplaced objects, then dynamic objects like units… I suspect at this point that actually reloading any of this is month(s) away.
I’m using a class called SaveGame that simply stores a list of SaveScene objects. SaveScene is a class that contains information needed to restore scenes, like the name , skybox and a list of all saveable objects. The objects are saved in a SaveObject class containing such things as the name and an ID, used later for fixing references across the loading process, as well as a List for the data of components that need to be saved. Each component has a list with the first entry being the name and all other entries being data used for loading. All these classes can be easily serialized.
For getting the saveable game state, I use a manager class for every scene that has a list of Entity objects, which are placed on every saveable GameObject. The Entity class handles saving by getting the string lists from the components and creating a SaveObject. the managers use the SaveObjects and makes the SaveScenes, which are then bundled in the SaveGame and directly written to disk.
For loading I reconstruct the managers and GameObjects from the SaveScenes and Saveobjects, then load functions are called on scripts in multiple passes to fix references and load data.
Dynamic objects are added to the manager of the current active scene, being treated exactly like any other object.
Currently I load into empty scenes, but this could be adapted to load into existing scenes and overwrite existing components.
In my case, the saving and loading isn’t too difficult of a structure, with most work needed in the components themselves to provide methods for creating string lists they can load and save their data from//to.
I don’t know how applicable this approach is to you, but I’d be glad to help with any questions.