2024-07-20 Unity插件 Odin Serializer2 —— 序列化教程

文章目录

  • [1 由根对象决定序列化](#1 由根对象决定序列化)
  • [2 实现 Odin 序列化器](#2 实现 Odin 序列化器)
    • [2.1 继承已有序列化类](#2.1 继承已有序列化类)
    • [2.2 自定义序列化类](#2.2 自定义序列化类)
  • [3 避免 Unity 无限深度警告](#3 避免 Unity 无限深度警告)
  • [4 指定序列化秘钥](#4 指定序列化秘钥)
    • [4.1 External String Reference Resolver](#4.1 External String Reference Resolver)
    • [4.2 External GUID Reference Resolver](#4.2 External GUID Reference Resolver)
    • [4.3 External Index Reference Resolver](#4.3 External Index Reference Resolver)
  • [4 功能与限制](#4 功能与限制)
    • [4.1 平台支持](#4.1 平台支持)
    • [4.2 启用序列化](#4.2 启用序列化)
    • [4.3 序列化内容](#4.3 序列化内容)
    • [4.4 支持的数据格式](#4.4 支持的数据格式)
  • [5 序列化重构](#5 序列化重构)
    • [5.1 重命名字段和属性](#5.1 重命名字段和属性)
    • [5.2 重命名类型](#5.2 重命名类型)
    • [5.3 从 Unity 转移到 Odin](#5.3 从 Unity 转移到 Odin)
  • [6 序列化 Dictionary](#6 序列化 Dictionary)
    • [6.1 使用 Odin 序列化字典](#6.1 使用 Odin 序列化字典)
    • [6.2 使用 Unity 序列化字典](#6.2 使用 Unity 序列化字典)
  • [7 序列化协议](#7 序列化协议)
    • [7.1 Odin 序列化协议](#7.1 Odin 序列化协议)
    • [7.2 Unity 序列化协议](#7.2 Unity 序列化协议)
  • [8 使用 Odin 保存数据](#8 使用 Odin 保存数据)
  • [9 序列化调试器](#9 序列化调试器)
  • [10 自定义序列化类](#10 自定义序列化类)
  • [11 AOT 序列化](#11 AOT 序列化)
  • [12 建议与总结](#12 建议与总结)

1 由根对象决定序列化

​ 在使用 Odin 时,Odin 会尽量避免序列化 Unity 本身应该已经序列化的任何字段。这是在根序列化对象的级别确定的:

csharp 复制代码
public class ExampleScript : SerializedScriptableObject
{
    // Unity 不会序列化, Odin 会序列化。
    public Dictionary<int, string> FirstDictionary;

    // Unity 会序列化,因此 Odin 将不会序列化。
    public MyClass MyReference;
}

[Serializable]
public class MyClass
{
    // 尽管有 OdinSerialize 属性,但此字段未序列化。
    [OdinSerialize]
    public Dictionary<int, string> SecondDictionary;
}
  1. FirstDictionary 字段将由 Odin 序列化,因为 Unity 不会对其进行序列化,并且 ExampleScript 是序列化的根对象(所有 SerializedMonoBehaviour 或 SerializedScriptableObject 始终是根序列化对象)。

  2. ExampleScript 类中的 MyReference 字段可以由 Unity 序列化,因此 Odin 将跳过它。由于 Odin 跳过了 MyReference 字段,因此 Odin 甚至不会考虑 MyClass 类型的任何成员。因此,尽管有 OdinSerialize 属性,但 SecondDictionary 字段不会被序列化。

    在这种情况下,解决方案很简单。可以强制 Odin 序列化它:

    csharp 复制代码
    public class ExampleScript : SerializedScriptableObject
    {
        public Dictionary<int, string> FirstDictionary;
    
        [NonSerialized, OdinSerialize]
        public MyClass MyReference;
    }

    NonSerialized 属性阻止 Unity 序列化字段,而 OdinSerialize 属性强制 Odin 序列化该字段。这样,MyClass 类中的 SecondDictionary 字段现在将仅由 Odin 序列化。如果只添加了 [OdinSerialize] 而没有添加 [NonSerialized],则 Unity 和 Odin 将序列化同一字段,这可能会导致细微的错误和不必要的数据重复出现。

2 实现 Odin 序列化器

2.1 继承已有序列化类

​ 虽然 Odin 默认会处理所有没有自定义编辑器的类型,但它的序列化系统更为谨慎。你需要明确选择使用 Odin 的序列化系统。

​ 使用时,只需从特殊序列化类之一继承,例如 SerializedMonoBehaviour 或 SerializedScriptableObject。这种方式支持所有常用继承的 Unity 类型。

csharp 复制代码
public class YourMonoBehaviour : SerializedMonoBehaviour
{
    [SerializeField]
    private object SerializedPrivateField;

    public object SerializedField;

    [OdinSerialize]
    public object SerializedProperty { get; set; }
}

2.2 自定义序列化类

​ 如果已有需要继承的特定基类型,则通过实现 Unity 的 ISerializationCallbackReceiver 接口并使用 Odin 的 UnitySerializationUtility 类,可以在需要的地方自行实现序列化。

  1. 自定义 Odin-serialized ScriptableObject
csharp 复制代码
[ShowOdinSerializedPropertiesInInspector]
public class CustomSerializedScriptableObject : ScriptableObject, ISerializationCallbackReceiver
{
    [SerializeField, HideInInspector]
    private SerializationData serializationData;

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
		UnitySerializationUtility.DeserializeUnityObject(this, ref this.serializationData);
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
		UnitySerializationUtility.SerializeUnityObject(this, ref this.serializationData);
    }
}
  1. 自定义 Odin-serialized MonoBehaviour
csharp 复制代码
[ShowOdinSerializedPropertiesInInspector]
public class CustomSerializedMonoBehaviour : MonoBehaviour, ISerializationCallbackReceiver, ISupportsPrefabSerialization
{
    [SerializeField, HideInInspector]
    private SerializationData serializationData;

    SerializationData ISupportsPrefabSerialization.SerializationData { get { return this.serializationData; } set { this.serializationData = value; } }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
		UnitySerializationUtility.DeserializeUnityObject(this, ref this.serializationData);
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
		UnitySerializationUtility.SerializeUnityObject(this, ref this.serializationData);
    }
}

3 避免 Unity 无限深度警告

​ 默认情况下,Unity 不支持序列化引用、多态性或空值,目的是保护自己免受无限深度序列化循环的影响。

​ 当某个类型 NodeA 的成员 NodeB 包含其自身 NodeA 时,序列化将会无限递归,然后在控制台中向您发出警告。

​ 相比之下,Odin 支持序列化引用、多态性和空值。但 Unity 仍会发出警告,因此一种解决方法是:将成员同时标记为 NonSerialized 和 OdinSerialize,则该成员将由 Odin 序列化。这意味着 Unity 会忽略该成员的序列化,但 Odin 仍然会找到它并序列化它。

4 指定序列化秘钥

​ 使用 Odin 序列化可以自定义处理和恢复序列化数据的方式。

​ Odin 提供 3 种类型的外部参考解析器,允许使用 3 种不同类型的密钥。通过从接口继承来实现,并且必须在序列化和反序列化时在 SerializationContext 中指定。

  1. IExternalStringReferenceResolver
  2. IExternalGuidReferenceResolver
  3. IExternalIndexReferenceResolver

4.1 External String Reference Resolver

​ 允许您指定字符串 ID 以序列化和反序列化引用。例如,在 Unity 编辑器中,您可以使用它按照 Asset 的 GUID 序列化资产引用:

csharp 复制代码
public class ScriptableObjectStringReferenceResolver : IExternalStringReferenceResolver
{
	// 可以将多个字符串引用解析器串联在一起。
    public IExternalStringReferenceResolver NextResolver { get; set; } 

    public bool CanReference(object value, out string id)
    {
        if (value is ScriptableObject)
        {
            id = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject));
            return true;
        }

        id = null;
        return false;
    }

    public bool TryResolveReference(string id, out object value)
    {
        value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id));
        return value != null;
    }
}

​ 序列化和反序列化时,需要在序列化上下文中指定解析程序:

csharp 复制代码
byte[] Serialize(object obj)
{
    var context = new SerializationContext()
    {
        StringReferenceResolver = new ScriptableObjectStringReferenceResolver(),
    };
	return SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
}

object Deserialize(byte[] bytes)
{
    var context = new DeserializationContext()
    {
        StringReferenceResolver = new ScriptableObjectStringReferenceResolver(),
    };
	return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}

4.2 External GUID Reference Resolver

​ 外部 GUID 引用解析程序的工作方式与字符串解析程序大致相同,但它严格使用 GUID 键。

​ 该实现也与字符串解析器几乎相同:

csharp 复制代码
public class ScriptableObjectGuidReferenceResolver : IExternalGuidReferenceResolver
{
	// 可以将多个字符串引用解析器串联在一起。
    public IExternalGuidReferenceResolver NextResolver { get; set; } 

    public bool CanReference(Guid value, out string id)
    {
        if (value is ScriptableObject)
        {
            id = new Guid(AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject)));
            return true;
        }

        id = default(Guid);
        return false;
    }

    public bool TryResolveReference(Guid id, out object value)
    {
        value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id.ToString()));
        return value != null;
    }
}

​ 当然,在序列化和反序列化时需要指定它。

csharp 复制代码
byte[] Serialize(object obj)
{
    var context = new SerializationContext()
    {
        GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
    };
	return SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
}

object Deserialize(byte[] bytes)
{
    var context = new DeserializationContext()
    {
        GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
    };
	return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}

4.3 External Index Reference Resolver

​ 最后,索引解析器允许使用索引 ID 进行引用。这在 Serialized- 类中使用,例如 SerializedScriptableObject,用于重建由 Odin 序列化的 Unity 引用。

​ 如果希望以相同的方式重建引用,在序列化和反序列化之间保持列表相同非常重要。与字符串和 guid 解析器的一个显著区别是,不能将多个索引解析器链接在一起。

csharp 复制代码
public class IndexResolver : IExternalIndexReferenceResolver
{
	public List<UnityEngine.Object> ReferenceList;

	public IndexResolver()
	{
		this.referenceList = new List<UnityEngine.Object>();
	}

	public IndexResolver(List<UnityEngine.Object> references)
	{
		this.referenceList = references;
	}

	public bool CanReference(object value, out int index)
	{
		if (value is UnityEngine.Object)
		{
			index = this.referenceList.Count;
			this.referenceList.Add(value);
		}

		index = 0;
		return false;
	}

	public bool TryResolveReference(int index, out object value)
	{
	    value = this.referencedUnityObjects[index];
	    return true;
	}
}

​ 另一个显着的区别是,在序列化和反序列化过程中,为引用添加了列表本身。

csharp 复制代码
byte[] Serialize(object obj, out List<UnityEngine.Object> references)
{
	var resolver = new IndexResolver();
    var context = new SerializationContext()
    {
		IndexReferenceResolver = resolver,
    };
	var bytes = SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
	references = resolver.ReferenceList;

	return bytes;
}

object Deserialize(byte[] bytes, List<UnityEngine.Object> references)
{
    var context = new DeserializationContext()
    {
        IndexReferenceResolver = new IndexResolver(references),
    };
	return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}

4 功能与限制

4.1 平台支持

​ Odin 序列化目前在所有平台上都受支持:

  • 在非 IL2CPP 版本中,保存通用 Windows 平台;
  • 在 IL2CPP 或 Mono AOT 平台上使用 Odin 的序列化时,必须遵循特殊的 AOT 程序,才能使 Odin 的序列化在构建中正常工作。

4.2 启用序列化

​ 因此,使用 Odin 序列化通常不需要考虑太多,它将在必要时填补空白。对于如下情况, Odin 都会自动采取序列化:

  1. 任何未被 Unity 序列化的公共字段;
  2. 任何未被 Unity 序列化并标有 SerializeField 的私有字段。

​ 但如果确实需要确保特定成员始终由 Odin 序列化,则可以使用特定于 Odin 的 OdinSerialize 特性标记任何字段或自动属性,来确保这一点。

4.3 序列化内容

​ 默认情况下,Odin 将针对 Unity 不会序列化的对象,进行拓展序列化,这包括:

  1. 字典;
  2. 委托;
  3. 属性。
  4. 等。

​ 默认情况下,Odin 会尝试序列化你交给它的几乎所有东西。在大多数情况下,这将正常工作,在某些情况下则不会。对于不会的情况,可以手动扩展序列化。

4.4 支持的数据格式

  1. Binary

    性能最佳的格式,针对速度进行优化,并将垃圾分配保持在最低限度。默认情况下,它将在播放模式和构建中使用。

  2. Json

    默认情况下从不使用 json 格式,但支持对某些内容进行手动序列化并且希望序列化数据可读的情况。需要注意的是,json 格式没有最佳性能,并且在当前版本的 Odin 中分配了很多不必要的垃圾。目前,仅在必要时使用 json 格式。

  3. Nodes

    编辑器中的默认格式,但在播放模式下不是。它不会序列化为字节流,而是转成节点列表,由 Unity 再进行序列化,主要用于 Unity 编辑器的文本资产序列化。当与 Unity 的文本格式一起使用时,节点数据会被格式化,以便在版本控制中更容易合并,因此适合多人的协作项目。节点格式性能不错,但不如二进制格式快,默认只能在编辑器中使用。

5 序列化重构

5.1 重命名字段和属性

​ 重命名字段或属性时,可以使用 Unity 的 FormerlySerializedAs 或 Odin 的 BeforeSerializedAs 特性,让 Odin 知道如何将旧序列化数据映射到新字段。

csharp 复制代码
[FormerlySerializedAs("oldName")]
public int NewName;

​ 建议使用 Unity 的 FormerlySerializedAs 来支持 Odin 的 BeforeSerializedAs,因为 Unity 只支持其中之一,但 Odin 同时支持两者。Odin 的 BeforeSerializedAs 变体主要用于支持罕见的(不推荐的)序列化情况。

注意:不能在 Odin 和 Unity 之间传输序列化数据。如果某个字段已被 Unity 序列化,则不能使用它将其反序列化为 Odin 字段。

5.2 重命名类型

​ 使用 BindTypeNameToType 属性使用 Odin 完成重构类型。允许指定旧类型名称,以及要将类型反序列化的类型。

csharp 复制代码
// 也可以使用该特性对命名空间进行重命名
[assembly: BindTypeNameToType("MyOldNamespace.MyNewTypeName", typeof(MyNewNamesSpace.MyNewTypeName))]

namespace MyNewNamesSpace
{
	public class MyNewTypeName { ... }
}

​ 此特性仅适用于 Odin 序列化数据。

5.3 从 Unity 转移到 Odin

​ 有时可能需要在 Unity 和 Odin 序列化器之间传输数据,但没有现成的属性可以用来实现该功能,需要按几个顺序步骤进行,以确保不会丢失数据。

  • 不推荐的方法:

    仅仅重命名或从 Unity 可序列化的格式更改为 Odin 可序列化的格式,可能会导致数据丢失(建议使用版本控制项目,以便随时回退版本恢复错误)。

  • 推荐方法:

    更好的方法是简单地复制字段,把新字段改为 Odin 序列化,并在对象反序列化时手动传输和复制数据。

csharp 复制代码
public class MyMono : SerializedMonoBehaviour
{
	// 隐藏旧数据的显示
	[HideInInspector] 
	public List<MyClass> OldList;

	[NonSerialized, OdinSerialize]
	public List<MyClass> NewList = null;

    // OnAfterDeserialize 方法定义在 SerializedMonoBehaviour 类中,
    // 但也可以在 Unity 中使用 ISerializationCallbackReceiver,
    // Odin 还支持 System.Runtime.Serialization 命名空间中定义的序列化事件属性,
    // 如 [OnDeserialized]。
	protected override OnAfterDeserialize()
	{
		if (this.NewList != null)
		{
			// 这种情况下,我们只复制旧列表,但你可以根据具体情况做适当的处理。
			this.NewList = this.OldList.ToList();
			
			// 此时,最好将对象标记为 dirty,以便 Unity 重新序列化它。
		}
	}
}

[Serializable]
public class MyClass
{
	...
}

​ 这样,数据会从旧格式复制到新格式。

注意:Unity 并不会每次都重新序列化和保存对象,所以不能立即删除旧列表。要么保留旧列表一段时间,要么强制 Unity 重新序列化项目中的所有对象。从 Unity 2018.1 开始,可以使用 AssetDatabase.ForceReserializeAssets API 来实现,调用此 API 会在整个项目中迁移数据。

6 序列化 Dictionary

6.1 使用 Odin 序列化字典

​ 只需继承序列化类 SerializedScriptableObject,就可以序列化字典了!

csharp 复制代码
public MyScriptableObject : SerializedScriptableObject
{
	// 该字典将被 Odin 序列化
	public Dictionary<int, string> IntStringMap;
}

6.2 使用 Unity 序列化字典

​ 通过创建继承 Dictionary 和 Unity 的 ISerializationCallbackReceiver 接口的新类,可以将 Dictionary 数据转换为 Unity 可以序列化的格式。

csharp 复制代码
public abstract class UnitySerializedDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
	[SerializeField, HideInInspector]
	private List<TKey> keyData = new List<TKey>();
	
	[SerializeField, HideInInspector]
	private List<TValue> valueData = new List<TValue>();

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
		this.Clear();
		for (int i = 0; i < this.keyData.Count && i < this.valueData.Count; i++)
		{
			this[this.keyData[i]] = this.valueData[i];
		}
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
		this.keyData.Clear();
		this.valueData.Clear();

		foreach (var item in this)
		{
			this.keyData.Add(item.Key);
			this.valueData.Add(item.Value);
		}
    }
}

​ 由于 Unity 不序列化泛型类型,因此必须通过继承 UnitySerializedDictionary 来创建具体的 Dictionary 类型:

csharp 复制代码
[Serializable]
public class KeyCodeGameObjectListDictionary : UnitySerializedDictionary<KeyCode, List<GameObject>> { }

[Serializable]
public class StringScriptableObjectDictionary : UnitySerializedDictionary<string, ScriptableObject> { }

​ 这样一来,就有了可以在 Unity 中使用的字典。

csharp 复制代码
public class MyScriptableObject : ScriptableObject
{
	public KeyCodeGameObjectListDictionary KeyCodeMap;

	public StringScriptableObjectDictionary StringMap;
}

​ 这些 UnitySerializedDictionaries 可以像任何普通的字典一样在 Inspector 窗口中显示,甚至可以在 Odin Inspector 中显示为普通的字典。

注意:Unity 的预制件修改系统可能存在一些奇怪的交互,且预制件修改后的值可能无法在 Inspector 窗口中正确显示。

7 序列化协议

7.1 Odin 序列化协议

​ 在 MonoBehaviours、ScriptableObjects 等中使用 Odin 进行序列化时,具有如下特点:

  1. Odin 不会替换 Unity 序列化器,而是扩展它。

    Odin 通过将非 Unity 序列化的字段和属性转换为 Unity 可以序列化的数据格式来实现这一点。

  2. Odin 将序列化数据存储在 SerializationData 字段中。

    Unity 的序列化器将启动并使用自己的序列化器序列化 Odin 序列化数据,并将其写入某个位置的文件。

​ 这种实现具有很多优势:

  • 利用了 Unity 的所有现有系统,因此不会改变通常的 Unity 工作流程。
  • 允许 Unity 处理其自己对象引用的序列化。这允许在几乎任何地方引用 Unity 对象,并且这些引用在播放器中仍然有效。

​ 但此种实现也带来了一个缺点:对于同一字段,可能序列化两次:一次是 Unity,一次是 Odin。对此,Odin 将尝试智能地过滤掉应该已经由 Unity 序列化的字段:

csharp 复制代码
public class MyScriptableObject : SerializedScriptableObject
{
    // Unity 可以序列化基本类型,因此 Odin 会跳过这个字段。
    public float PublicFloat;

    // 这个字段会被 Unity 和 Odin 都跳过。
    private float privateFloat;
    
    // OdinSerialize 强制 Odin 序列化这个字段。然而,这个字段也会同时被 Unity 序列化,导致数据重复。
    [OdinSerialize]
    public float OdinAndUnityFloat;

    // 在 Odin 中,NonSerialized 属性会被 OdinSerialize 属性覆盖。Unity 不会序列化这个字段,但 Odin 会。
    [NonSerialized, OdinSerialize]
    public float OdinOnlyFloat;
}

​ 序列化程序仅在根级别选择:类或结构的任何类成员都将由序列化程序序列化,该序列化最终包含该值的"根"字段:

csharp 复制代码
public class MyScriptableObject : SerializedScriptableObject
{
    // 由于 MySerializedObject 类定义了 Serializable 属性,所以 Unity 会序列化这个字段。
    // 但是,Unity 不会序列化 Dictionary 字段,因此 Dictionary 字段不会被序列化。
    public MySerializedObject UnitySerialized;

    // 和上面的例子一样,Odin 被强制序列化这个字段。
    // Dictionary 将会被保存。
    [NonSerialized, OdinSerialize]
    public MySerializedObject OdinSerialized;
}

// 另一种强制 Odin 序列化这个类的方法是移除 MySerializedObject 定义中的 Serializable 属性。
// 这样做有效,因为 Unity 只会序列化带有 Serializable 属性的类型。
[Serializable]
public class MySerializedObject
{
    // 在这里使用类似 [NonSerialized, OdinSerialize] 的属性不会有区别,
    // 因为这种类型的序列化取决于选择的顶层序列化器。
    public Dictionary<int, string> MyDictionary;
}

​ 一个好的经验法则:如果某个属性或字段未显示在 Inspector 窗口中(不使用 ShowInInspector 属性之类的内容),则不会序列化该成员。

注意:

  1. Odin Serializer 速度非常快,但由于它扩展了 Unity 序列化器,因此它只能增加序列化和反序列化时间。

  2. 最重要的是,Unity 序列化器具有刻意的简单设计和原生实现,非常非常快------比 Odin 快得多。

    因此,如果使用 Unity 序列化就能满足要求,那么这绝对是推荐的方法。

7.2 Unity 序列化协议

  1. Unity 仅序列化定义 Serializable 属性的任何类或结构。

  2. Unity 不会序列化任何泛型类型变体,例如 Dictionary。

    但是,可以通过继承泛型类型来对泛型类进行具体定义,并且该类和所有泛型字段(当然其他规则仍然适用于这些)将由 Unity 序列化。List 是此规则的一个例外。

  3. Unity 不会序列化任何多态引用,因此不会序列化任何抽象、接口或对象字段。

  4. Unity 不会序列化 DateTime 字段或 mscorlib 中定义的任何其他结构和类。

  5. 根据版本的不同,Unity 不支持所有基础枚举类型。

    Unity 5.6 版引入了对除 long 和 ulong 之外的所有基元类型的支持。在此版本之前,仅支持 byte 和 int 枚举类型。

8 使用 Odin 保存数据

  1. 创建一个数据类,其包含多种数据类型,使数据与 Unity 组件分开。因为在运行时,Odin 无法保存对实际 Unity 对象的引用或内容,即 Unity 对象的引用或依赖关系。因此,建议将其分离为一个干净的 C# 数据类。
csharp 复制代码
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;

[Serializable]
public class Data
{
    public int     i;
    public float   f;
    public string  s;
    public Vector3 v;

    public List<GameObject> lg = new List<GameObject>();

    [ShowInInspector]
    public Dictionary<string, Vector2> dsv = new Dictionary<string, Vector2>();
}
  • 需要添加 Serializable 特性,以实现该类的序列化。
  • 使用 ShowInInspector 特性,可以在 Inspector 窗口中查看该 Dictionary。
  1. 创建 Test 脚本,其包含一个 Data 成员作为数据,并添加如下特性:

    • SerializeField:使该数据被序列化(public 成员可默认不指定该特性)。
    • InlineProperty:将 data 内容显示在标签旁,而不是以折叠方式呈现。
    • LabelWidth:指定 data 标签长度。

    同时编写存储(Svae)和读取(Load)数据的方法,使用 Button 特性将其作为按钮使用。

csharp 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;

public class Test : MonoBehaviour
{
    [SerializeField, InlineProperty, LabelWidth(45)]
    public Data data;

    [Button]
    public void Save() {
        byte[] bytes = SerializationUtility.SerializeValue(data, DataFormat.Binary);
        File.WriteAllBytes(Application.streamingAssetsPath + "/data", bytes);
    }

    [Button]
    public void Load() {
        byte[] bytes = File.ReadAllBytes(Application.streamingAssetsPath + "/data");
        data = SerializationUtility.DeserializeValue<Data>(bytes, DataFormat.Binary);
    }
}
  1. 将 Test 脚本拖拽到任意一个场景物体上,并在 Inspector 窗口上随意更改 Test 中的 data 数据。点击 Save 按钮,则 data 数据被存储到 StreamingAssets 文件夹下的二进制文件 data 中(可右键点击 Fresh 刷新 Project 窗口查看)。
  1. 清除数据(remove 脚本再重新拖拽进来),点击 Load 按钮,可以看到数据被加载。这里 Lg 中的成员为 None,是因为 Odin 序列化不保存 Unity 对象的引用,这点需要注意。

9 序列化调试器

​ Odin 包含一个序列化调试实用程序,如果遇到值消失或未显示在检查器中的问题,它可以为提供帮助。

​ 调试器将显示:

  • 任何给定类型的哪些成员正在序列化;
  • 是否由 Unity、Odin 还是两者共同序列化。
  • 从序列化的角度,提供任何给定成员所发生情况的详细描述。

打开方式:

  1. Tools > Odin > Serializer > Serialization Debugger 访问窗口,然后从下拉列表中选择脚本类型以开始调试。
  1. 单击组件齿轮下拉菜单中的"调试序列化"按钮直接开始调试组件。

10 自定义序列化类

​ 使用 SerializationUtility 类,该类包装序列化系统并处理所涉及的所有样板代码。示例代码如下所示:

csharp 复制代码
// 定义一个包含一些随机垃圾字符串数据、随机数字、Unity 对象和循环引用的类
public class MyData
{
    public string str = new string(Enumerable.Range(0, 20).Select(i => (char)UnityEngine.Random.Range(50, 150)).ToArray());
    public List<float> numbers = new List<float>(Enumerable.Range(0, 10).Select(i => UnityEngine.Random.Range(0f, 100f)));
    public GameObject unityObjectReference = UnityEngine.Object.FindObjectOfType<UnityEngine.GameObject>();
    public MyData reference;
}

// 某处的方法可能如下所示,用于将数据序列化为 JSON
private void SerializeData()
{
    // 保存到 Assets 文件夹
    string path = Application.dataPath + "/data.json";

    // 初始化一些数据
    var originalData = new MyData();
    originalData.reference = new MyData();
    originalData.reference.reference = originalData;

    // Unity 应该能够处理其自己奇怪对象的序列化和反序列化。
    // 如果你的数据图包含 UnityEngine.Object 类型,你需要为 Odin 提供一个 UnityEngine.Object 列表,
    // Odin 将使用它作为外部引用解析器。
    List<UnityEngine.Object> unityObjectReferences = new List<UnityEngine.Object>();

    // 数据格式
    DataFormat dataFormat = DataFormat.JSON;
    // DataFormat dataFormat = DataFormat.Binary;
    // DataFormat dataFormat = DataFormat.Nodes;

    // 序列化
    {
        var bytes = SerializationUtility.SerializeValue(originalData, dataFormat, out unityObjectReferences);
        File.WriteAllBytes(path, bytes);

        // 如果你需要 JSON 字符串,使用 UTF8 编码
        // var jsonString = System.Text.Encoding.UTF8.GetString(bytes);
    }

    // 反序列化
    {
        var bytes = File.ReadAllBytes(path);

        // 如果你有要反序列化的字符串,使用 UTF8 编码获取字节
        // var bytes = System.Text.Encoding.UTF8.GetBytes(jsonString);

        var data = SerializationUtility.DeserializeValue<MyData>(bytes, dataFormat, unityObjectReferences);
    }
}

​ 如果查看生成的 json 文件,会看到 Odin 的 json 格式在特殊的 $ 前缀条目中保留了一堆元数据。因此,其他 Json 序列化器可能无法正常解析 Odin 序列化的 json 文件。Odin 序列化的 json 文件规定了额外元数据的特殊格式,例如类型信息,因此生成的 json 文件可能并不是标准的序列化 json。

注意:

  1. 是否应该使用它取决于您的确切用例。如果只是想将一些非常简单的数据保存到 json 中,建议使用 Unity 自己的 JsonUtility 类 - 它比 Odin 的 json 序列化快得多,后者相对较慢。(

  2. Odin 的二进制格式非常快且高度优化。相比之下,json 优化不是那么多。因为它优先级不高,且默认情况下,Json 从未在 Odin 中使用。

11 AOT 序列化

​ 详情见 https://odininspector.com/tutorials/serialize-anything/aot-serialization#odin-inspector

12 建议与总结

  • 尽可能使用 Unity 自己的序列化器!

  • 对于 MonoBehaviours/Components 和 ScriptableObjects,Odin 序列化程序不会替换,而是扩展现有的 Unity 序列化程序。

    1. Odin 首先序列化 Unity 未接触的所有数据,并将其转换为 Unity 可以理解的数据存储格式和 Unity 对象列表。
    2. 然后,Unity 将此数据与对象的其余部分一起序列化。

    因此,总会有两个序列化传递。当然这也意味着使用 Odin 序列化器总是比仅使用 Unity 的序列化器慢。

​ 因此,建议仅在 Unity 序列化器无法满足要求时才使用 Odin。

相关推荐
__water4 小时前
『功能项目』回调函数处理死亡【54】
c#·回调函数·unity引擎
__water4 小时前
『功能项目』眩晕图标显示【52】
c#·unity引擎·动画事件
__water5 小时前
『功能项目』第二职业法师的平A【57】
c#·unity引擎·魔法球伤害传递
躺下睡觉~6 小时前
Unity-Transform类-父子关系
java·unity·游戏引擎
躺下睡觉~6 小时前
Unity-Transform类-缩放和看向
unity·游戏引擎
__water7 小时前
『功能项目』战士的伤害型技能【45】
c#·unity引擎·战士职业伤害型技能
君莫愁。8 小时前
【Unity】检测鼠标点击位置是否有2D对象
unity·c#·游戏引擎
Lingbug9 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
咩咩觉主9 小时前
Unity实战案例全解析:PVZ 植物卡片状态分析
unity·c#·游戏引擎
Echo_Lee09 小时前
C#与Python脚本使用共享内存通信
开发语言·python·c#