Saving the complete state of a game at any time and then restoring to that state is hard for any game, but in a fully dynamic voxel world that constantly changes, controlled by dozens of lua scripts, all with their own internal state, implementing this was quite a challenge.

The quicksave feature in Teardown is central to gameplay and needs to be extremely robust for the game to be playable, so I knew early on this had to work flawlessly. It also had to be relatively fast. A long delay would be annoying and cause players to use it less often, limiting creativity and experimentation. Furthermore, it is one of those features that’s quite unrewarding to work on, because no matter how good it is, it doesn’t really add anything to the game, other than working as expected, while even the smallest error instantly results in corrupt state and most likely a crash.

Let’s start with the world itself. It consists of thousands of individual voxel volumes, each with anything from a couple of hundred up to millions of voxels. These volumes are altered dynamically as the player causes destruction. Both voxel content and the size of the volumes change, new volumes are being added and others are removed. In theory, it would probably be possible to keep a diff for the world and use that for tracking state, but for robustness purposes I wanted to save the entire state of world for each save. The larger levels contain roughly half a billion voxels, so I first thought it would be unrealistic to save all that state, but since the voxel data compresses very well it turned out to actaully be a viable option. Proper entropy coding like zlib can easily get the size down to a few percent of the original size, but compressing half a gigabyte of data takes a while even on a fast machine. Instead I’m using simple run-length encoding which has almost zero cost, both on the compressing and the decompressing end. Using this gets the size down to 15-20% of original size, making quicksave files on the larger levels around 80 Mb in size. It’s still a lot, but acceptable and very quick to load. The binary mission content files that we ship with the game are actually just an initial quicksave snapshot of each level, but I run these through zlib at the bake step, cutting size down to about 20 Mb per mission.

Compared to the voxel data, all other game state is tiny, but equally important. I’m using an explicit form of serialization, where each object has callbacks for saving and loading state. This means each object can choose freely what to save, and might leave out cached state or temporary acceleration structures. The voxel objects, for instance, has a separate physics representation, which is also voxel data, but in a different format. This is 100% reproducible from the main voxel data, so instead of saving it, it’s generated at load time. The same goes for spatial acceleration structures for physics, culling, rendering and lighting. I’m using a serialization context that gets passed around to each object that keeps track of pointer serialization by iterating over the objects twice at load time.

//Code example for saving the state of a physical body
void Body::saveState(TSerializeOutput& ser)

This explicit form of serialization is flexible, but it also makes it error prone. I tend to group class members in stateful and stateless sections in the header. This makes it a bit more explicit what needs to be saved and easier to check if everything is being saved, but I wish I had a better system in place to verify that all state is indeed saved.

Scripting was by far the hardest part to serialize. All gameplay logic in Teardown is implemented in lua scripts and since scripts can be written by anyone, even outside the development team (there is already an active modding community) I wanted state serialization for scripts to be fully automated. Hence, in contrast to the engine-side serialization, there should be no need for callbacks or explicit state serialization. One would have hoped that the lua library offered some way to serialize global state, but unfortunately there is not much in there to help (at least not in lua version 5.1 that I’m using, correct me if I’m wrong). Fortunately, traversing the global state of a lua context is relatively easy. All global variables show up in the globals table (_G), so if all handles and engine interaction is handled properly, serializing that table is enough. There is still a lot of non-trivial code to untangle table references and types correctly and the end result is not perfect, but it works for all our own scripts. There are corner cases, like multiple tables referencing other tables and circular dependencies that will not serialize correctly, so I’ll have to go over that at some point, but for the most part it works really well.