How to transfer save data through versions?

Started by
7 comments, last by frob 4 years, 10 months ago

For example:
I have one game save version, which contains variables: var1,var2,var3,var4 (It is saved locally or globally whatever...)

Then I want to update my game(it's on android/ios) ,new game save version contain same variables as before but with additionally variables (var1,var2,var3,var4,var5) maybe remove some variable so it become var1 var2 var4 var5 etc.

That union of that variables are in one [System.Serializable] class which I load by FileStream, Cloud... then serialize/deserialize...

So it's very obviously that simple loading it , will not work so I need to convert it some way ... I don't know...

And my question is how to transfer save data through versions like in way written above.

 

 

 

Advertisement

Save data isn't something you "drag along" for the most part. Upgrade, if there's portability, yes. But usually once you change your save state format, you're SOL and have to reproduce it from scratch.

What you're describing in your post is something you might traditionally find in a document format (ods/rtf/doc/etc), which is supported via feature clusters - you save for feature set x, which might be opened in one way or another by a version of the (or some unrelated) application supporting feature set y, which is at least partially inclusive of x. While this might work for text formatting, strict formats like game states require a lot more foresight. What you're generally looking for are two types of compatibility:

1) can version X be directly upgraded to Y? For instance - if all you do is change the data representation but not the contents of the data, then you can provide a pretty basic tool that supports reading X (and Y) and writes out Y. An example would be going from an XYZ to and XZY coordinate system.
2) is version Y functionally different from X? If yes, then you're screwed. In a vast majority of cases you cannot upgrade a game state that is missing functional elements to a new version that includes or omits them (could you "upgrade" a Doom 2 save game that stores data about an Archvile to a Doom 1 save game, which doesn't have this type of monster to start with?). If you don't know how to map X directly to to Y, then you cannot perform the crossgrade*.

That being said...

Serialization and deserialization are very low-level parts of a save game. These essentially apply to (1) from the above two examples. Serialization reduces your data to a series of basic types (byte, short, int, float, double) with a specific byte order (small or big endianess). In short - serialization is "how you write your data to disk" while a save game version is "what data you write to disk".

 

 

* that being said, if you have some sort mechanism that can fill in a new variable in a valid way without corrupting the previous state, then you might be good to go. In your case, if var5 is something like a tool selection ID or some other such inconsequential variable, then might just give it a default value if the input was somehow invalid.

I know of two main ways to deal with changing serialized formats:

1. Include a "serializedVersion" number in your data, and have your loading code perform the appropriate conversion to your latest format.  This is typically a one-way upgrade, and older versions of your game typically will not be able to handle serializedVersions NEWER than themselves (this can be important if you have a multiplayer game, or cloud syncing between multiple devices with different versions of the game).

2. Include field IDs in your serialized format (key/value pair style).  protobuf and JSON use this approach.  You MIGHT also need a serializedVersion.  When loading old savegames, your loading code needs to handle cases where a field is missing.  Otherwise it's fairly easy to deal with.  The main gotchas are if you change the meaning of the values without a way to indicate whether you need to convert the value to something else (i.e. converting a timestamp from Unix seconds to milliseconds).

After thinking about it I would realize it next way:

After loading data from save file,it checks is version same(that save data contain version)

If it is same version program will just continue same as before:

If it isn't same version program it will convert that GameState version N into GameState current version

So first in first version (1.0) it will not check anything because it's only version 

if version is 1.1 and save data is 1.0 it will call function(constructor) GameState (GameState10) which builds new data

If version is 2.2 it must have function that will convert for each version like

GameState(GameState10),GameState(GameState11),GameState(GameState12),GameState(GameState13),GameState(GameState14),GGameState(GameState20),GameState(GameState21),GameState(GameState22)

So I need to save data order for every previous version and then convert it properly.

 

On 6/21/2019 at 12:11 PM, ggenije said:

So first in first version (1.0) it will not check anything because it's only version 

if version is 1.1 and save data is 1.0 it will call function(constructor) GameState (GameState10) which builds new data

If version is 2.2 it must have function that will convert for each version like 

GameState(GameState10),GameState(GameState11),GameState(GameState12),GameState(GameState13),GameState(GameState14),GGameState(GameState20),GameState(GameState21),GameState(GameState22)

So I need to save data order for every previous version and then convert it properly. 

