Tavados
Tavados

Reputation: 2308

Unity/C# Savegame Migration

I've written a SaveLoad class, which contains a Savegame class that has a bunch of ints, doubles, bools but also more complex things like an array of self-written class objects.

That savegame object is being created, serialized and AES encrypted on save and vice versa on load - so far, so good.

The problem I'm facing now is that if there are new variables (in a newer version of the game) that have to be stored and loaded, the game crashes on load, because the new variables can't be loaded correctly (because they are not contained in the old save file). E.g. ints and doubles contain the default 0 while an array is not initialized, thus null.

My current "solution": For each variable that is being loaded, check if it doesn't contain a specific value (which I set in the Savegame class). For example: In Savegame I set

public int myInt = int.MinValue;

and when loading, I check:

if(savegame.myInt != int.MinValue){
    //load successful
}else{
    //load failed
};

This works so far for int and double, but once I hit the first bool, I realized, that for every variable I have to find a value that makes "no sense"(not reachable usually), thus was a failed load. => Shitty method for bools.

I could now go ahead and convert all bools to int, but this is getting ugly...

There must be a cleaner and/or smarter solution to this. Maybe some sort of savegame migrator? If there is a well done, free plugin for this, that would also be fine for me, but I'd prefer a code-solution, which may also be more helpful for other people with a similar problem.

Thanks in advance! :)

Upvotes: 3

Views: 1083

Answers (3)

pixelpax
pixelpax

Reputation: 1517

I'm facing the same problem and trying to build a sustainable solution. Ideally someone should be able to open the game in 10 years and still access their save, even if the game has changed substantially.

I'm having a hard time finding a library that does this for me, so I may build my own (please let me know if you know of one!)

The way that changing schemas is generally handled in the world of web-engineering is through migrations-- if an old version of a file is found, we run it through sequential schema migrations until it's up-to-date.

I can think of two ways to do this:

  1. Either you could save all saved files to the cloud, say, in MongoDB, then change their save data for them whenever they make updates or
  2. You need to run old save data through standardized migrations on the client when they attempt to load an old version of the save file

If I wanted to make the client update stale saved states then, every time I need to change the structure of the save file (on a game that's been released):

  1. Create a new SavablePlayerData0_0_0 where 0_0_0 is using semantic versioning
  2. Make sure every SavablePlayerData includes public string version="0_0_0"
  3. We'll maintain static Dictionary<string, SavedPlayerData> versionToType = {"0_0_0": typeof(SavablePlayerData0_0_0)} and a static string currentSavedDataVersion
  4. We'll also maintain a list of migration methods which we NEVER get rid of, something like:

Something like

public SavablePlayerData0_0_1 Migration_0_0_0_to_next(SavablePlayerData0_0_0 oldFile)
{
   return new SavablePlayerData0_0_1(attrA: oldFile.attrA, attrB: someDefault);
}
  1. Then you'd figure out which version they were on from the file version, the run their save state through sequential migrations until it matches the latest, valid state.

Something like (total pseudocode)

public NewSavedDataVersion MigrateToCurrent(PrevSavedDataVersion savedData) 
{
   nextSavedData = MigrationManager.migrationDict[GetVersion(savedData)]
   if (GetVersion(nextSavedData) != MigrationManager.currentVersion) {
        return MigrateToCurrent(nextSavedData, /* You'd keep a counter to look up the next one */)
   }
}
  1. Finally, you'd want to make sure you use a type alias and [Obsolete] to quickly shift over your codebase to the new save version

It might all-in-all be easier to just work with save-file-in-the-cloud so you can control migration. If you do this, then when a user tries to open the game with an older version, you must block them and force them to update the game to match the saved version stored in the cloud.

Upvotes: 0

Stephen P.
Stephen P.

Reputation: 2397

Your issue is poor implementation.

If you are going to be having changes like this, you should be following Extend, Deprecate, Delete (EDD).

In this case, you should be implementing new properties/fields as nullables until you can go through and data repair your old save files. This way, you can check first if the loaded field is null or has a value. If it has a value, you're good to go, if it's null, you don't have a value, you need to handle that some way.

e.g.

/*We deprecate the old one by marking it obsolete*/
[Obsolete("Use NewSaveGameFile instead")]
public class OldSaveGameFile
{
    public int SomeInt { get; set; }
}

/*We extend by creating a new class with old one's fields*/
/*and the new one's fields as nullables*/
public class NewSaveGameFile
{
    public int SomeInt { get; set; }
    public bool? SomeNullableBool { get; set; }
}

public class FileLoader
{
    public SavedGame LoadMyFile()
    {
        NewSaveGameFile newFile = GetFileFromDatabase(); // Code to load the file
        if (newFile.SomeNullableBool.HasValue)
        {
            // You're good to go
        }

        else
        {
            // It's missing this property, so set it to a default value and save it
        }
    }
}

Then once everything has been data repaired, you can fully migrate to the NewSaveGameFile and remove the nullables (this would be the delete step)

Upvotes: 1

wvisaacs
wvisaacs

Reputation: 188

So one solution would be to store the version of the save file system in the save file itself. So a property called version.

Then when initially opening the file, you can call the correct method to load the save game. It could be a different method, an interface which gets versioned, different classes, etc but then you would require one of these for each save file version you have.

After loading it in file's version, you could then code migration objects/methods that would populate the default values as it becomes a newer version in memory. Similar to your checks above, but you'd need to know which properties/values need to be set between each version and apply the default. This would give you the ability to migrate forward to each version of the save file, so a really old save could be updated to the newest version available.

Upvotes: 0

Related Questions