Comments
You can use your Mastodon account to reply to this post.
Today I’ll take a look at a problem I recently encountered when I wanted to refactor some code in my Unity Volume Rendering plugin.
I had the following code:
[Serializable]
public class VolumeDataset : ScriptableObject
{
public float scaleX = 1.0f;
public float scaleY = 1.0f;
public float scaleZ = 1.0f;
}
An instance of this class is referenced by another component, that could potentially be serialised and saved as a prefab or as part of a scene.
I wanted to replace these three floats with a Vector3
, like this:
[SerializeField]
public Vector3 scale = Vector3.one;
However, there is one problem. If I do this change, any previously serialised instances of this class (stored in a scene or prefab) will lose their saved scale values, and not copied over to the new scale
variable.
So how can we do this refactoring without breaking existing prefabs and scenes?
If we were simply re-naming a variable, we could use FormerlySerializedAs
and it would just work (unity would be able to recognise that the variable has changed names, and prevent loss of data on load).
However, in our case we’re replacing three variables of type float
with one Vector3
. I couldn’t find a perfect solution for this, but this is what I decided to do:
scale
variable of type Vector3
scaleX
,scaleY
and scaleZ
private, and keep them around for some months/releases to give users of the library some time to update.scaleX
/scaleY
/scaleZ
over to the new scale
vector on deserialisation, and does the opposite on serialisation.Here’s the code:
[SerializeField]
public Vector3 scale = Vector3.one;
[SerializeField, System.Obsolete("Use scale instead")]
private float scaleX = 1.0f;
[SerializeField, System.Obsolete("Use scale instead")]
private float scaleY = 1.0f;
[SerializeField, System.Obsolete("Use scale instead")]
private float scaleZ = 1.0f;
public void OnBeforeSerialize()
{
scaleX = scale.x;
scaleY = scale.y;
scaleZ = scale.z;
}
public void OnAfterDeserialize()
{
scale = new Vector3(scaleX, scaleY, scaleZ);
Debug.Log(scale);
}
Whenever someone loads a previously serialised instance (when loading a scene, etc.), the old scale values will be copied over to the new vector. If I keep the old scale variables around for some time, this should give users of my plugin some time to update their prefabs/scenes, so that it won’t break anything when I eventually remove them - maybe in a year or so.
We just made three public variables now be private. This might be an unpleasant surprise to people using them, as they will get compilation errors when the update our library. We could instead deprecate them, using System.Obsolete
.
However, then we’d get warnings whenever we access them in OnBeforeSerialize
and OnAfterDeserialize
.
So a better solution would be to make them private, re-name them to something else and use FormerlySerializedAs
to make sure old values are copied over. And then we can create a public property to replace the old public variables, which we can then safely deprecate using System.Obsolete
.
[SerializeField]
public Vector3 scale = Vector3.one;
[SerializeField, FormerlySerializedAs("scaleX")]
private float scaleX_deprecated = 1.0f;
[SerializeField, FormerlySerializedAs("scaleY")]
private float scaleY_deprecated = 1.0f;
[SerializeField, FormerlySerializedAs("scaleZ")]
private float scaleZ_deprecated = 1.0f;
[System.Obsolete("Use scale instead")]
public float scaleX { get { return scale.x; } set { scale.x = value; } }
[System.Obsolete("Use scale instead")]
public float scaleY { get { return scale.y; } set { scale.y = value; } }
[System.Obsolete("Use scale instead")]
public float scaleZ { get { return scale.z; } set { scale.z = value; } }
public void OnBeforeSerialize()
{
scaleX = scale.x;
scaleY = scale.y;
scaleZ = scale.z;
}
public void OnAfterDeserialize()
{
scale = new Vector3(scaleX, scaleY, scaleZ);
Debug.Log(scale);
}
Here we replace the three scale variables with three new public deprecated properties, that return or sets the value of our new scale vector. The purpose of these is to make sure users who were accessing the scaleX/Y/Z variables won’t get any compilation errors after upgrading. Instead, they will now get a warning saying that these variables are deprecated.
To prevent loss of data, we also define three new private float variables where we use the FormerlySerializedAs
attribute to make sure the data is copied over from the old scale variables. Then on load, we copy these values over to the new scale vector. So the purpose of these three new floats is simply to work as an intermediate for copying over the old values on load. Later, when users of my library have been giver some time to load and save their scenes/prefabs, I can remove all three, and also the
Sharing is caring!