学习Unity Editor之ScriptableObject

洪宏硕
2023-12-01

SciptableObject

根据Unity官方文档的描述,ScriptableOjbect是当我们想创建一个不需要绑在特定game objects上的对象时所使用的类。

ScriptableObject可以当做数据容器来使用,它不需要绑定在场景的特定GameObject上。我们可以将ScriptableOjbects当做assets保存在我们的项目中。

通常来说,ScriptableOjbects主要用来保存数据,但同时我们也可以用它来在场景中生成序列化对象

using UnityEngine;
using System.Collections;

public class MyScriptableOjbectClass : ScriptableObject
{
    public string objectName = "ScriptableObject";
    public bool isColor;
    [MenuItem("MyTool/tool")]
    static void tool()
    {
        Debug.Log("Tool is running");
    }
}

通过上述代码将在Editor中新增一项菜单,通过点击tool菜单项,我们就可以调用tool函数,执行相关运算。

脚本序列化

原文:https://docs.unity3d.com/Manual/script-Serialization.html

序列化可以自动地将数据结构或者对象状态转化为一种Unity引擎可以保存并再创造的格式。Unity中一些功能也使用了序列化,比如保存和载入,inspector窗口,prefabs. 

由此可见,数据的组织方式将影响Unity序列化这些数据,从而对项目的性能造成很大影响。

理解热重载(hot reloading)

Hot reloading

热重载是一种当编辑器打开的情况下,我们创建或者修改脚本,脚本逻辑可以马上被应用在项目中,而不需要为了让改动生效重启游戏或者Editor。

当我们改动并保存一个脚本时,Unity引擎热重载所有当前已经被载入的脚本数据。它首先在所有的载入脚本中保存可序列化的变量,然后在载入脚本之后,它将恢复这些变量。而所有不能够被序列化的数据将在热更新之后丢失。

Saving and loading

Unity引擎使用序列化来从硬盘中读取和保存场景(Scenes),Assets和AssetBundles。这还将包括在脚本的API中保存的数据比如MonoBehaviour 组件和ScriptableOjbects.

The Inspector Window

当我们在Unity引擎中使用inspector window查看或者修改一个GameObject组件的字段时,Unity 将对应的数据序列化并再Inspector window中显示出来。当Inspector window显示一个字段的值时它实际上并不会和Unity的脚本API进行交互。

举例来讲,如果我们在脚本中使用了属性,当我们在Inspector window中修改或者查看属性的值的时候,脚本中的任一个该属性的getter和setter方法都不会被调用。这意味着,虽然Inspector window中一个字段的值表示着脚本属性,但是修改这些值将不会导致Inspector调用脚本中如何的属性setter和getter方法。

Serialization rules(序列化规则)

Unity中的序列器(serializer)在一个实时的游戏环境中运行,它对性能有很大的影响。所以,Unity中的序列化和其他编程环境的序列化会有所不同。以下是一些如何在Unity中使用序列化的一些提示。

如何确保脚本中的字段是可序列化的

  • is public, or  has a SerializeField attribute
  • is not static
  • is not const
  • is not readonly
  • Has a fieldtype can be serialized

可被序列化的简单类型字段

  • Custom non-abstract, non-generic classes with the Serilizable attribute
  • Custom structs with the Serializable attribute
  • References to objects that derive from UnityEngine.Object
  • Primitive data types(int, float, double, bool, string, etc)
  • Enum types
  • Certain Unity built-in types: Vector2, Vector3, Vector4, Rect, Quaternion, Matrix4*4, Color, Color32, LayerMask, AnimationCurve, Gradient, RectOffset, GUIStyle

可被序列化的容器类型。

  • An array of a simple filed type that can be serialized
  • A List<T> of a simple filed type that can be serialized

需要注意的是,Unity并不支持多层类型(multidimensional arrays, jagged arrays and nested containers)的序列化。如果我们想序列化这两种类型,我们可以1.将nested type封包进一个class或者struct. 2.使用serialization callbacks ISerializationCallbackReceiver 来进行自定义的序列化。

如何保证一个自定义类可以被序列化

  • Has the Serializable attribute
  • is not abstract
  • is not generic
  • is not static

