在 Unity 中序列化嵌套的多态对象

Serializing nested polymorphical objects in Unity

各位游戏开发者,我正在开发一个 Unity 项目,该项目允许关卡设计师编辑场景元素的指令,说明它们应如何对事件采取行动。

screenshot of command editor in unity inspector

我已经设法用一个共同的抽象基础 class Command 来表达所有可执行指令单元——表达式、语句、控制块。它是这样的:

[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.
*/

其中Execute()是在运行时执行命令,Inspect()是绘制检查器GUI。

每个实体类型的命令都是 Command 的派生 class,例如一个 if-else 块就像:

[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(/* ... */) { /* ... */ }
}

常量表达式不包含子命令:

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

问题来了:只要触发重新序列化,我在检查器面板中编写的所有命令都会丢失(例如代码更改并因此重新编译时)。 这可能是因为 Unity 未能序列化存储在 base class 字段中的 subclass 实例;在重新序列化期间,所有类型信息和包含的数据都将丢失。 更糟糕的是,这些多态实例甚至是嵌套的。

我试图解决这个问题但失败了:给定一个基础字段class,显然不可能通过调用属于的任何方法将实例“升级”到子class那个例子;它必须通过为字段分配在别处创建的 subclass 实例来在外部完成。 但是同样,每个 subclasses 都有自己的字段,这些数据我还没有弄清楚从哪里恢复。

有人能帮忙吗?

既然您已在此处更正了代码,我将向您指出 Script Serialization,尤其是

部分

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.

所以在你的情况下我会简单地使用 ScriptableObject 并做

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

[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(/* ... */) { /* ... */ }
}

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

例如

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

每个都在自己的脚本文件中具有匹配的名称(这部分对于序列化程序非常重要)。

然后您将通过资产创建这些实例 -> 右键单击​​ -> 创建 -> 例如“条件”并将其引用到相应的插槽中。

另请注意,这些现在是 re-usable,您可以在不同的地方简单地引用相同的项目,如果您使用普通的可序列化 class,这是不可能的,因为 [=37] =]

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.