Posted On: 2019-10-14
Maintaining a project over the long term requires constraints. These constraints can be affect anything from which technology stack to what kind of development processes should be supported. Constraints can also vary in scope, they can range from business-wide (such as "use the same engine for all projects") all the way down to task-specific (such as "this task should not add any new external dependencies.")
When designing for an ambiguous target like "fun" or "thought-provoking", constraints often play more of a role in design than requirements - for example, I will iterate on a satisfying-feeling movement set first, using the constraints to guide the design, and then once that is complete I will extract level design requirements from the character's movement (such as "must be traversable for characters that jump no more than 4 units high.") In an earlier blog post about designing the save architecture, I enumerated the constraints guiding the save (and load) system I was designing at the time - focusing entirely on the constraints rather than requirements. Unfortunately, there were two additional (unanticipated) constraints affecting the save system - and the recent discovery of these two constraints has cast doubt on the fitness of the save system as a whole.
The first missing constraint was the idea that every change to the game world should be persisted in the save. This is perhaps best illustrated by an example: if the player is pushing crates to solve a puzzle (such as in a Sokoban game) then the possible changes include the character's position and the position of each crate. If the save system is written to preserve these changes, then the player can choose to save and stop playing at any time, and when they return the character and crates will be in the exact position where the player last left them. This is quite different from how many games (including Sokoban-style puzzle games) handle saving: the player's progress through the game is saved (which levels were completed) but the exact position of objects is not saved. Each approach has its advantages - saving everything means that the player can stop at any time and pick up right where they left off , but also means that when the player revisits previously-solved puzzles, all the crates will be in their last position: the solution to the puzzle. Saving only player progress means that leaving and coming back will reset the puzzle, but it also means that going back to earlier puzzles gives the player the chance replay the puzzle again from the beginning.*
Regarding why I want to save everything, it is my hope that doing so will reinforce the consequences of both of player action and inaction. Much of what I have been designing for the upcoming prototype includes ways for the player to impact the world, and having that impact be preserved across saves makes it possible for it to have meaning*. Additionally, it is my hope that the world state itself will become a chronicle of the events of the game - both those involving the player and those which happen off-screen. Ideally, this could become a sort of dynamic environmental storytelling, where player attention to the environment can enable them to read the subtext of a narrative which they themselves are influencing.
In order to realize the goal of saving everything, it falls to me to write all the code necessary for each individual thing that will be saved. While not technically difficult, it is tedious and error-prone, which is especially dangerous as save-related bug are a particularly difficult category to diagnose. Such an approach is an opt-in design - each individual change opts into being saved by having additional code written to accommodate it.
The opposite of the opt-in approach is the opt-out approach. In such an approach, everything is automatically saved by default, and individual elements that should not be saved must be explicitly flagged for omission. Opt-out approaches are also subject to human error (such as forgetting to omit something) but the impact of such mistakes is often reduced performance rather than actual logic errors*. In the past I have designed and used opt-out systems for (non-game-dev) tasks that involved contributions from individuals with mixed awareness of the underlying system. Having those systems work by default (albeit sometimes slower) was a massive boon both for product stability and developer productivity.
Unfortunately, designing the save system without an awareness of these constraints meant that I designed a system that was opt-in, and the system has already produced a number of unpleasant errors (environment elements disappearing and previously deceased characters walking around with 0 hp.) Even as I work through squashing each of these bugs, I am keenly aware that, so long as my system remains opt-in, I should expect more bugs in the future. The experience is (apparently) the classical definition of technical debt: I originally designed my save system with incomplete knowledge, and, having gained new knowledge, I must choose whether to pay the large cost to rewrite it, or keep paying interest (bug-fixing, etc.) on every feature until I do. For now, I am choosing to pay the interest (so that I can see the results of my most recent work) but, as I intend to pay the debt off soon, I am also starting design work on how to make it opt-out. I expect this to be a more complex task than writing the opt-in version (not least because it involves arbitrary third-party classes) but I am confident it is at least technically possible.
Constraints play a key role in designing maintainable systems. As demonstrated with my save system, missing a constraint up-front can lead to inadequate solutions that require rework down the line. That being said, in many cases it is not possible to get all the constraints correct on the first try, particularly when the constraints involve the user/developer experience. As such, I don't feel particularly bad about the situation with the save system: I put in a lot of design work up-front, but I missed some constraints nonetheless. Realistically, it's quite possible that the same thing will happen again as I work on the new design. I will certainly do my best to spot everything this time through, but sometimes there are holes you can only see when you step into them. Such is the nature of programming.