Posted On: 2019-07-15
Over the past week, I've been architecting and developing the save system for my main project. Although save systems are typically very project-specific, after reviewing what I've created, I have found that the architecture itself seems to be project-agnostic. In light of that, I will explore the architecture of my save system in some detail, in the hopes that it is useful, or at least interesting.
Before getting into the architecture proper, I will go over a few of the constraints and assumptions that defined how I architected the save system:
To best illustrate how the architecture works, I will walk through an example: consider a character picking up a coin. At the moment the character picks up the coin, the coin should disappear, and the number associated with the character's money should increase. In order to save that the character has picked up the coin, both pieces of information need to be recorded in the save file (the LiteDB database.)
Each object in the game is associated with a "Persistence" script. This script is responsible for taking a change that happened in the game (ie. disappearing coin) and translating that into data that can be saved. In the case of the coin, the change that must be recorded is the coin being disabled*. To accommodate this, the Persistence script observes the change to the "enabled" field (which changed to "false") and relays this change on to a separate system: the "Scene Change Watcher".
There is only ever one "Scene Change Watcher" in a scene, and it is responsible for collecting information about all the changes that need to be saved. As illustrated in the coin example, the "Scene Change Watcher" doesn't detect changes, rather, it relies on other scripts telling it whenever a change has occurred. It collects that information, and waits for another script (which will be described later) to tell it to save those changes.
One important detail about how the "Scene Change Watcher" and the "Persistence" interact: there are many different types of "Persistence" scripts, and the "Scene Change Watcher" needs to be aware of each kind**, in order to make sure it passes the correct information to the save system. To understand why this is important, consider the character that is picking up the coin: that character has a "money" property which will increase (or possibly decrease) throughout the game. In contrast, the coin has no such property, it only has the enabled/disabled property to track. As such, the "Persistence" for the character and the "Persistence" for the coin are dramatically different.***
The last script related to saving that is in the Game scene**** is the "Auto Save." The details of this script are not particularly important (it will likely change as the project grows) however, the one thing that is important is that it is responsible for notifying the "Scene Change Watcher" whenever it is time to save changes. If this architecture is used in other projects, the "Auto Save" script can be easily swapped out as needed (for example: swapping to a script that allows players to manually save the game.)
Although this describes all the scripts in the Game scene, this does not fully describe the how the game is saved. To better understand that, we will need to step back a bit further, and look at a set of scripts that are loaded when the game starts: the Title scene of the game.
Visual representation of the scripts in the Game scene. The arrows represent what each script depends upon, and the arrows at the top refer to objects in other scenes.
The Game scene is not the only active scene - the Title scene* is also running scripts at the same time. For the purposes of the save system, the Title scene contains all the scripts associated with actually writing the save data to the database. Those scripts will remain available even when other scenes are loaded, so that the scripts in other scenes (such as the Game scene) can use them.
The "Save Slot" script is the central location for all saving and loading operations. Scripts in other scenes (such as the "Scene Change Watcher") will reference and use the "Save Slot" to save data. Since the "Save Slot" is so highly visible, it actually contains very little logic. All the technical details associated with saving are in a separate class: the "World State". For example, when the "Scene Change Watcher" saves a change, it uses the "Save Slot" to get the "World State", and then tells the "World State" which change to save.**
The "World State" is the closest that any of my code will get to the actual save file on the file system - it relies on LiteDB for the actual file operations. In fact, the vast majority of the code in the "World State" interacts directly with the LiteDB database: for example, when saving a change to a character's total money, the "World State" will look up that character in the Database, set the total money to the correct value, and then tell the database to update the record to match the new information.***
Visual representation of the scripts in the Title scene. Arrows coming in from the bottom indicate objects in other scenes that depend on this scene.
Between the Title scene and the Game scene, all the scripts related to saving are available. To recap: individual "Persistence" scripts track changes in Unity, and report them to the "Scene Change Watcher". The "Auto Save" tells the "Scene Change Watcher" when to save the outstanding changes, and the "Scene Change Watcher" uses the "Save Slot" to locate the "World State". It then tells the "World State" which changes need to be saved, and the "World State" takes care of coordinating with LiteDB to update the save file.
Saving data is only half the story, however. Next week, I will explore the other half of the save system: loading the save data. It will build upon the concepts laid out in this post to show how the save data is used to populate the Game scene with objects. As something of a teaser, here is the full architecture diagram, including the Preload scene which will be explored in detail next week:
The complete save and load architecture, including the Preload scene, which will be covered in next week's article about loading save data.