Posted On: 2022-10-31
I've written previously about the save system I'm using in my project - first a two-parter about the architecture, and then later a post about a surprise challenge and a follow-up about solving that challenge. I'm currently in the process of moving the save system into a separate library*, and thought I should write a short piece about how taking the time to design things is making this faster and easier - and creating the opportunity to make some big improvements along the way.
My project focuses (both mechanically and narratively) on persistence, so the save system needs to track a lot of information for everything in the game. Although I currently use LiteDB to store that information, the save system is generally designed to be implementation agnostic: in theory, I should be able to swap LiteDB out for something else (ie. SQLite) while preserving as much of the existing code as possible. The save system is designed to use only simple Data Transfer Objects (DTOs), that do nothing more than organize the data for saving/loading, and, to keep things tidy, the various components/objects/etc. that are actually used in the project itself are responsible for reporting their respective changes to the save system.
Although easy to describe, the implementation for the save system in the project was, admittedly, a bit messy. Many of the earliest properties in the DTO were set using "persistence" components, which, while technically functional, were a holdover from an earlier design where the save system needed to understand how every object was organized in order to accurately save it. Moving to a separate library creates an excellent opportunity to leave that old code behind - and it's proving to be trivially simple to do so. Delegating data mapping to interfaces means that the code in the save library can exclusively work with DTOs, and the actual game's implementation in Unity can easily implement those interfaces without touching (or even thinking about) the code in the save library. What's more, using a separate library for the save system doesn't mean just using one library - the design naturally lends itself to keeping everything that's LiteDB-specific together in a 2nd library, making it even easier to swap that out, should it become necessary.
While much of moving the code to a new libary is simple rewriting - keeping (and cleaning up) the necessary parts while ignoring anything that doesn't belong in the save system - using a separate library like this is opening up new opportunities for automated testing. I've mentioned previously how getting code out of Unity and into separate libraries is a prerequisite for proper automated unit testing, and this change is opening up new possibilities as far as what I can test and how thorough my tests can be. Additionally, by being more rigorous in the separation between what's a part of the save system versus what's a part of the game's logic, I can better stabilize the save system: even as the game's logic changes, saving will continue to work exactly as intended. This will be especially important when I (eventually) tackle versioning and save upgrades: having users lose their progress due to an automatic update is a nightmare scenario, and rigorous automated testing will be an essential tool in avoiding it.
Having a concrete, simple-to-describe design makes implementing the save system much simpler. Unfortunately, the fact that the original implementation predated fully understanding all the constraints meant that there was room for improvement, and I'm happy to have this opportunity to make those changes in a way that also aligns with larger changes that are otherwise necessary for my project. Being able to fully unit test the save system is a massive boon - one which I expect will become essential once the game's scripts start to read (and modify) the plethora of data that's being persisted. Overall, I'm really happy with these changes - which is especially important, given that this is just the tip of the iceberg. I hope you'll join me next week to learn about these larger project changes, and why they're happening now.