ue3d正确理解Unity的SerializeReference属性

正确理解Unity的SerializeReference属性

分类:
ue3d - 正确理解Unity的SerializeReference属性

本文总结了Unity中用于序列化引用的SerializeReference属性。

介绍

SerializeReferenceAttribute是Unity2019.3中添加的序列化引用的函数。

如果用得好的话,这是一个非常有用的功能,但是需要了解规范才能正确使用它。还需要了解与传统值序列化SerializeField本质上的区别。

在这篇文章中,SerializeReference将总结这方面的基础知识,并考虑如何实际使用它。

查看序列化字段

SerializeReference在了解之前,先简单总结一下旧的 SerializeField 的行为。

原始类型和结构可以序列化

SerializeField通过添加,可以显式地序列化私有基元类型和结构,作为测试,将创建一个附加了一些基本类型和结构的组件

using System;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField] private int _field1; // 基本
    [SerializeField] private Vector3 _field2; // 单位定义结构
    [SerializeField] private ExampleStruct _field3; // 自制的构造体
    
    [Serializable] // 对自制的结构体附加可序列化属性
    public struct ExampleStruct
    {
        public int exampleInt;
        public string exampleString;
    }
}

将此组件附加到适当的游戏对象会将值序列化到场景或预制件,这些值可以从检查器中编辑,如下所示。

ue3d - 正确理解Unity的SerializeReference属性

可以序列化UnityEngine.Object派生类型的引用

还可以序列化对从 UnityEngine.Object 派生的类型实例的引用。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField] private GameObject _field1;
    [SerializeField] private MeshRenderer _field2;
    [SerializeField] private Example _field3;
}

如果将此组件附加到适当的 GameObject,可以确认可以按如下方式分配每个对象的引用。

ue3d - 正确理解Unity的SerializeReference属性

非UnityEngine.Object类的实例可以序列化为值

非 UnityEngine.Object 类的实例如果具有Serializableattriutes ,也可以被序列化。

但是,在这种情况下,序列化字段的值,而不是引用。换句话说,将同一个实例分配给不同的字段,反序列化时会产生不同的实例。

下面是确认此行为的代码。

using System;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField] private ExampleInner _inner1;
    [SerializeField] private ExampleInner _inner2;

    [MenuItem("Example/SerializeTest")]
    public static void SerializeTest()
    {
        var example = FindObjectOfType<Example>();
        if (example == null)
            return;
        var gameObject = example.gameObject;

        // 将同一实例代入_inner1和_inner2
        var inner = new ExampleInner
        {
            exampleInt = 1234,
            exampleString = "Example"
        };
        example._inner1 = inner;
        example._inner2 = inner;
        EditorUtility.SetDirty(example);

        // 判定参照是否相同
        Debug.Log($"判定1: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})");

        // 切换并返回场景以进行序列化和去实化
        var scenePath = gameObject.scene.path;
        EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
        EditorSceneManager.OpenScene(scenePath);

        // 重新判定参照是否相同
        example = FindObjectOfType<Example>();
        Debug.Log($"判定2: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})");
    }

    // 序列化类
    [Serializable]
    public class ExampleInner
    {
        public int exampleInt;
        public string exampleString;
    }
}

如果将其附加到场景中合适的游戏对象Example > SerializeTest并执行它,将输出以下日志。

ue3d - 正确理解Unity的SerializeReference属性

能够确认反序列化时这两个对象是完全不同的实例。

另外,有一个规范,这样序列化的字段不会为null,并且会一直处于实例被赋值的状态。

不支持抽象类或接口

现在,SerializeField非UnityEngine.Object抽象类类型和接口类型无法序列化。然而,从UnityEngine.Object派生的抽象类可以被序列化。

using UnityEngine;

public class Example : MonoBehaviour
{
    // 这些不能序列化
    [SerializeField] private ExampleAbstract _field1;
    [SerializeField] private IExample _field2;
    
    // 这些可以序列化
    [SerializeField] private ExampleAbstractBehaviour _field3;
}

public abstract class ExampleAbstract
{
}

public interface IExample
{
}

public abstract class ExampleAbstractBehaviour : MonoBehaviour
{
}

通用类型可以序列化(从Unity2020.1开始)

从 Unity 2020.1 开始,通用类型现在可以使用 SerializeField 进行序列化

using System;
using UnityEngine;

public class Example : MonoBehaviour
{
    // 使Generic类的实例可序列化
    [SerializeField] private ExampleGeneric<int> _example;
}

// 可序列化且通用的类
[Serializable]
public class ExampleGeneric<T>
{
    public T _testField;
}

了解 SerializeReference 规范

SerializeReference来看看以下规格:

非 UnityEngine.Object 的 SerializeReference 函数

首先,作为前提,用于序列化SerializeReference不是从 UnityEngine.Object 继承的类型的引用。如上所述,可以序列化SerializeField对UnityEngine.Object 派生类型的引用,以便可以使用它。

如果尝试将 UnityEngine.Object 分配给具有属性的 Interface 类型,将输出以下错误SerializeReference。

Fields with [SerializeReference] cannot serialize objects that derive from Unity.Object

序列化参考

连载SerializeField包含参考信息,为了确认这一点,来重写中使用的代码。

using System;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeReference] private ExampleInner _inner1;
    [SerializeReference] private ExampleInner _inner2;

    [MenuItem("Example/SerializeTest")]
    public static void SerializeTest()
    {
        var example = FindObjectOfType<Example>();
        if (example == null)
            return;
        var gameObject = example.gameObject;

        // 将同一实例代入_inner1和_inner2
        var inner = new ExampleInner
        {
            exampleInt = 1234,
            exampleString = "Example"
        };
        example._inner1 = inner;
        example._inner2 = inner;
        EditorUtility.SetDirty(example);

        // 判定参照是否相同
        Debug.Log($"判定1: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})");

        // 切换并返回场景以进行序列化和去实化
        var scenePath = gameObject.scene.path;
        EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
        EditorSceneManager.OpenScene(scenePath);

        // 重新判定参照是否相同
        example = FindObjectOfType<Example>();
        Debug.Log($"判定2: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})");
    }

    // 序列化类
    [Serializable]
    public class ExampleInner
    {
        public int exampleInt;
        public string exampleString;
    }
}

如果将其附加到场景中合适的游戏对象Example > SerializeTest并执行它,将输出以下日志。

ue3d - 正确理解Unity的SerializeReference属性

发现将同一个实例序列化和反序列化为两个字段仍然指向同一个实例。

可以序列化抽象类型和接口类型的字段

属性还可以附加到抽象类和接口类型的字段,将实例分配给这些字段会序列化实际的类型信息。

using System;
using UnityEditor;
using UnityEngine;

public class Example : MonoBehaviour
{
    // 接口,接口
    [SerializeReference] private IExampleInner _inner1;
    // 抽象类
    [SerializeReference] private ExampleInnerBase _inner2;

    [MenuItem("Example/SerializeTest")]
    public static void SerializeTest()
    {
        var example = FindObjectOfType<Example>();
        if (example == null)
            return;

        // 将同一实例代入_inner1和_inner2
        var inner = new ExampleInner
        {
            exampleInt = 1234,
            exampleString = "Example"
        };
        example._inner1 = inner;
        example._inner2 = inner;
        EditorUtility.SetDirty(example);
    }

    public interface IExampleInner
    {
    }

    public abstract class ExampleInnerBase
    {
    }

    [Serializable]
    public class ExampleInner : ExampleInnerBase, IExampleInner
    {
        public int exampleInt;
        public string exampleString;
    }
}

检查器还显示与实际类型相对应的属性。

ue3d - 正确理解Unity的SerializeReference属性

两个 MonoBehaviour 之间不能共享引用

SerializeReference序列化的参考信息不能在两个 MonoBehaviour 之间重用,序列化为
ScriptableObject时也是如此,并且不能在两个 ScriptableObject 之间重用。

可以代表空

如上所述,有一个规范规定具有SerializeField该属性的字段不能为空,标记为的字段SerializeReference可以表示 null。

不能用于使用泛型的类实例

尝试使用Generic序列化类的实例也不起作用,在这种情况下,还应该定义一个继承自它的非泛型类。

using System;
using UnityEditor;
using UnityEngine;

public class Example : MonoBehaviour
{
    // can represent不能序列化
    [SerializeReference] private ExampleInnerBase _inner1;

    [MenuItem("Example/SerializeTest")]
    public static void SerializeTest()
    {
        var example = FindObjectOfType<Example>();
        if (example == null)
            return;

        // 可序列化
        var inner = new ExampleInnerInt
        {
            example = 1234,
            exampleString = "Example"
        };
        // 不能序列化
        /*
        var inner = new ExampleInner<int>
        {
            example = 1234,
            exampleString = "Example"
        };
        */
        example._inner1 = inner;
        EditorUtility.SetDirty(example);
    }
    
    public abstract class ExampleInnerBase
    {
    }

    public class ExampleInner<T> : ExampleInnerBase
    {
        public T example;
        public string exampleString;
    }

    [Serializable]
    public class ExampleInnerInt : ExampleInner<int>
    {
    }
}

何时使用 SerializeReference

这么复杂得惊人SerializeReference,但是有什么实际用途呢?

第1部分:序列化图和树结构

提到序列化引用的特性,首先想到的是图结构或者树结构

ue3d - 正确理解Unity的SerializeReference属性

这样具有相互引用关系的结构SerializeReference可以说是完美的利用,可以用 ScriptableObject 来实现SerializeField,但是如果节点数量较多的话就比较痛苦了。

第2部分: 可以轻松切换编辑器的输入界面

下一个优点是可以轻松切换输入界面。例如,考虑输入技能的效果值,技能类型包括攻击和增益,对于攻击,可以输入攻击强度。对于buff,可以输入buff的类型(攻击力、防御力等)、效果值和效果时间。

SerializeReference将尝试使用来实现这个

using System;
using UnityEditor;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeReference] private Skill _skill;

    [MenuItem("CONTEXT/Example/Set Attack Skill")]
    public static void SetAttackSkill(MenuCommand command)
    {
        var example = (Example) command.context;
        example._skill = new AttackSkill();
        EditorUtility.SetDirty(example);
    }
    
    [MenuItem("CONTEXT/Example/Set Buff Skill")]
    public static void SetBuffSkill(MenuCommand command)
    {
        var example = (Example) command.context;
        example._skill = new BuffSkill();
        EditorUtility.SetDirty(example);
    }

}

[Serializable]
public abstract class Skill
{
    public float value;
}
    
[Serializable]
public class AttackSkill : Skill
{
}
    
[Serializable]
public class BuffSkill : Skill
{
    public BuffType buffType;
    public float duration;
}

public enum BuffType { Attack, Defense }

可以看到,当切换实例类型时,输入字段会发生相应的变化。

ue3d - 正确理解Unity的SerializeReference属性

第3部分:可以通过编辑器更改行为

实例被替换意味着行为也被替换,如下所示。

using System;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeReference] private Speaker _speaker;

    public void Start()
    {
        _speaker?.Speak();
    }
}

[Serializable]
public abstract class Speaker
{
    public string word;
    public abstract void Speak();
}

[Serializable]
public class NormalSpeaker : Speaker
{
    public override void Speak() => Debug.Log($"{word}");
}

[Serializable]
public class LoudSpeaker : Speaker
{
    public override void Speak() => Debug.Log($"{word}{word}{word}");
}

执行时,将根据正在序列化的实例的类型调用 Speak 方法。这也可以使用ScriptableObject 来实现,但必须为每种类型的行为创建一个 ScriptableObject。

以上是关于Unity中用于序列化引用的SerializeReference属性的全部内容,如果你有任何反馈,请随时在本页面下方留言。

相关信息

  • 类型:知识
  • 字数:1579
  • 字符:12886
  • 适用软件:Unity
  • 说明:无
  • 编号:166748

热门内容

提示:3D天堂作为服务提供者,尊重网络版权及知识产权,对某些行为的发生不具备充分的监控能力,若无意间侵犯到您的权利,请 联系我们,我们会在收到信息后尽快给予处理。

本站文章版权归本站自创作者所有,未经允许不得转载!