save & load

finally finished the nuts and bolts of saving and loading.

there's not much to say about a saving and loading system, except very technical things, so let's talk about technical things :P (that'll be fun! ...right? >_>)

when making this save system, there were a few things that were important considerations for me:

  1. serializing objects (turning in-memory data about the player, the world, etc, into a string for writing to a file) is a solved problem; I shouldn't have to do much work, as a programmer, to save the game
  2. for Mysterious Space, I used C#'s built-in serializer/deserializer, and I was 99% happy with that, but it requires some extra markup you have to put on all your classes, which is a problem when saving classes from libraries you didn't write (like XNA Colors).
  3. I should be able to provide a summary of the game on the load screen (current health, date of save, list of items, etc), without loading up the entire saved game (Mysterious Space ONLY shows your ship name, which is accomplished by just looking at the file name, since the file names are your ship's name; this is not super-great >_>)

the first thing I did, was to get the Newtonsoft.Json library. it's one of the most-downloaded NuGet packages, not only because end-user programmers like myself like JSON, but because many other libraries (REST servers & clients, for example) also like JSON. Newtonsoft.Json is super-good at handling JSON, INCLUDING serializing/deserializing objects, and provides options for handling common problems.

I use Newtonsoft is serialize LOOT × LORE × LOVE's World object, like so:

        public string Serialize(World world)
            StringBuilder sb = new StringBuilder();

            using (StringWriter s = new StringWriter(sb))
            using (JsonWriter w = new JsonTextWriter(s))
                _serializer.Serialize(w, world);

            return sb.ToString();

(the _serializer object here is a JsonSerializer. importantly, it has been given PreserveReferencesHandling = PreserveReferencesHandling.Objects setting.)

this is some trickery to get the data as a plain string. there ARE ways to write straight to a file, but I don't want to do that. why?

my third requirement: "I should be able to provide a summary of the game on the load screen (current health, date of save, list of items, etc), without loading up the entire saved game."

in order to accomplish this, what I really need is one save file with two pieces of data: the first, an object which summarizes the save (the date of the save, the player's appearance, and anything else I want displayed on the load screen), the second, the serialized world, produced by my Serialize function above.

there are a couple ways I can think of to do this, and probably some more ways I'm not thinking of; the way I chose was to make a SavedGame class which stores this metadata, AND the already-serialized world data.

pros to this approach all stem from the fact that we get everything down to a single object:

  • it's easy to serialize this SavedGame class in two steps: 1. serialize the World, and assign it to a variable on the SavedGame; 2. serialize the SavedGame itself. deserializing is a similar two-step process, except you don't deserialize the World until the player chooses to save the game
  • it's easy to compress the whole thing. I used ICSharpCode.SharpZipLib, because it makes it SO easy. here's my current Save function:

        public bool Save(World world)
            if (!Directory.Exists(SavePath))

            using (FileStream stream = File.Create(SavePath + "\\" + world.Player.Name + ""))
            using (GZipOutputStream gzStream = new GZipOutputStream(stream))
                _formatter.Serialize(gzStream, new SavedGame(world));

            File.Delete(SavePath + "\\" + world.Player.Name + ".save");
            File.Move(SavePath + "\\" + world.Player.Name + "", SavePath + "\\" + world.Player.Name + ".save");

            return true;

where _formatter = new BinaryFormatter(), and SavePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\LOOTxLORExLOVE".

(also: there's a little bit of extra logic in there which is attempting to protect your current save game in case saving the new one fails, HOWEVER: I haven't tested it, or added logic to properly handle failure, so I can't guarantee that the above code is actually the best way of doing what it wants to do! use with caution!)

cons to this approach:

  • with any generic serializer, by default you'll be saving more data than is strictly necessary, leading to large save files than are necessary... but this is the year 2017, and I'm seriously not worried if my save game is 4KB when it could have been 1KB. "BUT THAT'S 75% LESS" sure, but it's 3KB. do you realize how little that is? this whole devlog post is 7.5KB. IF my save game files become unreasonably large, THEN I will optimize, and Newtonsoft.Json lets you mark fields to not be saved, and provides options for how to handle loading up unsaved fields, so the optimization will be easy. so this is only a mild con.
  • MORE IMPORTANTLY: my specific implementation doesn't satisfy "I should be able to provide a summary of the game on the load screen (current health, date of save, list of items, etc), without loading up the entire saved game" as well as it could. my code IS loading up the entire saved game, it's just not fully deserializing it. if you're only going to allow a handful of saved games to be saved at a time, and your (uncompressed) save game data is small (so it doesn't take up a ton of RAM to keep them all loaded at once), then maybe this is not a large concern. but a better way would probably be to store both objects, serialized independently, with some kind of delimiter between the two. that way, you could read the save file up to that delimiter to get just the meta, and when the player actually chooses to load, then you can read everything AFTER the delimiter to get the actual game state.

so the nuts and bolts are in-place, but I'm definitely not done working on this save system. besides addressing that second con, the UI still doesn't show the items collected (even though that information IS saved), and there's no way to delete saves! I'd also like to put some of this logic into my BMGFramework, so that others can take advantage of the work I've done, if they like; doing that will definitely take some additional work & refactoring.


programming's a funny thing. there are a lot of details to any system, when you get into it, even a save/load game system.

and I'm not on any kind of schedule or deadline here, but I'm still reminded of this rule of thumb: if anyone asks you how long you think it'll take for you to do something (including when you ask yourself!), always double your initial guess!

P.S. now I'm curious about the file sizes of other games' saves; let's see here... wow... there's save data from old games I almost forgot I even played... hm... I should find recent games...

  • Cities Skylines? 7-18KB per save.
  • Civ 6? 1-1.5KB! (wow! I expected larger!)
  • Hellblade: Senua's Sacrifice? 642KB!? WHAT. that's WILDLY inefficient; there is much less about a Senua game to save than a Cities Skylines game, but anyway...
  • Life is Strange? 1-2KB.

I feel like Civ 6 must be doing some very special to keep their saves so small, but I'm not too embarrassed about 4KB saves, even if they probably do contain less information. Cities Skylines saves are rightly large - those cities can get HUGE... but, jesus, Senua's Sacrifice, what are YOU doing?! whatever it is, it's absolutely wrong. but even so: 642KB? do I ACTUALLY honestly care? no. my Windows Temp directory is over 200MB. my user's temp directory is over 600MB. I have 11 GIGABYTES of songs, mostly MP3s, averaging between 5 and 6MB per song. Senua's 642KB saves are peanuts.

(wait: my user temp directory is over 600MB!?!? wtf, Windows! I should do something about that...)


Leave a comment

Log in with to leave a comment.