How I chose a save game format for my game
Implementing the ability to save and load game states can be a big task. In most cases it’s also one of the most important things to get done early in development because it enables you, the developer, to test things much more efficiently. I’d say it’s pretty high on the priority list.
In developing my game, Factory Magnate, I’ve tried several approaches and rewritten the entire data persistence layer multiple times. My experience is limited to a tile based game, but I will walk you through what I learned. Also, obligatory disclaimer: your engine/framework/etc may offer other options or maybe even have something built-in ready to use. My framework of choice, LibGDX, doesn’t, so I had to implement everything from scratch.
So, here are the 3 approaches I tried.
Using a database.
Having worked with databases for 20+ years I immediately thought of using a database. I know SQL, I know table formats, I know everything to be able to use a database backend. So that seemed like an obvious choice.
I knew of SQLite but had never actually used it. I’m no expert on that particular product, but it’s basically a light weight database engine you can run off a single file and embed in your code, without the user/player having to install anything extra. There are libraries for most languages and it’s really easy to use – if you already know SQL, that is.
I had no trouble implementing it and coming up with the table designs I needed, early tests worked flawlessly. However, I grossly underestimated how much SQL I had to write to get everything saved and loaded properly. It took quite some time.
I ran with this for a couple of months, but as the game grew and I needed to save more and more data, the database file grew as well. Now, admittedly I haven’t researched save game file sizes. Maybe players don’t even care. But I watched my saves passing 50MB and it didn’t feel right. Read and write operations were also becoming increasingly slower, even with proper indexing.
Ultimately I dropped it.
Using JSON.
A developer favorite, JSON is a standardized format for data exchange. It’s easy to use and easy to understand, but some would say there is quite a lot of redundancy. It’s a fair point, but I won’t go into the pros and cons, you can find that elsewhere. I like JSON, and I wanted to try it.
I said I needed to write a lot of SQL before, but that is nothing compared to the amount of code I had to write to make the switch to JSON. It took forever. Once I was done, I found that the written JSON would take up nearly as much space as the database did, and it wasn’t super fast either.
If I hadn’t gotten a better idea, I would very likely have kept using JSON and optimized it to reduce redundancy.
Using plain text.
I don’t remember how it happened, but one day I came to think of the Prison Architect save file format. If you have the game installed, try opening one of the saves in a text editor and marvel at it’s simplicity and elegance. I don’t know if Chris at Introversion invented it himself or if it’s a well known format, but either way I wouldn’t have come across it if I hadn’t lurked around in the PA files. Some times it pays off to be a lurker.
So, excitedly I whipped up a quick test using some of the same principles and quickly decided this was what I had been looking for; it’s relatively easy to use in code, not too much overhead, it’s easy to read and it’s easy to expand. Everything I wanted.
You could say that using a format like this poses a risk to players hacking the save. Well, yes. If that’s a concern to you, you should consider passing the data through an encryption layer before you save it to file. Or you could calculate a checksum for the file and compare it when you’re done loading. If they don’t match, the save was altered. In which case you may decide that someone is trying to cheat and disable achievements for that save. Anyway, that’s outside the scope of this article.
Here’s a basic example from a save file:
BEGIN Tiles
BEGIN X 0 Y 0 TileType 3 END
BEGIN X 0 Y 1 TileType 2 END
...
END Tiles
Unsurprisingly, BEGIN end END marks the beginning end ending of a “section”. Here, Tiles being a section that describes all the tiles in the tile map. You could also have sections for inventory, research, economy, whatever you need as long as it can be descibed using simple properties.
To me, the true elegance lies in the way any one line is simply a set of keys and values, or properties if you will. X 0 translates to X = 0, TileType 3 translates to TileType = 3 and so on. Writing this data is super easy, reading is just a matter of reading each line and splitting the line on space. Then it’s a matter of iterating the array of strings and assigning values.
Here’s a method I’m using myself (Java code):
public Map<String, Object> getValues(String line)
{
Map<String, Object> values = new HashMap<>();
// Use whatever means you like to split the string. Here, I'm using an optimized splitting method.
String[] arr = split(line, " ");
for(int i = 0; i < arr.length; i++)
{
String key = arr[i];
if(i < arr.length - 1)
{
String val = arr[i + 1];
if(val.contains("\""))
{
values.put(key, val.replace("\"", ""));
}
else
{
// Very basic type checks. These fit my needs and are based on how I save properties.
// So, not a universal solution!
if(val.equals("true") || val.equals("false"))
values.put(key, Boolean.parseBoolean(val));
else if(val.contains(".") && !val.contains("["))
values.put(key, Double.parseDouble(val));
else if(isInt(val))
values.put(key, Integer.parseInt(val));
else
values.put(key, val);
}
}
}
return values;
}
When you get that map of keys and values back, you simply need to populate your objects accordingly.
Using somewhat compressed property names (e.g. “T” instead of “TileType”) I can squeeze a full save down to ~3 MB. Loading a save takes ~250 ms. That’s a very small footprint and impact. I love it.