当什么情况下序列化会表现的异常呢

自定义类表现的像结构体

对于不是继承于UnityEngine.Object 的类Unity引擎序列化时将直接通过进行序列化,就像它序列化结构体一样。

如果我们在几个不同的字段中保存了一个对于自定义类实例的引用,那么他们在序列化时将变成分开的对象。在这之后,当Unity引擎解序列化这些字段时,他们将会包含不同的对象,但每个对象的数据却是相同的。

当我们需要序列化一个复杂的包含引用的对象图时,不要让Unity自行序列化这样的对象。应该使用ISerializationCallbackReceiver来手动对他们进行序列化。这样做可以防止Unity从对象引用生成多个对象。

以上仅适用于自定义的类。Unity用inline的方式序列化自定义类因为他们的数据将会变成MonoBehaviour或ScriptableOjbect所使用的完整序列化数据的一部分。当字段引用的是UnityEngine.Object引申的类时,比如说public Camera myCamera. Unity会给camera这个UnityEngine.Object序列化一个引用。这同样也适用于继承自MonoBehaviour或者ScriptableObject的脚本,因为他们都继承自UnityEngine.Object.

对于自定义类不支持null

class Test : MonoBehaviour
{
    public Trouble t;
}

[Serializable]
class Trouble
{
    public Trouble t1;
    public Trouble t2;
    public Trouble t3;
}

在以上代码中不难想到至少会有两次allocations,一次是Test对象,一次则是Trouble对象。

然而,Unity 实际上在上述代码中至少做了一千次以上的allocations. 序列器不支持null. 当它序列化一个对象时,并且一个字段为null, Unity会实例化该类型的一个新对象,然后序列化之。显然这将导致无限循环,Unity目前支持七层的调用深度,当调用次数超过深度时,Unity将会停止序列化带有自定义类型的类,结构体,列表或者数组。

因为Unity许多的子系统都是建立在序列化系统之上的,这个出自Test MonoBehaviour的异常且巨大的序列化流将导致所有的子系统的性能变慢。

不支持多态

如果我们有一个public Animal[] animals数组,然后我们往数组内存入a Dog, a Cat and a Giraffe, 在序列化之后,我们只会有三个Animal的实例。

一个解决这个限制的方法就是认识到这仅适用于自定义类,因为他们在序列化时会被inline操作。 对于其他的UnityEngine.Objects的引用将会被序列化为引用,而且对于这些类,多态是可行的。我们可以写一个继承ScriptableObject的类或者继承MonoBehavior的类并引用它们。这样做的弊端在于我们需要将MonoBehaviour或Scriptable 的对象保存到某处,这样一来将失去高效序列化的优势。

Tips

Optimal use of serialization

我们可以通过组织数据来确保将Unity的序列化使用达到最优。

  • Orgnanise your data with the aim to have Unity serialize the smallest possible set of data. The primary purpose of this is not to save space on your computer's hard drive,but to make sure that you can maintain backwards compatibility with previous versions of the project. Backwards compatibility can become more diffucult later on in development if you work with large sets of serialized data.
  • Organise your data to never hae Unity serialize duplicate data or cached data. This causes significant problems for backwards compatibility: it carries a high risk of error because it is too easy for dat to get out of sync.
  • Avoid nested, recursive structures where you reference other classes. The layout of a serializd structrue always needs to be the same; independent of the data and only dependent on what is exposed in the script. The only way to reference other objects is through classes derived from UnityEngine.Object. These classes are completely seprate; the only reference each other and they don't embed the contents.

让Ediotr代码可以热重载

当重载脚本时,Unity 在所有加载的脚本中保存序列化的全部变量。在重加载脚本后,Unity将这些变量恢复到他们原本的,未被序列化之前的数值。

当重载脚本时,Unity恢复所有满足序列化条件的变量-包括哪些私有变量,甚至没有SerializeField属性的变量也包括在内。在某些情况,我们可能希望特定的私有变量不被恢复:比如说,我们希望重载脚本后一个引用可以为null.在这时,我们需要使用NonSerialized属性。

Unity不会恢复静态变量,所以如果我们想在重载脚本后能恢复变量状态就不应当使用静态变量。

 

 类似资料: