Savegames. I avoided them with the first three Ines games with the (barely defendable) excuse that they are short enough to be ok without them. Our latest point & click adventure game The founders of [redacted], however, is long enough that we needed to put together a puzzle dependency chart to keep track of things. I knew from the beginning that this time I would have to finally allow saving. This blog post documents how I did it.
As with many of the things that I’m doing, a huge shout-out to Ron Gilbert and his Thimbleweed Park development blog, which inspired my approach.
Saving and loading
The most important question to answer when designing a save/load system is “what data do we need to save?”. Following the Thimbleweed Park approach, I’m not trying to save minutiae such as the current frame of an animation, or the state of a coroutine. The data I need to recreate the state of the game is the following:
-
A global hash table called
state
, whose entries are mostly booleans, and in a few cases numbers or strings. This keeps track of things that have happened during the games, such as whether the player opened a certain door, talked with a character for the first time, solved a puzzle, etc. -
The current position of the player character (Ines), and the direction she is facing.
-
Which room we currently are in.
-
What’s in the inventory.
-
The state of the dialogue trees. This could in theory be done by saving any variables needed by the dialogue trees in the state table, but I designed them so that they could hold their own state1, so we need to extract it from there.
That’s it, we don’t need anything else. And you thought this was going to be difficult.
Here’s the code for saving the state, written in my scripting language Hurdy2. This is the actual function that the engine uses, I only cut out the part that saves the image data for the savegame screenshot.
|
|
And here is the function to load a savegame. Note that this assumes that all the existing save files have already been loaded by the engine.
|
|
Some things to note here:
-
In addition to repopulating the data that we had previously saved, the
load
function also needs to reset some things, such as UI, (some) animation state of the player character, running threads (the background scripts that make stuff happen in the game without stopping it), and audio. -
The most important thing we do here is running the function
changeRoom
. This is where the game is actually put in a state that resembles what it was when we saved it. See the next section for details.
To get an idea of what things look like, here is an example of what the data we are saving looks like.
|
|
Re-entering the room
The save
and load
functions look surprisingly easy, don’t they? How come it took me three games before getting here!?
Well, the non-obvious thing here is that the game must be structured correctly for the load
function to actually work.
My previous games are not structured correctly—I cannot just drop these functions in and magically add savegames to those games.
I will eventually revisit them to implement this, but it will require quite a bit of effort.
So how do you structure the code correctly? The trick is this: every time we enter a room in the game, we rebuild all of its state from scratch. This includes:
- starting the music (depending on the circumstances we may not do anything here if the correct one is already playing)
- positioning characters and resetting any custom states
- showing/hiding objects based on whether they have been picked up
- opening/closing doors as needed
- starting any room threads, such as stars flickering
All of this is done conditionally based on the global state in the state
table. Then when a game is loaded and we use changeRoom
, everything is recreated
and looks like it did when we saved (not exactly the same, but close enough to be happy about it).
This is the structure that’s missing from my previous games. There the rooms are only populated once at the beginning of the game, and changes (like hiding objects) are made once as needed. It probably wouldn’t take too long to refactor everything as needed, but that’s something to look into after the current game is done.
And now, some code to give an idea of how a room is set up.
|
|
|
|
Final thoughts
The most important thing I learned from this experience is that savegames are something that needs to be planned at the very beginning, they can’t just be stapled on top of what you already have.
I also found the approach with rebuilding the room every time we enter to be extremely useful for testing, regardless of whether we use it for saving or not. I set up by debug GUI so that I can toggle on and off all the state variables and reload the room on changes, which allowed me to get to virtually any point in the game without having to play through it, by just setting all the variables that would get me to the particular state I wanted. I highly recommend it.