Serializable non-UnityEngine.Object-extending classes instantiation: automatic vs manual

Hello.

Imagine I have the following quickly made up classes;

[Serializable]
public class Nested
{
}

[Serializable]
public class Container
{
    public Nested nested;
}

public Scriptable : ScriptableObject
{
    public Container container;
}

When I create an instance of the Scriptable class via Unity’s serialization system (by creating an asset with Scriptable class associated with it), Unity automatically creates both the container and the nested instances for me inline. Very handy!

Now imagine I also instantiate the Container class myself from code fully avoiding Unity’s automated instantiating on serialization/deserialization. The nested instance is not created for me automatically anymore.

What would then be the best place to manually instantiate the nested? Container’s constructor? Container’s onEnabled (will it even be called?). Or would the advice be to not have such multi-purpose classes at all to keep it clean?

My goal is to have two cleanly distinguishable paths of nested instantiation so that the manual path doesn’t interfere with the Unity’s automatic and vice versa. I don’t want some code that I think of as manual to be called randomly whenever Unity so decides.

Thanks a lot for a deeper insight.

2 Answers

2

I set up this simple test, as I was curious on how this works:

using UnityEditor;
using System;
using System.Collections;

public class Scriptable : ScriptableObject {

	[MenuItem("Custom/Create")]
	public static void Create() {
		var obj = ScriptableObject.CreateInstance<Scriptable>();

		AssetDatabase.CreateAsset(obj, "Assets/Scriptable.asset");
		AssetDatabase.SaveAssets();
		AssetDatabase.Refresh();
	}


	public Container container;
}

[Serializable]
public class Nested {

	public int someInt = 5;

	public Nested() {
		Debug.Log("Nested was created! someInt was: " + someInt + " it is now 10");
		someInt = 10;
	}
}

[Serializable]
public class Container {
	public Nested nested;

	public Container() { 
		nested = new Nested();
	}

}

Whenever the project is opened and the Scriptable asset is clicked, the constructor is called. Whenever the code is re-compiled, the constructor is called. This is because whenever those things happens, the scriptable object is deserialized when the editor runtime is reloaded.

Unity manually sets the value of the someInt variable after it runs the constructor, so if you set someInt to 20 in the inspector, Unity will print this every time you reload your scripts:

Nested was created! someInt was: 5 it is now 10

But the value will still actually be 20.

This means that you should never mix scriptable objects and constructors. Unity will complain if the scriptable object itself has a constructor, but it doesn’t complain if any of it’s fields has constructors. It becomes a mess if you assume that the constructor causes the object to be in some specific state, as Unity will gladly reflect that state away. This also goes for MonoBehaviours - don’t rely on the constructors for the objects that you have as fields in the behaviour.

So, if an object is going to be serialized, either avoid constructors alltogether, or have a default constructor that does nothing so it’s safe that the serialization code calls that constructor. If you have objects that are only going to live runtime, use constructors as much as you want.

Thanks a lot for for your thorough investigation. I suspected it's just not the best idea to "mess" with Unity. For now, as the number of cases, where I have classes instantiating both via Unity's serialisation and via code, is low, and the nesting is not deep, I simply mimicked Unity's default behaviour by manually traversing down the instance tree. Not the best bet, but oh well...

If I were you I would definitely create constructors for your objects. It gives you the flexibility to instantiate exactly what you need, and if you were to do it for every object you would essentially be copying the effects of Unity’s system, negating any concern if it’s process gets broken by the creation of constructors that override the default. More control over your own code is always better in my opinion since it opens up more options in terms of creativity in the development process.

Thanks a ton for elaborating, but I would dare to say this is not exactly what my main question is about. It's not about control, it's about clear separation of script-driven and Unity-serialization-driven instantiation. Apologies if I didn't express myself clearly enough.