Problem: Your game has data that belongs to the user, i.e. save data or custom maps, and you’ve introduced breaking changes in a new version of the game.
Solution: Migrate data on load so that it is compatible with the current version of the game.
Play the Example scene in Unity and check the logs. You will see the version 1 data (loaded from ExampleData.json) and the version 2 data (after migration).
Debug.Log($"before: {dataBefore.text}");
var dataObject = MigratableDataProcessor.FromJson<ExampleData.Example>(dataBefore.text);
var dataAfter = MigratableDataProcessor.ToJson(dataObject);
Debug.Log($"after: {dataAfter}");
{
"id": "12345",
"exampleValue": 100,
"SchemaVersion": 1
}
{
"id": "12345",
"values": {
"exampleValue1": 100,
"exampleValue2": -1
},
"SchemaVersion": 2
}
Wrap your serialized data in a static class like “ExampleData”. Create a new static class for the previous version. Previous versions of the data are wrapped in versioned static classes like ExampleDataV1, ExampleDataV2, etc. The latest version does not have a numbered class name, so that the latest version is always referenced throughout the project. The versioned static classes are only referenced in the implementation of the migration.
Increment the SchemaVersion when data structure changes and a new versioned static class is created. This is how the MigratableDataProcessor knows where to start the migration process.
Implement MigrateFromPreviousVersion for the current version of the data. See ExampleData.DataHandler.
public static class ExampleData // Current version of data structure does not have "VX" appended.
{
public class DataHandler : BaseMigratableDataHandler<Example>
{
public const int SCHEMA_VERSION = 2; // Increment when data structure changes.
public override int SchemaVersion => SCHEMA_VERSION;
public override MigratableDataProcessor.IVersionedData MigrateFromPreviousVersion(
MigratableDataProcessor.IVersionedData migrateFrom)
{
var fromData = (ExampleDataV1.Example) migrateFrom; // Migrate from the previous version.
Debug.Log($"{DataTypeName}: Migrating from {fromData.SchemaVersion} to {SchemaVersion}");
var values = new ValueWrapped(
fromData.exampleValue, // Move existing value to new location
-1 // Set a default value for new data
);
return new Example(
fromData.id,
values
);
}
}
[Serializable]
public class ValueWrapped
{
public int exampleValue1;
public int exampleValue2;
public ValueWrapped()
{
}
public ValueWrapped(int exampleValue1, int exampleValue2)
{
this.exampleValue1 = exampleValue1;
this.exampleValue2 = exampleValue2;
}
}
[Serializable]
public class Example : MigratableDataProcessor.IVersionedData
{
public string id;
public ValueWrapped values;
public int SchemaVersion => DataHandler.SCHEMA_VERSION;
public Example()
{
}
public Example(string id, ValueWrapped values)
{
this.id = id;
this.values = values;
}
}
}
public static class ExampleDataV1 // Append VX to previous versions of data structure.
{
public class DataHandler : BaseMigratableDataHandler<Example>
{
public const int SCHEMA_VERSION = 1; // Increment when data structure changes.
public override int SchemaVersion => SCHEMA_VERSION;
public override MigratableDataProcessor.IVersionedData MigrateFromPreviousVersion(
MigratableDataProcessor.IVersionedData fromData)
{
throw new NotImplementedException($"{DataTypeName}: No previous schema version.");
}
}
[Serializable]
public class Example : MigratableDataProcessor.IVersionedData
{
public string id;
public int exampleValue;
public int SchemaVersion => DataHandler.SCHEMA_VERSION;
public Example()
{
}
public Example(string id, int exampleValue)
{
this.id = id;
this.exampleValue = exampleValue;
}
}
}