You only need to obtain a reasonably correct current-version game state from any supported savefile version; there's no need to convert savefiles or game states.

This requires testing for specific saved game versions only rarely, and because you introduced an ambiguity: changes can usually be managed with default values (e.g. if since a certain version you start tracking character wounds and later the progress of how they heal, you can assume characters from saved games containing no wounds are not hurt and that wounds with no healing state are fresh).

In other cases you can test for mutually exclusive representations (e.g. an enumeration with eight possible directions, to be translated to multiples of 45°, vs. an angle; they could be called, respectively, "direction" and "facing").

Omae Wa Mou Shindeiru

14 minutes ago, LorenzoGatti said:

You only need to obtain a reasonably correct current-version game state from any supported savefile version; there's no need to convert savefiles or game states.

This requires testing for specific saved game versions only rarely, and because you introduced an ambiguity: changes can usually be managed with default values (e.g. if since a certain version you start tracking character wounds and later the progress of how they heal, you can assume characters from saved games containing no wounds are not hurt and that wounds with no healing state are fresh).

In other cases you can test for mutually exclusive representations (e.g. an enumeration with eight possible directions, to be translated to multiples of 45°, vs. an angle; they could be called, respectively, "direction" and "facing").

I'm not sure we understood each other.

The GameState contains data like characters which I did unlocked , maximum wave I did reached and similar which is core data...

Also I have PlayState which contains that "characters wounds,heal progress" but if I would solve that problem with GameState  ,PlayState will not make any problem just do same thing as before with GameState or just reset that game and start new(because my game is based on trying to reach maximum wave)

The essence of all posts is to tell you that you can't simply fire and forgett a sctruct into some kind of serializer and hope anything works fine. You need to do some work to have it either upwards and/or downwards compatible.

Instead of


MySerializer.DoYourThing(MyStruct)

you need to iterate all the fields in your savegame and test for matches in your current struct, otherwise ignore it. This makes your game version compatible to updates. On the other side to make your game also downwards compatible, you need to iterate the old savegame and store it in a list, then overwrite those fields that are contained in your current versions struct and save it again to disk.

This may however come with some drawbacks, changing an important value to a different meaning may cause your old version misread the savegame and do things you never wanted it to do so update safety is more generic.

We did something similar for our games using binary format and field IDs


FileStream fs = new FileStream("MySaveGameFile");
while(!fs.Eof())
{
	byte fieldId = fs.ReadByte();
    switch((FieldIds)fieldId)
    {
        case FieldIds.LocalPosition: DeseraializeVector3(fs); break;
        case FieldIds.Health: DeserializeUInt64(fs); break;
        ...
    }
}

 

Typically this gets implemented as a long chain of functions. 

Every structure you write would begin with a version number, the object's ID, and typically the size of the block that was written.  Then when you decode it, it often works something like this:


bool ConvertToV13( int version, const unsigned char* data )
{
   if(version == 12 || ConvertToV12( version, data )) {
       // Convert from 12 to 13.
     }
   }
}

bool ConvertToV12( int version, const unsigned char* data )
{
   if(version == 11 || ConvertToV11( version, data ) {
       // Convert from 11 to 12
     }
   }
}

bool ConvertToV11( int version, const unsigned char* data )
{
   if(version == 10 || ConvertToV10( version, data ) {
       // Convert from 10 to 11
     }
   }
}
      ...

 

It doesn't need to be a raw pointer to the data, there are a wide range of systems.  Key/value pairs are fairly common, as are objects with bit packing and compression functionality.  Even so, the code needs to handle migrating data from the earliest version of the structure up to the current form.

 

In some games I've worked on, there are also failsafe objects that are part of the root data structure.  For example, on The Sims, if an object no longer exists in the database (perhaps they uninstalled a downloadable object) then it gets converted into the failsafe object instead that is part of the base game and guaranteed to exist.  For example, a 1x2 DLC shop door object might have a failsafe of a basic door. A 2x2 DLC blackjack table object might have a failsafe object of a basic 2x2 wooden table.  Since the failsafe object is part of the common root data structure, the failsafe could be used even if the object or the decoder for the object aren't available.

 

Serialization is mostly a solved problem, but just because it is solved doesn't mean there isn't work to do.  Even when you use a great serialization library you still need to migrate between data versions.

This topic is closed to new replies.

Advertisement