8
\$\begingroup\$

I have a complex sim game I want to add save game functionality to. I'll be updating it with new features continually after release.

How can I make sure my updates don't break existing save games? What sort of architecture should I follow to make this possible?

\$\endgroup\$
  • \$\begingroup\$ I'm not aware of a generic architecture for this goal but I would make make the patching process also update/convert save games to ensure compatibility with new features. \$\endgroup\$ – loodakrawa Jan 16 '14 at 0:32
9
\$\begingroup\$

One easy-ish approach is to keep old loading functions around. You need only a single save function that writes out only the latest version. The load function detects the correct versioned load function to invoke (usually by writing out a version number somewhere in the beginning of your save file format). Something like:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

You can do this for the whole file, for individual sections of the file, for individual game objects/components, etc. Exactly what split is best is going to depend on your game and the amount of state you're serializing.

Note that this only gets you so far. At some point you might change your game enough that the save data from earlier versions simply makes no sense. For instance an RPG might have different character classes the player can choose. If you remove a character class there's not a whole lot you can do with saves of characters that have that class. Maybe you could convert it to a similar class that still exists... maybe. Same goes if you change other parts of the game enough that it doesn't closely resemble the old versions.

Be aware that once you ship your game it's "done." You might release DLC or other updates over time but they're not going to be particularly big changes to the game itself. Take most MMOs for instance: WoW has been maintained for many years with new updates and changes but it still is more or less the same game it was when it first came out.

For early development I simply wouldn't worry about it. Saves are ephemeral in early testing. It's another story once you get to public beta, though.

\$\endgroup\$
  • 1
    \$\begingroup\$ This. Unfortunately, this rarely works as pretty as advertised. Usually those loading functions rely on helper functions (ReadCharacter may call ReadStat, which may or not change from one version to the next), so you'd need to keep versions for each of those, making it harder and harder to keep up. As always, there's no silver bullet, and keeping old loading functions is a nice starting point. \$\endgroup\$ – Panda Pajama Jan 16 '14 at 6:02
5
\$\begingroup\$

A simple way to achieve a semblance of versioning is to make sense of the members of objects you are serializing. If your code has an understanding of the various types of data to be serialized you can get some robustness without doing too much work.

Say we have a serialized object that looks like this:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

It should be easy to see that the type ObjectType has data members called m_name, m_size and m_someStruct. If you can loop over or enumerate data members during run-time (somehow) then when reading this file you can read in a member name and match it up to an actual member within your object instance.

During this lookup phase if you don't find a matching data member you can safely ignore this portion of the save file. For example say version 1.0 of SomeStruct had a m_name data member. Then you patch and this data member was removed entirely. When loading your save file you will come across m_name and lookup a matching member and find no match. Your code can simply move on to the next member in the file without crashing. This lets you remove data members without any worries about breaking old save files.

Similarly if you add in a new type of data member and try to load from an old save file your code may just not initialize the new member. This can be utilized to an advantage: new data members can inserted into save files during patching manually, perhaps by introducing default values (or by more intelligent means).

This format also allows the save files to be easily manipulated or modified by hand; the order in which the data members doesn't really have much to do with the validity of the serialization routine. Each member is looked up and initialized independently. This might be a nicety that adds a little extra robustness.

All of this can be achieved through some form of type introspection. You'll want to be able to query a data member by string lookup, and be able to tell what the actual type of data the data member is. This can be achieved in C++ using a form of custom introspection, and other languages might have introspection facilities built in.

