Home > Mobile >  Serializing nested polymorphical objects in Unity
Serializing nested polymorphical objects in Unity

Time:02-17

Hi fellow game developers, I'm working on a Unity project that allows level designer to edit instructions to scene elements of how they should act to events.

screenshot of command editor in unity inspector

I've managed to express all executable instruction units--expressions, statements, control blocks--with a common abstract base class Command. It comes like this:

[Serializable]
abstract class Command {
    public abstract object Execute();
    public abstract void Inspect(/* ... */);
}
class CommandCarrier : MonoBehaviour {
    public Command command;
}
/*
    There are several carrier classes in the real project,
    this one is only for illustrating the problem.
    Command.Inspect() would be called by a CustomEditor of CommandCarrier.
*/

Where Execute() is to perform the command at runtime, and Inspect() is to draw the inspector GUIs.

Every solid type of command would be a derived class of Command, e.g. an if-else block would be like:

[Serializable]
class Conditional : Command {
    public Command condition, trueBranch, falseBranch;
    public override object Execute() {
        if((bool)condition.Execute()) trueBranch.Execute();
        else falseBranch.Execute();
        return null;
    }
    public override void Inspect(/* ... */) { /* ... */ }
}

A constant expression would contain no sub-commands:

[Serializable]
class Constant<T> : Command {
    public T value = default(T);
    public override object Execute() => value;
    public override void Inspect(/* ... */) { /* ... */ }
}

Here comes the problem: all the commands I've written in the inspector panel would be lost as long as a reserialization is triggered (like when the code changed and therefore is recompiled). This is probably because Unity failed to serialize a subclass instance stored in a field of base class; all the type information and the contained data are lost during reserialization. What's worse is that these polymorphical instances are even nested.

I've tried to solve the case and failed: given a field of base class, it's apparently impossible to "upgrade" an instance to a subclass by calling whatever methods belonging to that instance; it must be done externally by assigning the field with a subclass instance created elsewhere. But again, every subclasses have their own fields, and these data I haven't figure out where to recover from.

Could anybody help?

CodePudding user response:

Now that you corrected your code here I would point you to Script Serialization and in particular the section

No support for polymorphism

If you have a public Animal[] animals and you put in an instance of a Dog, a Cat and a Giraffe, after serialization, you have three instances of Animal.

One way to deal with this limitation is to realize that it only applies to custom classes, which get serialized inline. References to other UnityEngine.Objects get serialized as actual references, and for those, polymorphism does actually work. You would make a ScriptableObject derived class or another MonoBehaviour derived class, and reference that. The downside of this is that you need to store that Monobehaviour or scriptable object somewhere, and that you cannot serialize it inline efficiently.

The reason for these limitations is that one of the core foundations of the serialization system is that the layout of the datastream for an object is known ahead of time; it depends on the types of the fields of the class, rather than what happens to be stored inside the fields.

So in your case I would simply use ScriptableObject and do

abstract class Command : ScriptableObject
{
    public abstract object Execute();
    public abstract void Inspect(/* ... */);
}

and

[CreateAssetMenu]
public class Conditional : Command 
{
    public Command condition, trueBranch, falseBranch;
    public override object Execute() {
        if((bool)condition.Execute()) trueBranch.Execute();
        else falseBranch.Execute();
        return null;
    }
    public override void Inspect(/* ... */) { /* ... */ }
}

and

public abstract class Constant<T> : Command 
{
    public T value = default(T);
    public override object Execute() => value;
    public override void Inspect(/* ... */) { /* ... */ }
}

and e.g.

[CreateAssetMenu]
public class IntConstant : Constant<int>
{
}

each in their own script files with matching name (that part is very important for the serializer).

And then you would create instance of these via the Assets -> right click -> Create -> "Conditional" for example and reference it into the according slots.

Also note that these are now re-usable and you can simply reference the same item in various places, something that wasn't possible if you use a normal serializable class due to

When might the serializer behave unexpectedly?

Custom classes behave like structs

With custom classes that are not derived from UnityEngine.Object Unity serializes them inline by value, similar to the way it serializes structs. If you store a reference to an instance of a custom class in several different fields, they become separate objects when serialized. Then, when Unity deserializes the fields, they contain different distinct objects with identical data.

When you need to serialize a complex object graph with references, do not let Unity automatically serialize the objects. Instead, use ISerializationCallbackReceiver to serialize them manually. This prevents Unity from creating multiple objects from object references. For more information, see documentation on ISerializationCallbackReceiver.

This is only true for custom classes. Unity serializes custom classes “inline” because their data becomes part of the complete serialization data for the MonoBehaviour or ScriptableObject they are used in. When fields reference something that is a UnityEngine.Object-derived class, such as public Camera myCamera, Unity serializes an actual reference to the camera UnityEngine.Object. The same occurs in instances of scripts if they are derived from MonoBehaviour or ScriptableObject, which are both derived from UnityEngine.Object.

  • Related