Save Game
Oct 19, 2015
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:
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:
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:
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 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
good decision
currentRoom: "LunarSurface"
hasSpaceSuit: 0
mwahahahaha
I have two questions:
1. Is your engine able to save mid-dialog? I don't mean in the middle of talking, but if you are in the middle of a dialog puzzle, can you save to try out a few things?
2. Just a gimmick, but would it be possible to restore cutscenes by storing an "offset" of the cutscene progress and upon loading the save state right before the cutscene, just fast forward until where the player decided to pause the game and save explicitly?
Once again, I really like your solution and I am just being curious about the corner cases here. : )
2- That would be really hard. "offset" is a complex issue.
That is, call your global Update() multiple times (if you use fixed time steps) up to the desired offset,
bypassing your rendering stage and any synchronization with clock.
So start from the beginning of the cut-scene, and Update() until the named event
(e.g. if the event "4th-sentence" does not exist anymore because of a patch, do not skip).
Your approach is pure genius. The exact kind of compromise only experience gains you. Instead of solving hard, and ultimately pointless and problems you cut the problem exactly at the right part. Really clever!
TL;DR more specific saving needs more data: more data means more ways for things to go bork!
Also, you can't save all of the data in the game because some of it is related to things that are only true for the current time.
E.g. if you remember that the room you get to when going through the "shop door" is the "shop inside", you remember at which point in memory (RAM) that other object was loaded. Now if you shut down your computer and restart it and then start the game again, it might get loaded into a different location in RAM (e.g. because you viewed a website before you played the game the first time, and that used up the memory the game is now in after the restart). So you have to be very careful which parts you save.
Some things you can just change (e.g. save the room's name and then when loading look up what address the room with that name was loaded to), but others you can't. There are many special cases. Thimbleweed Park is working on a shoestring budget, so they take shortcuts so they don't have to figure out a special way to handle each issue, if there is one way that covers 90%, and the 10% are what people rarely do anyway.
It gets even worse when you consider things under the operating system's control. I.e. a savegame should depend on what sound card is attached or how many screens. If you just saved out the entire game frozen in time, then tried to continue it on another computer, it would suddenly find a completely different sound card is installed. Maybe it's stereo and mono, but the game is still feeding it only one channel audio ...
It's like running across an open field and suddenly being teleported into a forest. You'd run into a tree. That's what you're doing with a game, so you kinda need a way to tell it "you've just been teleported". In most cases, you can just tell them, they'll look around, reorient themselves and then go on. But if it's in the middle of a step or they've been running (i.e. cutscene), you can't stop them in mid-air, and can't abruptly decelerate them. There are solutions to work around that, but it's much easier to just restart at the beginning of the step.
Does that in any way help explain it ... ? :-)
I ran into this issue with a game I developed recently, but since its visibility was low I never got beyond the brute force method of saving. This means I ran into the very problems you described :P
Basically, you can't just "image" the whole game, but instead you have to create a system that backs up the game on command.
PS: thanks for using comments in your code!
Doesn't squirrel model a virtual machine? How about serializing the virtual machine (maybe gzipped), and just restoring it?
Uhm... yeah, and reloading all native objects (the ones created by C++ code). If squirrel functions use dependency injection (or a helper function to get the native objects), that should be OK...
But squirrel threads would be part of the VM serialized and restored, and also the execution context...
So it will provide an easy way for you. Thank you for your comment, Ron! :-)
Though, if you have a "fixed variable", like I don't know, the actor's name, and for some reason you need to change its default value in an update, wouldn't the savegame value get in the way?
Yeah, I think of the few players who actually save in the middle of a cut scene, most will appreciate the "previously on" feature. :-) Cutscenes are just for watching, so it's not a problem. At worst, one could break up a longer cutscene into several shorter ones to "fake" being able to save halfway through it. Conversations on the other hand get really annoying if you have to navigate the entire tree and give a bunch of answers again, just because you took a call on your iPhone which quit the game to make room for the phone app in memory.
So as long as conversations can be restored or are very short, barely anyone will notice.
On that note, if you use a single autosave file, make sure to save somewhere else then replace the existing autosave. I've made this mistake with my own programs and no one enjoys a corrupted save file if the game crashes during the save process and there isn't a backup.
I don't use anything in clouds!
And autosaves must be also possible if the machine is offline.
- Include code to do a silent in-memory conversion from a version 1 save game into a version 2 save game (and more functions to go from version 2 to 3, and from 3 to 4, which you would call incrementally if required)?
- Convert all existing save games to the newest version when TP is first started after patching?
- Or would you simply always use default values for the missing variables in old save games - could be tricky if they depend on other values?
Maybe the save file can have a version number
corresponding to the version of the game
installed? Here's some items from version 1:
1 sword
1 hat
1 phone
The game is updated to version two and now
one object is removed and another is added:
1 2 sword
1 2 hat
1 phone
2 joystick
Now the items are marked as they are in version
1-2, but the phone doesn't have a 2 mark which
means version 2 of the game will not try to load
that item in the players inventory.
If you get a save file from a friend that is using
version 2 of the game, then your version 1 of
the game will simply ignore the version 2
items because the code said to ignore items
higher than the game's current version number.
If you remove a room in version 2, then the
characters can maybe get moved to a default
location or something like that?
I don't know if any of this makes sense, I
just want to try to help out.
A function that, when loading a game, calculates an initial value for those variables that were introduced post-V1 could work as this function could use all existing variables. It would also force the programmer (a.k.a. Ron) to think hard about all new variables and what value it should have for existing save games, thus ensuring that it's not an afterthought.
So any added state will just magically "be there" in the ideal case. Bigger problem is if isDetective has been saved to disk and then a bugfix requires for it to be "yes" on one character. But you could work around that by just renaming it to isADetective and then ignoring the value.
So, new dynamic values just get the state that they'd have a new game, which is fine because they weren't there when the user first played them. New dynamic values that depend on other state get updated as they're loaded anyway. New static values are "just there". It's actually quite beautiful !
Thanks for another interesting update!
What about "sq" in sq_poptop(vm)? hsq?
vm? surely not Virtual Machine? This is some kind of generic or dynamic object type?
I guess I could find maybe out if I read into Lua or Squirrel. Indulge me, if you don't object to my laziness.
I think that situations like the player being able to mess around with e.g. a NPC walking are similar. It's fun to try to mess with them and if the designers included special text or script actions for such situations it feels really nice to discover.
So of course, use cutscenes to solve these design issues whereever you want, but in case you have to do lots of extra work for workarounds and more complex scripts (also in the testing department) to handle such situations, there will be people who really appreciate it. It makes the game somewhat more alive and rememberable, just like the bus driver in Zak.
I did it exactly like that in 'Caren and the Tangled Tentacles' (there is a Youtube vid of the released version in the link :)
There it meant to also store the 'last room' since the 'entry behavior' for each room depends on where we came from.
It boiled down the savegame data to all global variables.
It would be nice if you could switch app, without losing your progress in the game.
- Pick up Squirrel
- Use Squirrel on complex nested call stack
- Talk to JSON
- Give complex nested call stack with Squirrel in the middle to JSON
- Pick up save game
Done.
Mr. Gilbert, don't feel too bad. You may be very good at writing them, but playing adventure games is a whole other kettle o' fish! LOL!
-dZ.
I remember the same first "downside" occurred in Maniac Mansion and Zak: if a character was walking and I wanted to save the game, when I reloaded the game, the character was standing, faced towards its direction. I think this is not an issue. Even better: it's in perfect 1987 style!
*ducks*
However, the game itself seems like it would be a game run (or installed) from a CD-ROM. No way this game could even seemingly fit on a stack of 5 1/4" floppies. :)
I'm sure whatever save screen Ron goes with will end up being neato.
For my (very simple) game, I ended up storing all player actions that are relevant to a puzzle (aside from moving between scenes, whether an action does something or not is determined by whether it's part of a puzzle) and writing that out to file as the current state of in-progress puzzles plus a list of completed puzzles. When the player loads, I have my StateManager class walk through all of the actions implied by the loaded puzzle states to arrive at the game state the player left at, then I plonk them down in the scene they were in when they saved.
That assumes that the order that the puzzle states were saved in is appropriate, which feels fine since it's whatever order the player completed them in, plus I can force/respond to specific ordering by shuffling my puzzle dependencies. It also means I don't need to care about inventory items and stuff since they're effectively cosmetic, with direct puzzle dependency (eg: throwing a snowball requires the "pickup snow" puzzle to be completed first) filling in the logic.
One question, probably a very silly one, but is there any risk for TP of there being a so-called "memory leak", which usually occurs in huge open world games like Skyrim and Red Dead Redemption where the save file becomes tremendously bloated in size from having to remember every little change in game data and it causes the game to lag worse and worse the more you play the game?
Maybe this changes with the "powah of da cloud"!
Is there a hot coffee or tea dispenser in the game? I need it just now...
Regarding the "saving during cut scenes", I think your approach is actually beneficial. If I save during a cut scene, I'd probably want it to start over again anyway :)
is a cutscene predictable, if you know the initial variables at the point in time when it triggered?
If so, couldn't you save the (let's call it) 'cutscene ratio' to restore the actual state? (we've seen 0.732 of this cutscene...)
But when I encounter such technical posts that I understand at the 50% ....
I always feel the need to ask what The Secret of Monkey Island is... O_O
Btw, RSS is not firing...
What about auto-save? modern gamers are just used to close the game and it re-loading where they left automatically.
Benefits:
1. all verb processing, all character's movements, all cutscenes can be considered as such scenarios. It provides a conceptual simplicity since all these things are implemented as scenarios.
2. decoupling: scenario can be declared in one context and called in another whenever it is required.
3. multiple parallel scenarios can run simultaneously.
Each step knows how to serialize/deserialize its state. So it is possible to save and load game during cutscenes.
P.S. I always thought that it is great advantage of SCUMM-games that they could be saved at any moment. Very interesting how it was made.
What I do not really understand is, what are global multi-tasking threads? You say you are able to avoid them completely, But for what kind of game action those global scripts could be used or be helpful (despite the fact that you decided to not use them)?
I think of complex actions that span multiple rooms, like the puzzle in MI where the shop owner walks to the swordmaster, or the melting jug. I may be totally wrong. But I would appreciate if you gave an example situation.
SImon Simon
Pretty hardcore post as well, thanks for explaining the case all in-depth. Though now I have a slight urge to go through the whole source and try to understand how it all fits together. The horrible pain of not knowing the unknown. Oh and the two downsides. I don't think them as downsides at all. It would be a bit weird I think, if the character would be moving right after loading a game. In fact I might experience that as a bug in the game... Also with the cutscene playback it is only common sense to get it play from the beginning no matter what (Of course for busy people it would be important to save the extra 4.324 sec it takes to wait for the whole cutscene to be played).
That way, your scripts would be (marginally) easier to write. Plus, you wouldn't run into the problem of forgetting the `g.` prefix and thus having globals that aren't saved.
Regarding 'downside 1': Also, most players will intuitively save when their current character is standing still. So they won't notice/mind. :)
Warning Interstellar spoiler: https://youtu.be/nOipaf5Rt9o
(WARNING! HIGH LEVEL SPOILER!!!)
https://www.youtube.com/watch?v=iJio07EtKYc&feature=youtu.be&t=1m21s
(WARNING! HIGH LEVEL SPOILER!!!)
Thanks for sharing!
Which also brings me to another question: If you do an action that changes another room - e.g. opening a door by switching a lever, or removing an npc - do you change that room's data in memory, or will the code for the other room upon entering check for the lever state in the first room, and adjust the door accordingly?
Just a thought (allegedly)... :)
As I have said before, I kind of think of this game like a point-and-click interactive X-Files episode.. the truth is out there!.
Thanks God It's Friday :
- a new weekend is coming
- a new TP podcast is in the air ready to be recorded
Ciao!
Having this amount of state also made cloud saving impossible. Can you imagine merging that much data? Me neither, so those games didn't support cloud save.
It is a lot better to save the minimum amount of state and have an enterScene() put things in their place depending on the local and global variables - even if that means not restoring cutscenes in the middle. That's what I've done in later games - the C++ engine autosaved when you switch rooms (including cutscenes) and when you resumed the game, it reentered the room. It's less technically "perfect" but it is a lot better for both you and the end-user. And supporting cloud saving is a lot easier. Those games were set up so that variables could be merged either as "take the max of", "replace", or "let a function deal with it" but there was so little state that it was very simple to do.
I tried both on multiple shipped large games and the latter solution is the superior one.
Keep up the good work!