UI design – “this is the worst”
button remapping – “no wait, this is the worst”
saving system – “ok, actually, this is the worst”
— William Chyr (@WilliamChyr) November 20, 2016
I like making small games. I’ve never even thought about a save system and no one has ever asked for one.
But Golden Krone Hotel is getting bigger than other things I’ve made. I’ve been surprised by how long some runs take players. It only takes me an hour to beat the game (and usually 10 minutes to die a horrible death), but some players were reporting 5 hour runs. Lots of people were asking about saves.
Sure, let’s write a save system. How hard can it be?
1) It seems easy, but it ain’t
Find all the state in your program, save it, and then reload it again. Seems easy. But if you try to do this late into a project as an afterthought (as I did), you’ll almost certainly forget how much state is actually in your program.
That’s because the better you get at writing encapsulated code, the more you’ll shelter yourself from the complexity horrors lurking inside each abstraction. You’ll forget about how much stuff is going on behind the scenes and how tedious it would be to track all of it down.
But track it down you must. Writing a save system is like getting a take home exam that requires a perfect score to pass. If you load one thing incorrectly, your game will malfunction in subtle ways or, if you’re luckier, crash spectacularly.
2) It’s coupled to every part of your game
By definition, the save system has to have its fingers in all your program’s many pies. The only exception is temporary state that can be easily reloaded. For certain types of games, that may be a pretty big exception: think of Super Mario Bros. where only a few numbers needed to be saved (current level, number of lives, etc.).
For a roguelike with permanent levels, however, there’s a lot of stuff. My game has roughly 15 levels with 40×40 tile maps. That’s 24,000 tiles, not to mention:
- Monsters
- Items
- Learned spells
- Learned abilities
- Potions
- Status effects
- Player stats
Every level currently in Golden Krone Hotel
It’s easy to get tight coupling between the save system and all those different modules, which can make the code very brittle.
Now if you’re smarter than I am, you would think about how to centralize all your state at the beginning of your project. Josh Ge describes such a method and it sounds pretty great (though I’m still thinking about it would be properly implemented in a language like JavaScript):
All objects are stored in allocated memory pools and accessed via handle/ID (in other words, I don’t use pointers), so when you save the game, regardless of how many references there are to objects, all you have to do is save the memory pools and reload them when starting up again–bingo any references are still intact, cyclical ones and all. (I’ve got plenty of cyclical references and don’t have to worry about them getting out of whack.) It’s a really powerful idiom; learned it from one of the old Gems books.
On the bright side, revisiting almost every line of my project helped me clean up a lot of dead code.
3) Debugging it is a nightmare
Halfway through this save endeavor, I realized it would be much easier to save parts of my game by recursively visiting objects and their properties.
The bulk of the work is done by this one function
Automating that work was a big win, but it makes debugging pretty awful. I would regularly crash the browser (having never received a useful error message, I’m guessing it was through many a stack overflow).
Some bugs were hard to reproduce too. One required multiple save/loads before showing up. Others only arose when interacting with rare in-game items.
4) Circular dependency hell
“Experience keeps a dear school, but fools will learn in no other.” -Benjamin Franklin
Yup, that’s me. I often have a hard time accepting a design pattern until I feel the same pain that instigated the design pattern in the first place. Well, save systems provide the pain.
I’m fond of circular references, despite many people considering it an anti-pattern. A tile needs to know what monster is standing on it and a monster needs to know what tile it is standing on. So what? Well here are two big problems.
First, if you recursively visit an object/array and all its descendant properties (and some of those properties are circular) you are guarandamnteed to get stuck in an infinite loop. You can bail out of such recursion by keeping track of which objects you’ve visited, but then you skip saving certain things. The essence of the problem is that pointers just can’t get serialized and deserialized.
Again, this would be solved by Ge’s solution described earlier. What I came up with is also a handle system, just one that’s constructed on the fly solely for the purpose of saving. Whenever I encounter an object, I store it in a map. If I encounter a direct reference to that object again, I replace that with an indirect reference to the object map instead.
The second issue is rebuilding the objects. Going through this process helped me understand a lot about dependency injection. The problem is summarized quite simply:
Foo(Bar b){ //do stuff } Bar(Foo f){ //fuck }
I can’t construct either of these objects before the other. There’s no way to square this circle.
The answer is to create separate functions that do the work of the constructor and can take in circular dependencies (it’s fine as long as we’ve first constructed the objects). I name these methods finishWiring and during loading it’s easy to find out which objects still need to be wired up.
5) Fast, good, cheap. Pick any NaN
Several painful trade offs are involved in a save system. I started out with saves about 30MB each, which seemed pretty high.
I was able to get the saves down to 7MB through a few optimizations, but like any other optimization, it caused readability to go down. Save files got down to just 300KB after compression. That’s definitely a number I can live with, but compression (I used this LZW library) takes a few additional seconds and that sucks.
6) It can make your code worse
Several of my optimizations produced some rather ugly code as a byproduct:
- I had to mark temp variables that I wanted to totally avoid saving. I did this by preprending variable names with an underscore.
- I removed default values of variables when those defaults were 0, false, or null. Fewer variables on each object means fewer things to save, but much more confusion.
- When creating my object maps during saving, I compressed the property names of all objects using base 62 (using a-z/A-Z/0-9 as digits). It saved a good bit of space, but made debugging much harder.
I’m also considering generating levels on the fly so that early saves are tiny, but that will introduce more complexities to the level generation.
7) You’ll wonder if it’s even worth it
In the depths of debugging this mess, I often questioned if this was really worthwhile. These thoughts popped up regularly: How hard is it to just leave the window open? Is it really such a burden to carve out an hour to play the game?
It’s hard to maintain motivation in such times. I could just scrap the whole thing and pretend like it’s not a big deal.
But then I thought about a discussion I once had with a developer at a conference. He was asked by his players to introduce saves and he refused. He almost seemed offended that they would ask and gave all sorts of weak justifications against it. Thinking about how I play games and about my response at the time (“is that really rational… or is it sour grapes?”) made me realize that saving is something players deserve, even if it’s a huge pain.
Feel free to tell me about how much you love writing save systems in the comments. And if you want to see how my system turned out, check out the latest update to Golden Krone Hotel.