\$\endgroup\$
  • \$\begingroup\$ This will be useful for making data and classes more robust. (In .NET the feature is called "reflection"). I wonder about collections... my AI is complicated and uses many temporary collections to process data. Should I try to avoid saving them...? Perhaps limit saving to "safe points" where the processing is over. \$\endgroup\$ – Rye bread Jan 16 '14 at 0:49
  • \$\begingroup\$ @aman If you save a collection then you can write the actual data in these collections like in my original example, except in an "array format", as in many of them in a row. You can still apply the same idea to each individual element of an array, or any other container. You will just have to write some generic "array serializer", "list serializer" etc. If you want a generic "container serializer" you will probably need an abstract SerializingIterator of some sort, and this iterator would be implemented for each type of container. \$\endgroup\$ – RandyGaul Jan 16 '14 at 1:12
  • 1
    \$\begingroup\$ Oh and yes, you should try to avoid saving complicated collections with pointers as much as you can. Often times this can be avoided with a lot of thought and clever design. Serialization is something can get very complicated, so it will pay off to try to simplify it as much as possible. @aman \$\endgroup\$ – RandyGaul Jan 16 '14 at 1:15
  • \$\begingroup\$ There's also the problem of deserializing an object when the class has changed... I think the .NET deserializer will crash in many cases. \$\endgroup\$ – Rye bread Jan 17 '14 at 12:29
2
\$\begingroup\$

This is a problem that exists not only on games, but also on any file exchange application. Certainly, there are no perfect solutions, and trying to make a file format that will keep with any type of change is likely to be impossible, so it's probably a good idea to prepare for the type of changes you may be expecting.

Most of the times, you'll probably be just adding/removing fields and values, while keeping the general structure of your files untouched. In that case, you can simply write your code to ignore unknown fields, and use sensible defaults when a value can't be understood/parsed. Implementing this is quite straightforward, and I do it a lot.

However, sometimes you will want to change the structure of the file. Say from text-based to binary; or from fixed fields to size-value. In such case, you will most likely want to freeze the source of the old file reader, and create a new one for the new file type, like in Sean's solution. Make sure you isolate the entire legacy reader, or you may end up modifying something that affects it. I recommend this only for major file structure changes.

These two methods should work for most cases, but keep in mind that they are not the only possible changes you might encounter. I had a case in which I had to change the entire level loading code from whole-reading to streaming (for the mobile version of the game, which should work on devices with significantly reduced bandwidth and memory). A change like this is much deeper and will most likely require changes in many other parts of the game, some of which required changes in the structure of the file itself.

\$\endgroup\$
0
\$\begingroup\$

At a higher level: if you're adding new features to the game, have a "Guess New Values" function which can take the old features and guess what new ones' values will be.

An example might make this clearer. Suppose a game modelling cities, and that version 1.0 tracks the cities' overall level of development, while version 1.1 adds Civilization-like specific buildings. (Personally, I prefer tracking overall development, as being less unrealistic; but I digress.) GuessNewValues() for 1.1, given a 1.0 savefile, would begin with an old level-of-development figure, and guess, based on that, what buildings would have been built in the city -- perhaps looking at the city's culture, its geographic position, the focus of its development, that kind of thing.

I hope that this can be comprehensible in general -- that if you're adding new features to a game, loading a savefile that doesn't yet have those features requires making best guesses as to what the new data will be, and combining those with the data you loaded.

For the low-level side of things, I'd endorse Sean Middleditch's answer (which I've up-voted): keep the existing load logic, possibly even keeping around old versions of the relevant classes, and call first that, then a converter.

\$\endgroup\$
0
\$\begingroup\$

I would suggest going with something like XML (if you save files are very small) that way you only need 1 function to handle the markup no matter what you put in it. The root node of that document could declare the version that saved the game and allow you to write code to update the file to the latest version if need be.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

This also means you can apply a transform if you want to convert the data in to a "current version format" before loading the data so instead of having lots of versioned functions lying around you would simply have a set of xsl files that you choose from to do the conversion. This can be time consuming though if you are not familiar with xsl.

If your save files are massive xml could be a problem, typically i've save files work really well where you just dump key value pairs in to the file like this ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Then when you read from this file you always write and read a variable in the same way, if you need a new variable you create a new function to write it and read it. you could just write a function for variable types so you would have a "string reader" and a "int reader", this would only fial if you changed a variables type between versions but you should never do that because the variable means something else at this point so you should create a new variable instead with a different name.

The other way of course is to use a database type format or something like a csv file, but it does depen on the data you are saving.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.