【Unity】ScriptableObject实践入门-2 ISerializationCallbackReceiver和SerializeReference

前言

SO的核心功能是存储,在此过程中正确的序列化必不可少。于是本节将从ISerializationCallbackReceiver和SerializeReference两方面补充序列化的使用。

ISerializationCallbackReceiver

自定义序列化/反序列化回调的接口。在开发过程中有些类不能被序列化,出于某些原因无法改动源代码,但又不得不对其序列化,例如Dictionary字典、Type类型。这时候就可以包装一个实现此接口的类,从而达到序列化的目的。

以Type类型简单举例:现有需求,多个类需要存储Type数据。由于Unity无法序列化Type类型,一般做法是,改为存储string,需要时再将其转换成Type。但考虑到有多个类,为了避免重复代码,可以将Type包装成一个可序列化的类。

    [Serializable]
    public class SerializableType : ISerializationCallbackReceiver
    {
        [SerializeField]
        private string type;

        [NonSerialized]
        public Type typeValue;

        public SerializableType(Type value)
        {
            typeValue = value;
        }

        //序列化之前
        public void OnBeforeSerialize()
        {
            type = typeValue?.AssemblyQualifiedName;
        }

        //反序列化之后
        public void OnAfterDeserialize()
        {
            if(string.IsNullOrEmpty(type))
            {
                typeValue = null;
                return;
            }
            typeValue = Type.GetType(type);
        }
        //省略部分代码……
    }

这里有几点需要注意:

  • 对于不想序列化的数据,使用[NonSerialized]标记
  • OnBeforeSerialize将会在Unity序列化之前被调用,这里用于将Type”序列化“为String
  • OnAfterDeserialize将会在Unity反序列化之后被调用,这里用于将String”反序列化“为Type
  • 实现ISerializationCallbackReceiver不会影响Unity序列化过程,只是多执行了两个回调而已
  • (补充)如果实现ISerializationCallbackReceiver的类中,存储了同样实现ISerializationCallbackReceiver的成员,需要在OnBeforeSerialize时手动调用该成员的OnBeforeSerialize方法,在OnAfterDeserialize时手动调用该成员的OnAfterDeserialize方法

以此类推,很容易想到可序列化字典的实现方法:在OnBeforeSerialize时将字典的keys和values放入两个List当中,在OnAfterDeserialize时再将两个List里的内容取出组成新字典即可。

SerializeReference

按引用序列化。这个标签扩充了Unity序列化的功能,使其灵活性大幅提升,但由于诞生以来就bug不断,又存在效率问题,仍然存在争议。不过对本人来说,牺牲一点运行效率提高开发效率是值得的。

功能一:序列化引用

在SerializeReference出现之前,对于所有可以序列化的字段,Unity都采用内联方式——也就是拷贝一份嵌进去。这意味着你只能存储(组合)而不能单纯保存引用(关联)。对于图形来说,这意味着必须要把复杂的图形展开,分别存储元素和元素之间的联系。

以二叉树为例,在名为T的SO中建立公有List<Node> tree,其中有三个节点node1,node1left和node1right。每个节点数据结构中有公有的string name,[SerializeReference]Node leftNode和[SerializeReference]Node rightNode。那么打开T的文件,会发现其大概会以以下格式存储:

tree:{

guid1 name:node1 leftNode:guid2 rightNode:guid3

guid2 name:node1left leftNode: rightNode:

guid3 name:node1right leftNode: rightNode:}

本质是给对象设guid作为标识,然后记录引用对象的guid

在实际开发中,如果不能保存引用,就得额外记录引用信息,然后再做繁琐的查询和绑定。比如存储一个列表,同时想记录列表中某个关键元素,就只能记下元素信息,然后查找了。不足之处也有,就是一个序列化对象仍然无法在不同宿主(GameObject、ScriptableObject等)中共享。

功能二:支持多态和null

还是拿二叉树举例,不过这次的节点Node有两个子类,一种叫DogNode,另一种叫CatNode。如果不使用SerializeReference标签为List<Node> tree标记,就无法支持多态,那么tree中所有的节点都会被当作基类Node序列化,从而丢失数据。标记后,tree的存储格式会变为:

tree:{guid1, guid2, guid3}

在Inspector面板里,tree的每个元素都会按其实际类型绘制,方便了数据查看和配置。、

另外,没有SerializeReference标记的字段,若反序列化时值为null,Unity会为其创建默认对象(最多嵌套七层)。如string类型就会反序列化为””而不是null。按引用序列化之后,字段就可以被反序列化为null了。

代码例

    public class Graph : ScriptableObject
    {
        [SerializeField]
        [SerializeReference]
        protected List<Node> nodeList;

        [SerializeField]
        [SerializeReference]
        protected List<DogNode> dogNodes;

        [SerializeField]
        [SerializeReference]
        protected List<CatNode> catNodes;
    }

P.S.根据最新文档SerializeReference – Unity 脚本 API节选

The value assigned to a field with the SerializeReference attribute must follow these rules: Must not derive from UnityEngine.Object. For example it cannot be a ScriptableObject.

不能序列化引用SO。但本人在项目实际中,SO里序列化引用List<SO>并未出错,推测其限制不适用于List。

© 版权声明
THE END
喜欢就点个赞吧
点赞1 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容