Handling Nontrivial Data Migrations in Unity


It’s a truism of software development in general that systems in production tend to have a lot of inertia. Want to change something after it’s in use? Better prepare for a lot of pain.

Such is the way with data migrations, and in this, Unity is no exception. It appears that the only official tool that Unity gives us the FormerlySerializedAs attribute, which handles renames of fields.

This attribute is indeed quite handy, but what if you’re doing more than a rename? In this post, I’ll outline the general framework I use for non-trivial migrations. I’ll warn you - it’s not pretty, but it works.

The problem

First, let me outline a simple - and only somewhat contrived - example. Say we’ve got a serialized list of enemies in an enemy spawner. We loop through the enemies and spawn them, one-by-one, on a set delay.

public class Spawner: MonoBehaviour
{
    public List<Enemy> enemies = new List<Enemy>();

    public IEnumerator Spawn()
    {
        // Spawn 'em!
    }
}

Everything’s working fine and dandy, but then, one day our designer knocks on our door. They want to assign a spawn point to each of these enemies, instead of having them all spawn at the same place.

There’s some other sub-optimal solutions to this, but when it comes down to it, we want to change our Spawner to this:

public class Spawner: MonoBehaviour
{
    public class Spawn
    {
        public Enemy enemy;
        public Transform spawnPoint;
    }

    public List<Spawn> enemies = new List<Spawn>();
}

However, with this change, all Spawners lose references to the enemies they had previously because Unity can’t deserialize the old data format into the new one. Now our level designer needs to go through every level, remembering exactly what enemies were assigned to what spawner and tediously re-assigning every one.

For simple projects, it can be fine to eat this cost. For instance, if there’s only one level with a handful of spawners in it, reassigning all the enemies isn’t so much of a pain.

For larger projects, handling this migration automatically is not just a matter of convenience - it’s a necessity. So it was in our project. Here’s how I handle it.

Step one: Fill the new data container

Instead of deleting and trying to overwrite our old data, let’s have our new data co-exist with the old one:

public class Spawner: MonoBehaviour
{
    public class Spawn
    {
        public Enemy enemy;
        public Transform spawnPoint;
    }

    // new
    public List<Spawn> enemiesNew = new List<Spawn>();

    // old
    public List<Enemy> enemies = new List<Enemy>();
}

Then, we can fill in our new data from the old one using ISerializationCallbackReceiver:

public class Spawner: MonoBehaviour: ISerializationCallbackReceiver
{
    // ...

    ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        enemiesNew.Clear();
        foreach (var enemy in enemies)
        {
            enemiesNew.Add(new Spawn { enemy = enemy, spawnPoint = this.transform });
        }
    }

    ISerializationCallbackReceiver.OnBeforeSerialize()
    {
        // TODO - read below
    }
}

If you compile this code and inspect a Spawner, we’ll see that enemiesNew starts getting filled with the old values of enemies.

Notice that I’ve left OnBeforeSerialize empty. With the technique that I describe below, it will remain empty. That being said, if you fill in the OnBeforeSerialize as the Unity docs show, moving the enemies in enemiesNew back into enemies, you will have a workable solution. Throw a [HideInInspector] attribute on enemies, and your designers won’t know the difference. However, that enemies field will still be around in your code, plaguing you years later when you’ve forgotten all about this.

Unfortunately, even though we’ll start to see Spawner instances filling enemiesNew, we cannot yet remove the enemies field. Not all Spawners have been updated yet.

Step two: Reserialize your assets

When you change the data format of a class, Unity does not change the on-disk format of assets of that type until they are dirtied and saved. That means our above change actually did not do anything to our assets in the library. If we were to remove the enemies field, now, we’d find that enemiesNew still remains empty.

Normally, this is great for us. it means Unity doesn’t have to go through the entire library, re-saving all assets, every time you change your code. But in this specific case, it means we have to manually reserialize all of the Spawners in our scenes. For this, Unity provides us a utility method - AssetDatabase.ForceReserializeAssets.

You’ll need to call this from somewhere inside the editor. I have my own utility window that contains a bunch of utility methods like this. Here is how I am calling it:

private void OnGUI()
{
    if (GUILayout.Button("Resave all assets"))
    {
        int option = EditorUtility.DisplayDialogComplex("Reserialize all?",
            "Reserialize all can take a long time. Choose \"Pick dir\" to only reserialize assets in a specific directory",
            "All", "Cancel", "Pick dir");

        if (option == 0)
        {
            AssetDatabase.ForceReserializeAssets();
        }
        else if (option == 2)
        {
            string dir = EditorUtility.OpenFolderPanel("Pick Folder to Reserialize", "", "");
            AssetDatabase.ForceReserializeAssets(DirSearch(dir));
        }
    }
}

// Recursively returns all files in a directory
static IEnumerable<string> DirSearch(string sDir)
{
    foreach (string d in Directory.GetDirectories(sDir))
    {
        foreach (string f in Directory.GetFiles(d))
        {
            string relativePath = f;
            
            // AssetDatabase methods take a path relative to the project directory
            if (relativePath.StartsWith(Application.dataPath))
            {
                relativePath = "Assets" + relativePath.Substring(Application.dataPath.Length);
            }

            yield return relativePath;
        }

        foreach (var sub in DirSearch(d))
        {
            yield return sub;
        }
    }
}

ForceReserializeAssets, by default, will reserialize all assets in your library. This took about an hour in our project. Picking just the directory containing our project’s files (i.e. excluding all of our asset store assets) took about 5 minutes, and you can get it down even further if you know exactly which assets are touched by your format change, which you can likely do with some editor magic.

Step three: Removing the original data

Now that all of our spawners have filled the enemiesNew field on disk, we can go ahead and remove enemies. If you’re content with leaving the new field named as enemiesNew, this is as far as you need to go.

Unfortunately, if you want to rename enemiesNew to enemies, we must run reserialization again to get our Spawners to pickup the removal of enemies. Otherwise, Unity will try to deserialize the contents of the old enemies list into enemiesNew, leaving us once again unable to proceed.

But finally, after removing enemies and running reserialization again, we can rename enemiesNew to enemies using the bog-standard FormerlySerializedAs attribute, leaving us with beautiful, clean data.

Conclusion

You should be able to apply this framework to any data migration you want to handle in Unity. Unfortunately it takes about 15 minutes of downtime (maybe more if you need to troubleshoot), and you need to ensure that no one else is touching the assets while you’re reserializing them.

I do find it strange that there’s no officially-documented way to do this. If anyone knows of a better way to handle migrations, especially one that requires less steps, I’d be glad to hear it. Leave a comment!