For the first 9 months of this project, Savegame was my evil nemesis. It taunted and poke at me, refusing to ever heed enough to grant me the slightest satisfaction of how well the engine was advancing. Savegame would would pop up and say "How are you going to save that?", and I would shrink down and reply "I have no idea." My only victory over Savegame was my ability to lock it up in a cage buried deep in the dark dungeon of my mind and pretend it wasn't there, tricking myself into believing the incessant howling was just the wind on a stormy night.
I knew I needed to confront this demon. When the cage could no longer hold the beast, it was going to be a battle that one of us would leave either dead or tamed.
The morning of the battle was as any morning was. Nothing about it hinted the confrontation that could — only hours later — lay waste to everything within it's reach.
It began as an idea: "Hey, wait... what if I..." and that slowly formed into a plan. Some might call the idea cheating, but honor in war is a myth; you do what you can to win and this was no different.
My idea was a weapon, a special weapon that not only could, but would, mean victory and the defeat of my nemesis, Savegame.
The problem with savegames in adventure games is a vast amount of information that needs to be saved. Adventure games aren't divided up into "levels" and I deplore the concept of "check points", especially in adventure games. Players should be able to save whenever they want and expect to be back exactly where they left off upon load.
The issue isn't file format. It's not about whether you store it in JSON, XML or raw binary. The file format is the easy part.
What is hard is iterating deep into the recesses of the game and gathering up every scrap of data that might need to be saved. It's not just a matter of saving some global variables that depict the state of the game, it's the much more complex matter of tripping through the animation system, remembering each frame an actor or object is displaying. Going through all the objects in the game and saving off their state and the state of every local variable they contain.
The way the Thimbleweed Park Engine works, is that each room in the game has a piece of code called the enter code:
Nickel function enter() {
// Code that inits the room when entering
}
// More stuff
}
This code is called whenever a room is entered or given focus. It's responsible for setting up the state of any objects or actors that might be dependent on an external state of the game, as well as starting any ambient animations or sounds.
The enter code goes back to the SCUMM system and I've always found it a convenient way to organize the state of a room. It keeps all the code about setting up the room in one place, rather than spreading it around various object and other rooms in the game.
When you enter the Nickel, the enter code looks at the state of the game and decides if Natalie is there and then places her in the right spot. It does the same thing for the photocopy machine and the stack of newspapers.
And this was my idea, my +5 to cleaving idea that would finally vanquish my persistent foe.
When a game loads, I just re-enter the room. It saved me (no pun intended) from having to save a huge minutiae of detail about the current room and, more importantly, the state of all the multi-tasking scripts that might be running. The enter code would just restart them as needed.
All I needed to do was save off five pieces of information. 1) Global variables, 2) Local variables for each room, 3) local variables for each actor, 4) Internal state of each actor and 5) Internal state of every object.
On load, I would just re-run the enter code for the room the player was currently in and everything would be set back the way it was.
Step one proved to be a tad difficult. There are a lot of global variables, most of which are internal to the Squirrel interpreter. I didn't need to save any of these, I just needed to save the global variables related to the game and I had no way to tell the difference. So, I did what I probably should have done to begin with, I moved all the game global variables in to a table called "g". Rather than "talked_to_sheriff" it was now "g.talked_to_sheriff."
All the rooms, objects and actors are also in the global namespace to make it easier for the gameplay programmer and I didn't need want to iterate through them from here, that would come later.
The other issue I faced was the data contain in each variable. Saving ints and floats is easy, but saving variables that possible contained actors, objects or rooms was a lot more complex.
Actors are defined as follows:
ray name = "Ray"
fullname = "Agent Ray"
detective = YES
ate_hotdog = NO
ordered_hotdog = NO
picked_up_tape = NO
flags = GIVEABLE|TALKABLE
// more stuff
}
actorCreate(ray)
The actorCreate(ray) command takes the table 'ray' and creates an internal actor and associates the table with it. Later on in the code, something like this might happen:
last_actor = ray
And here is the problem. Tables are saved as references, so the variables last_actor and ray really point to the same table, they are not copied on assignment. But, if the savegame system serialize both of those variables, they would become two completely different tables on load, not what we want.
Internally, actors (plus objects and rooms) are given a unique int id when they are created that the system uses to reference them, this is automatically added to the actor's table and called _id. This allows the engine to know the actor that a table is referring to. The problem is, I can't save those _id's out, because they are assigned at run time, and if the order of the actor assignment was changed during a patch, the numbers would all be wrong, not to mention that if a new variables were added during the patch, it would be erased on load. One of the main criteria for the savegame system is that it must survive patches and updates.
So what I really needed to save was the name of the table (ray), so when the savegame system de-serialized last_actor, it knew it was the actor ray and could just point to that master table again. The same is true for objects and rooms.
Since the engine keeps a list of all the actors, rooms and object independent from the tables in the code, after saving the global variables, I iterated through each of them, saving off the variables found in each table and also some internal state information that is not saved in the tables, things like x,y position, touchable, facing direction, etc.
All this information is saved in a JSON savegame file that looks like this:
{
currentRoom: "Diner"
gameTime: 156.52
inputState: 16
savetime: 1445006566
selectedActor: "ray"
version: 0
actors: {
bankmanager: {
_dir: 2
_pos: "{540,11}"
_roomKey: "Bank"
defaultVerb: 3
detective: 0
flags: 131072
name: "Mr. Cruffman"
sawRaysBadge: 0
sawReyesBadge: 0
dialog: NULL
last_room: {
_roomKey: "Bank"
}
}
...
The savegame system knows when it sees an entry like last_room, it should look up the master table for Bank and place it in the variable last_room.
When loading, it serialize all the data, merging it with existing variables or creating new ones if they don't exist, then it re-enters the room specified in currentRoom.
I also created two global functions...
function preSave() {
}
function postLoad() {
}
That get called, just in case there ends up being a complex situation that that needs attention. So far, there has been none, but the hooks might be useful.
I don't need to save the state of the sound system, because most sounds are restarted in the enter code and this reduced the complexity of the task immensely. When I implement the music system, I will need to save it's state because music sits above the current room, but that is a task for another day and shouldn't be too hard.
There are two types of multi-threaded functions in the game: local and global. Local multi-threaded functions are killed when you leave a room and restarted when you enter, so I didn't need to save these. Global multi-threaded functions needed to be saved, and I didn't (and still don't) have a good way to save these. It's not just a matter of saving a few local variables and the PC, there might be a complex nested call stack, each with it's own set a local variables and enclosures and it all needs to be saved and then recreated on load. I looked at the Squirrel source code and I don't see an easy way to serialize and recreate a running thread.
The good news is that we don't use global scripts very often and in each of those cases, they can be done in a different way, so I opted to just not save global threads. Problem solved. I'm sure it could be solved if I wanted to bang my head against the problem for a few weeks, but it's just not worth it.
There are two downsides to my "enter the room on loaded" scheme.
1. If an actor is walking across the room when you save, and then you load, they will stop walking. I don't see this as a huge downside. Chances are you're loading the game a few hours, it not days, later and I don't think you'll remember where they were headed. The enter code restarts any NPC, so they will behave normally.
2. You can't really save in the middle of cut-scenes as that would require you know where actors were walking and the exact state of the entire animation system at that moment, precisely what I was trying to avoid.
My solution to that problem was to silently save the game right before a cut-scene, and then if you save during the cut-scene, it actually saves the game saved right before. The downside is you might loose a little progress, but we're trying to keep all cut-scenes to less than 10 seconds, so you won't really loose much, and once again, I don't think most players will even notice and it saves a ton of time on my end that I'd rather be using for other things.
And that's the save game system.
Tada!
- Ron