Unity 脚本编译和构建(Build)说明(官方文档翻译校正)

目录

脚本重要概念

事件函数的执行顺序

事件函数是一组内置事件, MonoBehaviour 脚本可以通过实现适当的方法来选择性地订阅这些事件,通常称为回调。这些回调对应于 Unity 核心子系统中的事件,例如物理、渲染和用户输入,或者脚本生命周期的各个阶段,如创建、激活、帧依赖和帧独立更新以及销毁。当事件发生时,Unity 会在您的脚本上调用关联的回调,让您有机会实现对该事件的逻辑响应。
由于 Unity 以预定顺序触发这些事件并调用关联的 MonoBehaviour 回调,因此这里记录了这些顺序。了解执行顺序很重要,这样您就不会尝试使用一个回调来完成依赖于尚未调用的另一个回调的工作。但是请记住,有些回调是针对用户输入等事件,这些事件可能会在游戏运行时的任何时候发生。您应该结合 MonoBehaviour 脚本参考(事件回调在 Messages 下列出)来全面了解每个事件的意义和限制。

脚本生命周期概述

下图总结了 Unity 如何在脚本的生命周期内安排和重复事件函数的执行顺序。
在这里插入图片描述

流程图的范围

以下流程图的范围仅限于您可以通过在 MonoBehaviour 脚本上实现适当回调来订阅的内置事件函数,这些回调记录在 MonoBehaviour 脚本参考的 Messages 下。还显示了一些引发事件的子系统本地的附加内部方法以提供上下文。

除了这些内置事件函数之外,您还可以在脚本中订阅许多其他事件。几个主要类如 Application、SceneManager 和 Camera 提供了您可以注册自己的回调方法的委托。像 RuntimeInitializeOnLoadMethodAttribute 这样的方法属性也可以用于在场景的某些阶段执行方法。有关您感兴趣的组件或子系统的事件回调以及它们的执行顺序的详细信息,请参阅脚本参考。

脚本生命周期流程图

注意:某些浏览器不支持 SVG 图像文件。如果上图未正确显示(例如,您看不到任何文本),请尝试其他浏览器,例如 Google Chrome 或 Mozilla Firefox。

一般原则

一般来说,您不应该依赖于为不同 GameObjects 调用同一事件函数的顺序,除非顺序被明确记录或可设置。如果您需要更精细地控制播放器循环,可以使用 PlayerLoop API。

您不能指定为同一 MonoBehaviour 子类的不同实例调用事件函数的顺序。例如,一个 MonoBehaviour 的 Update 函数可能在另一个 GameObject 上的同一 MonoBehaviour 的 Update 函数之前或之后调用,包括它自己的父或子 GameObjects。

您可以通过项目设置窗口的脚本执行顺序面板指定应在不同子类的事件函数之前调用一个 MonoBehaviour 子类的事件函数。例如,如果您有两个脚本,EngineBehaviour 和 SteeringBehaviour,您可以设置脚本执行顺序,使 EngineBehaviours 总是在 SteeringBehaviours 之前更新。如果以叠加方式加载多个场景,配置的脚本执行顺序会逐个场景完全应用,而不是跨场景部分应用,因此 EngineBehaviours 和 SteeringBehaviours 会在一个场景上更新,然后才在下一个场景上更新。

第一次场景加载(First Scene load)

这些函数在场景开始时调用(每个场景中的每个对象一次)。

  • Awake:当创建对象的新实例时调用的第一个生命周期函数。始终在任何 Start 函数之前调用。如果 GameObject 在启动期间处于非活动状态,则在激活它之前不会调用 Awake。
  • OnEnable:在对象变得启用和活动时调用,始终在 Awake(在同一个对象上)之后和任何 Start 之前调用。

对于属于场景资源的对象,所有脚本的 Awake 和 OnEnable 函数都在任何一个的 Start 和后续函数调用之前调用。但是,当您在运行时实例化对象时,这无法强制执行。

Awake 仅在每个对象的范围内保证在 OnEnable 之前调用。跨多个对象的顺序是不确定的,您不能依赖一个对象的 Awake 在另一个对象的 OnEnable 之前调用。任何依赖于场景中所有对象的 Awake 已被调用的工作都应在 Start 中完成。

场景加载和卸载之前(Before scene load and unload)

上述图表中未显示的是 SceneManager.sceneLoaded 和 SceneManager.sceneUnloaded 事件,这些事件允许您在场景分别加载和卸载时接收回调。有关详细信息和示例用法,请参阅相关的脚本参考页面。您可以期待在 OnEnable 之后但在所有对象的 Start 之前接收到 sceneLoaded 通知。有关禁用域和场景重载的详细信息,请参阅包括场景加载在内的执行流程图。

您还可以使用 RuntimeInitializeOnLoadMethodAttribute 及其类型 BeforeSceneLoad 和 AfterSceneLoad 在场景加载之前或之后运行您的方法。有关标记有这些类型的方法的执行顺序信息,请参阅 RuntimeInitializeOnLoadMethodAttribute 脚本参考主页面。

Editor

  • Reset:在将脚本首次附加到对象时以及使用重置命令时调用以初始化脚本的属性。
  • OnValidate:每当设置脚本的属性时调用,包括当对象被反序列化时,这可能在各种时间发生,例如在编辑器中打开场景以及域重载之后。

第一帧更新之前(Before the first frame update)

  • Start:仅在脚本实例启用时在第一帧更新之前调用。对于属于场景资源的对象,Start 函数在所有脚本的 Update 调用之前调用。但是,当您在游戏过程中实例化对象时,无法强制执行。例如,如果您从另一个对象的 Update 函数实例化对象,则实例化对象的 Start 不能在原始对象的 Update 首次运行之前调用。

帧之间(In between frames)

  • OnApplicationPause:在检测到暂停的帧结束时调用,有效地在正常帧更新之间。调用 OnApplicationPause 之后将发出一个额外的帧,以便游戏显示表示暂停状态的图形。

更新顺序(Update Order)

当您跟踪游戏逻辑和交互、动画、相机位置等时,可以使用一些不同的事件。常见的模式是在 Update 函数中执行大多数任务,但也有其他函数可以使用。

  • FixedUpdate:在游戏时间的固定间隔而不是每帧调用。由于这些更新是固定的,而帧速率是可变的,在帧速率高时可能不会有固定更新,而在帧速率低时每帧可能有多个固定更新。所有物理计算和更新都在 FixedUpdate 之后立即进行,由于它是帧速率独立的,因此在 FixedUpdate 中计算运动时无需乘以 Time.deltaTime。固定更新的间隔由 Time.fixedDeltaTime 定义,可以直接在脚本中设置或通过编辑器的时间设置中的固定时间步属性设置。有关更多信息,包括用于确定是否执行 Update 或 FixedUpdate 的时间计算,请参阅 Time。

  • Update:每帧调用一次,是帧更新的主要函数。

  • LateUpdate:在 Update 完成后每帧调用一次。在 LateUpdate 开始时,Update 中执行的任何计算都已完成。LateUpdate 的常见用途是跟随第三人称相机。如果您在 Update 中让角色移动和转向,可以在 LateUpdate 中执行所有相机移动和旋转计算。这将确保角色已完全移动,然后相机跟踪其位置。

动画更新循环(Animation update loop)

以下动画循环回调在从 MonoBehaviour 派生的脚本上调用:

  • MonoBehaviour.OnAnimatorMove
  • MonoBehaviour.OnAnimatorIK

其他与动画相关的事件函数在从 StateMachineBehaviour 派生的脚本上调用:

  • StateMachineBehaviour.OnStateMachineEnter
  • StateMachineBehaviour.OnStateMachineExit
  • StateMachineBehaviour.OnStateEnter
  • StateMachineBehaviour.OnStateUpdate
  • StateMachineBehaviour.OnStateExit
  • StateMachineBehaviour.OnStateMove
  • StateMachineBehaviour.OnStateIK

有关这些回调的意义和限制,请参阅相关的脚本参考页面。
图表中显示的其他动画函数是动画系统的内部函数,仅用于提供上下文。这些函数具有关联的 Profiler 标记,因此您可以使用 Profiler 查看 Unity 在帧中的调用时间。了解 Unity 调用这些函数的时间可以帮助您准确了解调用的事件函数何时执行。有关动画函数和 Profiler 标记的完整执行顺序,请参阅 Profiler 标记。

渲染(Rendering)

此执行顺序仅适用于内置渲染管线。有关基于脚本渲染管线的渲染管线执行顺序

的详细信息,请参阅 Universal Render Pipeline 或 High Definition Render Pipeline 文档的相关部分。如果您想在渲染之前立即进行工作,请参阅 Application.onBeforeRender。

  • OnPreCull:在相机剔除场景之前调用。剔除确定相机可见的对象。OnPreCull 在剔除发生之前调用。
  • OnBecameVisible/OnBecameInvisible:当对象对任何相机变得可见/不可见时调用。由于对象可能在任何时间变得不可见,因此 OnBecameInvisible 未在上述流程图中显示。
  • OnWillRenderObject:如果对象可见,则为每个相机调用一次。
  • OnPreRender:在相机开始渲染场景之前调用。
  • OnRenderObject:在所有常规场景渲染完成后调用。您可以使用 GL 类或 Graphics.DrawMeshNow 在此时绘制自定义几何图形。
  • OnPostRender:在相机完成场景渲染后调用。
  • OnRenderImage:在场景渲染完成后调用以允许对图像进行后处理,参见后处理效果。
  • OnGUI:在响应 GUI 事件时每帧多次调用。首先处理布局和重绘事件,然后为每个输入事件处理布局和键盘/鼠标事件。
  • OnDrawGizmos:用于在场景视图中绘制 Gizmos 以进行可视化。

注意:OnPreCull、OnPreRender、OnPostRender 和 OnRenderImage 是在 MonoBehaviour 脚本上调用的内置 Unity 事件函数,但仅当这些脚本附加到启用的 Camera 组件的同一个对象时。如果您想在附加到不同对象的 MonoBehaviour 上接收 OnPreCull、OnPreRender 和 OnPostRender 的等效回调,您必须使用等效的委托(注意名称中的小写 on) Camera.onPreCull、Camera.onPreRender 和 Camera.onPostRender,如相关脚本参考页面中的代码示例所示。

协程(Coroutines)

正常的协程更新在 Update 函数返回后运行。协程是一种可以暂停其执行(yield)直到给定的 YieldInstruction 完成的函数。

协程的不同用法:

  • yield:协程将在下帧调用所有 Update 函数后继续。
  • yield WaitForSeconds:在指定的时间延迟后继续,在该帧调用所有 Update 函数后继续。
  • yield WaitForFixedUpdate:在调用所有脚本的 FixedUpdate 之后继续。如果协程在 FixedUpdate 之前暂停,则在当前帧的 FixedUpdate 之后恢复。
  • yield WWW:在 WWW 下载完成后继续。
  • yield StartCoroutine:链式协程,如果理论上的协程 coroutineA 使用 yield StartCoroutine(coroutineB()) 启动另一个 coroutineB,则 coroutineA 暂停并等待 coroutineB 完成后再继续。有关示例,请参阅 MonoBehaviour.StartCoroutine。

当对象被销毁时(When the Object is destroyed)

  • OnDestroy:此函数在对象存在的最后一帧的所有帧更新之后调用(对象可能响应 Object.Destroy 或在场景关闭时被销毁)。

当退出时(When quitting)

这些函数在场景中的所有活动对象上调用:

  • OnApplicationQuit:在应用程序退出之前在所有游戏对象上调用。在编辑器中,当用户停止播放模式时调用。
  • OnDisable:当行为变得禁用或非活动时调用。

协程(Coroutines)

协程允许你将任务分散在多个帧上。在 Unity 中,协程是一种方法,它可以暂停执行并将控制权返回给 Unity,但随后在下一帧继续执行。

在大多数情况下,当你调用一个方法时,它会运行到完成,然后将控制权返回给调用方法,并附带任何可选的返回值。这意味着任何在方法内部发生的操作都必须在单个帧更新内完成。

在你希望方法调用包含过程动画或随时间进行的事件序列的情况下,可以使用协程。

然而,重要的是要记住,协程不是线程。在协程中运行的同步操作仍然在主线程上执行。如果你想减少主线程上使用的 CPU 时间,那么避免在协程中阻塞操作和其他脚本代码一样重要。如果你想在 Unity 中使用多线程代码,可以考虑 C# Job System 。

如果你需要处理长时间的异步操作,例如等待 HTTP 传输、资产加载或文件 I/O 完成,最好使用协程。

协程示例

例如,考虑逐渐减少一个对象的 alpha(不透明度)值直到它变为不可见的任务:

void Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
    }
}

在这个例子中,Fade 方法不会产生你期望的效果。为了使淡出效果可见,你必须在一系列帧上减少 alpha 值以显示 Unity 渲染的中间值。然而,这个示例方法在单个帧更新内执行完毕。中间值不会显示,对象会立即消失。

为了解决这个问题,你可以在 Update 函数中添加代码,使淡出效果按帧进行。然而,使用协程来处理这类任务可能更方便。

在 C# 中,你可以这样声明一个协程:

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return null;
    }
}

协程是你用 IEnumerator 返回类型声明的方法,并在方法体中包含 yield return 语句。yield return null 行是执行暂停并在下一帧恢复的点。要运行协程,你需要使用 StartCoroutine 函数:

void Update()
{
    if (Input.GetKeyDown("f"))
    {
        StartCoroutine(Fade());
    }
}

Fade 函数中的循环计数器在协程的生命周期内保持其正确的值,并且任何变量或参数在 yield 语句之间都会被保留。

协程时间延迟

默认情况下,Unity 会在 yield 语句后的帧恢复协程。如果你想引入时间延迟,可以使用 WaitForSeconds

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return new WaitForSeconds(.1f);
    }
}

你可以使用 WaitForSeconds 将效果分散在一段时间内,并且可以将其作为替代方案,以避免在 Update 方法中包含任务。Unity 每秒调用 Update 方法多次,所以如果你不需要任务如此频繁地重复,可以将其放在协程中,以获得定期更新但不是每一帧。

例如,你可能在你的应用程序中有一个报警器,当敌人接近时警告玩家,代码如下:

bool ProximityCheck()
{
    for (int i = 0; i < enemies.Length; i++)
    {
        if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
            return true;
        }
    }
    return false;
}

如果有很多敌人,每帧调用这个函数可能会引入显著的开销。然而,你可以使用协程每十分之一秒调用一次:

IEnumerator DoCheck()
{
    for(;;)
    {
        if (ProximityCheck())
        {
            // 执行一些操作
        }
        yield return new WaitForSeconds(.1f);
    }
}

这样减少了 Unity 进行检查的次数,而不会对游戏玩法产生明显影响。

停止协程

要停止协程,可以使用 StopCoroutineStopAllCoroutines。如果你通过将 GameObject 设置为 false 来禁用协程附加的 GameObject,协程也会停止。调用 Destroy(example)(其中 example 是一个 MonoBehaviour 实例)会立即触发 OnDisable,Unity 会处理协程,从而有效地停止它。最后,在帧结束时调用 OnDestroy

注意:如果你通过将 enabled 设置为 false 来禁用一个 MonoBehaviour,Unity 不会停止协程。

使用 Profiler 分析协程

协程的执行方式与其他脚本代码不同。大多数 Unity 中的脚本代码在性能跟踪中会出现在一个单独的位置,位于特定回调调用下。然而,协程的 CPU 代码总是出现在跟踪中的两个位置。

协程中的所有初始代码,从协程方法的开始到第一个 yield 语句,都会出现在 Unity 启动协程时的跟踪中。初始代码通常出现在调用 StartCoroutine 方法时。Unity 回调生成的协程(如返回 IEnumerator 的 Start 回调)首先出现在它们各自的 Unity 回调中。

协程代码的其余部分(从第一次恢复到执行完成)出现在 Unity 主循环内的 DelayedCallManager 行中。

这是因为 Unity 执行协程的方式。C# 编译器自动生成一个类实例来支持协程。Unity 然后使用这个对象来跟踪跨多个方法调用的协程状态。因为协程内的局部作用域变量必须在 yield 调用之间保持不变,Unity 将局部作用域变量提升到生成的类中,这些变量在协程期间保留在上。这个对象还跟踪协程的内部状态:它记住在 yield 之后协程必须恢复的代码位置。

因此,启动协程时的内存压力等于固定开销分配加上其局部作用域变量的大小。

启动协程的代码构造并调用一个对象,然后每当满足协程的yield条件时,Unity 的DelayedCallManager再次调用它。因为协程通常在其他协程之外启动,这将它们的执行开销分散在yield调用和DelayedCallManager之间。

你可以使用 Unity Profiler 检查和了解 Unity 在你的应用程序中执行协程的位置。为此,请在启用Deep Profiling的情况下对你的应用程序进行分析,这样会对你的脚本代码的每个部分进行分析并记录所有函数调用。然后你可以使用CPU Usage Profiler模块来调查应用程序中的协程。
在 DelayedCall 中有协程的 Profiler 会话

最佳实践是将一系列操作浓缩为尽可能少的单个协程。嵌套协程对代码的清晰度和维护性有用,但它们会带来更高的内存开销,因为协程会跟踪对象。

如果协程每帧运行并且没有在长时间运行的操作上 yield,那么将其替换为 UpdateLateUpdate 回调会更高效。如果你有长时间运行或无限循环的协程,这一点尤为有用。

预定义的程序集

Unity 根据脚本文件在项目文件夹结构中的位置,以四个不同的阶段编译脚本。Unity 为每个阶段创建一个单独的 CSharp 项目文件 (.csproj) 和一个预定义的程序集。(如果没有符合编译阶段的脚本,Unity 不会创建相应的项目文件或程序集。)

当脚本引用在不同阶段编译的类(因此位于不同的程序集中)时,编译顺序很重要。基本规则是无法引用在当前阶段之后的阶段编译的任何内容。在当前阶段或早期阶段编译的所有内容则是完全可用的。

编译阶段如下:

注意:Standard Assets 仅在 Assets 根文件夹中有效。

条件编译

Unity 对 C# 语言的支持包括使用指令(directives),这些指令允许您根据是否定义了某些脚本符号,有选择地包含或排除代码的编译。

您可以在 Microsoft 的 C# 预处理指令页面上阅读有关这些指令的更多信息。

Unity 提供了一系列内置的脚本符号,您可以在脚本中使用这些符号来有选择地包含或排除部分代码的编译。

例如,当为 Windows 独立平台 build Player 时设置的内置脚本符号是 UNITY_STANDALONE_WIN。您可以使用一种特殊类型的 if 语句来检查是否定义了此符号,如下所示:

#if UNITY_STANDALONE_WIN
  Debug.Log("Standalone Windows");
#endif

# 字符在 if 和 endif 前面表示这些语句是“指令”,在编译过程中处理,而不是在运行时处理。

因此,在上面的示例中,Debug 行仅在为 Windows 独立构建项目时包含在编译中。当在编辑器中或在其他目标构建中编译时,它将被完全省略。

Unity 内置了一些脚本符号,可以根据所选的平台、编辑器版本以及其他杂项系统环境场景来有选择地编译或省略代码。这些内置的脚本符号列在下文中。

此外,您还可以使用编辑器 UI、脚本或资产文件定义自己的脚本符号,从而根据任意定义来控制部分代码的编译。有关更多信息,请参阅自定义脚本符号(Custom scripting symbols)。

注意:脚本符号有时被称为“定义符号”、“预处理定义”或简称“定义”。

平台脚本符号

Unity 会根据创作和构建目标平台自动定义某些脚本符号,如下所示:

定义功能
UNITY_EDITOR用于从游戏代码调用 Unity 编辑器脚本的脚本符号。
UNITY_EDITOR_WIN用于 Windows 上的编辑器代码的脚本符号。
UNITY_EDITOR_OSX用于 Mac OS X 上的编辑器代码的脚本符号。
UNITY_EDITOR_LINUX用于 Linux 上的编辑器代码的脚本符号。
UNITY_STANDALONE_OSX用于 Mac OS X(包括 Universal、PPC 和 Intel 架构)上编译或执行代码的脚本符号。
UNITY_STANDALONE_WIN用于 Windows 独立应用程序上编译或执行代码的脚本符号。
UNITY_STANDALONE_LINUX用于 Linux 独立应用程序上编译或执行代码的脚本符号。
UNITY_STANDALONE用于任何独立平台(Mac OS X、Windows 或 Linux)上编译或执行代码的脚本符号。
UNITY_WII用于 Wii 控制台上编译或执行代码的脚本符号。
UNITY_IOS用于 iOS 平台上编译或执行代码的脚本符号。
UNITY_IPHONE已弃用。请改用 UNITY_IOS。
UNITY_ANDROID用于 Android 平台的脚本符号。
UNITY_LUMIN用于 Magic Leap OS 平台的脚本符号。您也可以使用 PLATFORM_LUMIN。请注意,Lumin 平台不再受支持。
UNITY_TIZEN用于 Tizen 平台的脚本符号。
UNITY_TVOS用于 Apple TV 平台的脚本符号。
UNITY_WSA用于通用 Windows 平台的脚本符号。此外,在使用 .NET 脚本后端编译 C# 文件时,定义 NETFX_CORE。
UNITY_WSA_10_0用于通用 Windows 平台的脚本符号。此外,在使用 .NET Core 编译 C# 文件时,定义 WINDOWS_UWP。
UNITY_WEBGL用于 WebGL 的脚本符号。
UNITY_FACEBOOK用于 Facebook 平台(WebGL 或 Windows 独立)的脚本符号。
UNITY_ANALYTICS用于从游戏代码调用 Unity Analytics 方法的脚本符号。版本 5.2 及以上。
UNITY_ASSERTIONS用于断言控制过程的脚本符号。
UNITY_64用于 64 位平台的脚本符号。

编辑器版本脚本符号

Unity 会根据您当前使用的编辑器版本自动定义某些脚本符号。

给定版本号 X.Y.Z(例如,2019.4.14),Unity 会以以下格式公开三个全局脚本符号:UNITY_XUNITY_X_YUNITY_X_Y_Z

以下是 Unity 2019.4.14 中公开的脚本符号示例:

定义功能
UNITY_2019Unity 2019 版本的脚本符号,在每个 2019.Y.Z 版本中公开。
UNITY_2019_4Unity 2019.4 主要版本的脚本符号,在每个 2019.4.Z 版本中公开。
UNITY_2019_4_14Unity 2019.4.14 次要版本的脚本符号。
您还可以基于编译或执行特定代码段所需的 Unity 最低版本,有选择地编译代码。给定上述版本格式(X.Y),Unity 公开一个格式为 UNITY_X_Y_OR_NEWER 的全局 #define,您可以用于此目的。

其他脚本符号

Unity 定义的其他脚本符号如下:

定义功能
CSHARP_7_3_OR_NEWER在构建支持 C# 7.3 或更高版本的脚本时定义。
ENABLE_MONO用于 Mono 的脚本后端 #define
ENABLE_IL2CPP用于 IL2CPP 的脚本后端 #define
ENABLE_VR在目标构建平台支持 VR 时定义。不意味着当前启用了 VR 或安装了支持 VR 所需的插件和包。
NET_2_0在 Mono 和 IL2CPP 上根据 .NET 2.0 API 兼容性级别构建脚本时定义。
NET_2_0_SUBSET在 Mono 和 IL2CPP 上根据 .NET 2.0 Subset API 兼容性级别构建脚本时定义。
NET_LEGACY在 Mono 和 IL2CPP 上根据 .NET 2.0 或 .NET 2.0 Subset API 兼容性级别构建脚本时定义。
NET_4_6在 Mono 和 IL2CPP 上根据 .NET 4.x API 兼容性级别构建脚本时定义。
NET_STANDARD_2_0在 Mono 和 IL2CPP 上根据 .NET 标准 2.0 API 兼容性级别构建脚本时定义。
NET_STANDARD_2_1在 Mono 和 IL2CPP 上根据 .NET 标准 2.1 API 兼容性级别构建脚本时定义。
ENABLE_WINMD_SUPPORT在 IL2CPP 上启用 Windows 运行时支持时定义。有关更多详细信息,请参阅 Windows 运行时支持。
ENABLE_INPUT_SYSTEM在 Player Settings 中启用 Input System 包时定义。
ENABLE_LEGACY_INPUT_MANAGER在 Player Settings 中启用旧版 Input Manager 时定义。
UNITY_SERVER在构建设置中启用服务器构建设置时定义。
DEVELOPMENT_BUILD在脚本运行于启用了“开发构建”选项构建的播放器中时定义。

自定义脚本符号

除了内置的脚本符号(与平台、编辑器版本和其他杂项系统环境相关),您还可以使用编辑器 UI、脚本或资产文件指定自己的自定义脚本符号。

通过编辑器设置脚本符号

要通过编辑器设置或移除定义指令,请转到 Edit > Project Settings > Player。然后在 Other Settings 面板中,向下滚动到 Script Compilation
在这里插入图片描述

通过脚本定义脚本符号

您可以使用以下 API 定义脚本符号:

  • PlayerSettings.SetScriptingDefineSymbolsForGroup
  • BuildPlayerOptions.extraScriptingDefines
  • Build.Player.ScriptCompilationSettings.extraScriptingDefines
为编辑器脚本编译设置脚本符号

如果需要通过脚本在编辑器中定义脚本符号,以便您的编辑器脚本受到影响,必须使用 PlayerSettings.SetScriptingDefineSymbolsForGroup。但请注意一些重要的细节:

重要:此方法不会立即生效。从脚本调用此方法不会立即应用和重新编译您的脚本。为了使您的指令根据脚本符号的更改生效,必须允许控制返回到编辑器,然后它会异步重新加载脚本并根据新符号和对它们起作用的指令重新编译它们。

例如,如果您在编辑器脚本中使用此方法,然后在同一脚本的下一行立即调用 BuildPipeline.BuildPlayer,此时 Unity 仍在使用旧的脚本符号运行您的编辑器脚本,因为它们尚未使用新符号重新编译。这意味着如果您的编辑器脚本作为 BuildPlayer 执行的一部分运行,它们将使用旧的脚本符号,您的 player 可能不会按预期 build。

在批处理模式下设置脚本符号

上述 Unity 编译的异步特性在您编写将在持续集成(CI)服务器上的 Unity 编辑器中批处理模式运行的编辑器脚本时也很重要。这是因为当编辑器在批处理模式下运行时,它是“无头”运行的(没有GUI也能运行的软件),因此没有编辑器循环导致它使用新脚本符号重新编译。因此,不应使用编辑器脚本在批处理模式 CI 服务器中设置脚本符号,因为脚本不会重新编译,因此不会应用。

相反,如果需要在批处理模式下运行的编辑器中定义特定符号,必须确保编辑器从一开始就使用正确的符号启动。可以通过使用 csc.rsp 资产文件而不是使用编辑器脚本来指定符号,如下所述。

通过 Asset 文件设置脚本符号

您可以通过项目中的 text asset 设置自定义脚本符号。为此,必须在项目的 Assets 文件夹根目录中添加一个定义自定义脚本符号的文本文件,名为 csc.rsp。Unity 会在启动时读取此特殊文件,并在编译任何代码之前应用。

例如,如果在 csc.rsp 文件中包含单行 -define:UNITY_DEBUG,则符号 UNITY_DEBUG 将作为全局定义的脚本符号包含在 C# 脚本中(编辑器脚本除外)。

每次更改 .rsp 文件时,都需要重新编译才能生效。可以通过更新或重新导入单个脚本文件来实现此目的。

注意:如果只想修改全局脚本符号,请通过 Player Settings 窗口在编辑器中将它们添加到 Scripting Define Symbols,因为这覆盖了所有编译器。如果选择使用 .rsp 文件,则需要为 Unity 使用的每个编译器提供一个文件。

程序集定义(Assembly definitions)

Assembly Definition和Assembly Reference(Assembly References)是可创建用于将脚本组织为程序集的 asset 。

程序集是 C# 代码库,其中包含由脚本定义的已编译类和结构并且还定义了对其他程序集的引用。有关 C# 中的程序集的一般信息,请参阅 Assemblies in .NET

默认情况下,Unity 几乎将所有游戏脚本都编译到预定义 程序集 Assembly-CSharp.dll 中。(Unity 还会创建一些较小的专用预定义程序集)。

这种安排对于小型项目而言可以接受,但是在向项目添加更多代码时会有一些缺点:

  • 每次更改一个脚本时,Unity 都必须重新编译所有其他脚本,从而增加迭代代码更改的整体编译时间。
  • 任何脚本都可以直接访问任何其他脚本中定义的类型,这样可能更加难以重构和改进代码。
  • 所有脚本都针对所有平台进行编译。

通过定义程序集,可以组织代码以促进模块化和可重用性。为项目定义的程序集中的脚本不再添加到默认程序集中,并且只能访问指定的其他程序集中的脚本。
在这里插入图片描述

上图演示了如何将项目中的代码拆分为多个程序集。因为 Main 引用 Stuff 并且不进行反向引用,所以知道对 Main 中的代码进行的任何更改都不会影响 Stuff 中的代码。同样,因为 Library 不依赖于任何其他程序集,所以可以更轻松地在其他项目中重用 Library 中的代码。

本部分将讨论如何创建和设置Assembly Definition和Assembly Reference Asset以便为项目定义程序集。

定义程序集

要将项目代码组织成程序集,请为每个所需程序集创建一个文件夹,并将应属于每个程序集的脚本移动到相关文件夹中。然后创建Assembly Definition Asset以指定程序集属性。

Unity 会获取包含Assembly Definition Asset(Assembly Definition asset)的文件夹中的所有脚本,并使用该资源定义的名称和其他设置将它们编译为程序集。Unity 还包含同一程序集中的任何子文件夹中的脚本,除非子文件夹具有自己的Assembly Definition或Assembly Reference Asset。

要包含来自现有程序集中一个非子文件夹的脚本,请在该非子文件夹中创建一个Assembly Reference Asset(Assembly Reference asset),并将它设置为引用定义目标程序集的Assembly Definition Asset。例如,可以将来自项目中所有 Editor 文件夹的脚本合并到它们自己的程序集中,无论这些文件夹位于何处。

Unity 会按照由其依赖项确定的顺序编译程序集;无法指定进行编译的顺序

引用和依赖项(References and dependencies)

当一种类型(如类或结构)使用另一种类型时,第一种类型依赖于 第二种类型。当 Unity 编译脚本时,它还必须可以访问该脚本所依赖的任何类型或其他代码。同样,当已编译的代码运行时,它必须可以访问其依赖项的已编译版本。如果两种类型处于不同的程序集中,则包含依赖类型的程序集必须声明对包含它所依赖类型的程序集的引用

可以使用Assembly Definition的选项控制项目中使用的程序集之间的引用。Assembly Definition设置包括:

注意:使用Assembly Definition创建的程序集中的类不能使用预定义程序集中定义的类型。

默认引用

默认情况下,预定义程序集会引用所有其他程序集,包括使用Assembly Definition创建的程序集 (1) 以及作为插件添加到项目中的预编译程序集 (2)。此外,使用Assembly Definition Asset创建的程序集会自动引用所有预编译程序集 (3):
在这里插入图片描述

在默认设置中,预定义程序集中的类可以使用项目中任何其他Assembly Definition的所有类型。同样,使用Assembly Definition Asset创建的程序集可以使用在任何预编译(插件)程序集中定义的所有类型。

可以通过在Assembly Definition Asset的 Inspector 中关闭 Auto Referenced 来防止预定义程序集引用某个程序集。关闭自动引用意味着在更改程序集中的代码时不会重新编译预定义程序集,但也意味着预定义程序集无法直接使用此程序集中的代码。

同样,可以通过在插件资源的 Plugin Inspector 中关闭Auto Referenced属性来防止自动引用插件程序集。这会影响预定义程序集以及使用Assembly Definition创建的程序集。请参阅 Plugin Inspector 以了解更多信息。

关闭插件的 Auto Referenced 时,可以在 Inspector 中为Assembly Definition Asset显式引用它。请启用该资源的 Override References 选项,然后添加对插件的引用。

注意:无法声明预编译程序集的显式引用。预定义程序集只能使用自动引用的程序集中的代码。

循环引用

当一个Assembly 引用第二个程序集,而第二个程序集又引用第一个程序集时,便存在循环Assembly Reference。程序集之间的这类循环引用是不允许的,会报告为错误并显示消息“Assembly with cyclic references detected”。

通常,程序集之间的这类循环引用是由于程序集中定义的类中的循环引用而发生的。虽然同一程序集中的类之间的循环引用在技术上没有什么无效之处,但不允许不同程序集中的类之间进行循环引用。如果遇到循环引用错误,则必须重构代码以移除循环引用或将相互引用的类置于同一程序集中。

创建 Assembly Definition Asset

要创建Assembly Definition Asset,请执行以下操作:

  1. Project 窗口中,找到包含要包括在程序集中的脚本的文件夹。
  2. 在该文件夹中创建 Assembly Definition Asset(菜单:Assets > Create> Assembly Definition)。
  3. 为资源分配名称。默认情况下,程序集文件使用分配给资源的名称,不过可以在 Inspector 窗口中更改名称。

Unity 会重新编译项目中的脚本以创建新程序集。完成后,便可以为新Assembly Definition更改设置。

包含Assembly Definition的文件夹中的脚本(包括任何子文件夹中的脚本(除非这些文件夹包含其自己的Assembly Definition或Reference Asset))会编译到新程序集中并从它们以前的程序集中移除。

创建 Assembly Definition Reference Asset

要创建 Assembly Definition Reference Asset,请执行以下操作:

  1. Project 窗口中,找到包含要包括在引用程序集中的脚本的文件夹。

  2. 在该文件夹中创建Assembly Reference Asset(菜单:Assets> Create>Assembly Definition Reference)。

  3. 为资源分配名称。Unity 会重新编译项目中的脚本以创建新程序集。完成后,便可以为新 Assembly Definition 引用更改设置。

  4. 选择新的 Assembly Definition Reference Asset以在 Inspector 中查看其属性。
    在这里插入图片描述

  5. 设置 Assembly Definition 属性以引用目标 Assembly Definition Asset 。

  6. 单击 Apply

包含 Assembly Definition Reference Asset的文件夹中的脚本(包括任何子文件夹中的脚本(除非这些文件夹包含其自己的Assembly Definition或Reference Asset))会编译到引用程序集中并从它们以前的程序集中移除。

创建特定于平台的程序集

要为特定平台创建程序集,请执行以下操作:

  1. 创建 Assembly Definition Asset

  2. 选择新的Assembly DefinitionReference Asset以在 Inspector 中查看其属性。
    在这里插入图片描述

  3. 选中 Any Platform 选项并选择要排除的特定平台。或者,可以取消选中 Any Platform 并选择要包含的特定平台。

  4. 单击 Apply

为平台构建项目时,会根据选定平台包含(或排除)程序集。

为编辑器代码创建程序集

通过编辑器程序集可以将编辑器脚本置于项目中的任何位置,而不仅仅是置于名为 Editor 的顶层文件夹中。

要在项目中创建包含编辑器代码的程序集,请执行以下操作:

  1. 在包含编辑器脚本的文件夹中创建特定于平台的程序集
  2. 仅包含编辑器平台。
  3. 如果有包含编辑器脚本的其他文件夹,则在这些文件夹中创建Assembly Definition Reference Asset并将它们设置为引用此Assembly Definition。

创建测试程序集

通过测试程序集可以编写测试并使用 Unity TestRunner 运行它们,同时还使测试代码与应用程序附带的代码分开。Unity 提供 TestRunner 作为 Test Framework package 的一部分。请参阅 Test Framework documentation 以了解有关安装 Test Framework 包和创建测试程序集的说明。

引用另一个程序集

要使用属于另一个程序集一部分的 C# 类型和函数,必须在Assembly Definition Asset中创建对该程序集的引用。

要创建Assembly Reference,请执行以下操作:

  1. 选择需要引用的程序集的Assembly Definition以在 Inspector 中查看其属性。

  2. Assembly Definition References 部分中,单击 + 按钮以添加新引用。
    在这里插入图片描述

  3. 将Assembly Definition Asset分配给引用列表中新创建的字段。

通过启用 Use GUIDs 选项可以更改引用Assembly Definition Asset的文件名,而无需更新其他Assembly Definition中的引用以反射新名称。(请注意,如果删除了资源文件的元数据文件,或者将文件移到 Unity 编辑器之外,而没有同时随它们移动元数据文件,则必须重置 GUID。)

引用预编译的插件程序集

默认情况下,项目中使用 Assembly Definition 创建的所有程序集都会自动引用所有预编译程序集。这些自动引用意味着在更新任何一个预编译程序集时,Unity 都必须重新编译所有程序集,即使未使用程序集中的代码也是如此。要避免这种额外开销,可以覆盖自动引用并指定仅引用程序集实际使用的预编译库:

  1. 选择需要引用的程序集的Assembly Definition以在 Inspector 中查看其属性。
  2. General 部分中,启用 Override References 选项。
    在这里插入图片描述

选中 Override References 后,Inspector 的 Assembly References 部分会成为可用状态。

  1. Assembly References 部分中,单击 + 按钮以添加新引用。
  2. 使用空字段中的下拉列表分配对预编译程序集的引用。该列表会显示项目中适用于当前在项目 Build Settings 中设置的平台的所有预编译程序集。(可在 Plugin Inspector 中为预编译程序集设置平台兼容性。)
  3. 单击 Apply
  4. 为构建项目时针对的每个平台重复操作。

有条件地包含一个程序集

可以使用预处理器符号控制程序集是否进行了编译并包含在游戏或应用程序的构建中(包括编辑器中的运行模式)。可以在Assembly Definition选项中通过 Define Constraints 列表指定必须为要使用的Assembly Definition的符号:

  1. 选择程序集的Assembly Definition以在 Inspector 中查看其属性。

  2. Define Constraints 部分中,单击 + 按钮以将新符号添加到约束列表中。
    在这里插入图片描述

  3. 输入符号名称。
    可以通过在名称前放置感叹号来“否定”符号。例如,约束 !UNITY_WEBGL 会在未定义 UNITY_WEBGL 时包含程序集。

  4. 单击 Apply

可以使用以下符号作为约束:

  • Scripting Define Symbols 设置中定义的符号,可以在 Project SettingsPlayer 部分中找到这些符号。请注意,Scripting Define Symbols 适用于当前在项目 Build Settings 中设置的平台。要为多个平台定义一个符号,必须切换到每个平台并单独修改 Scripting Define Symbols 字段。
  • Unity 定义的符号。请参阅 Platform dependent compilation
  • 使用Assembly Definition Asset的 Version Defines 部分定义的符号。

在确定是否满足约束时,不会考虑脚本中定义的符号。

有关其他信息,请参阅 Define Constraints

根据 Unity 和项目包版本定义符号

如果您需要根据项目使用的特定 Unity 版本或包版本在程序集(assembly)中编译不同的代码,可以将条目添加到 Version Defines 列表中。该列表指定符号何时应被定义的规则。对于版本号,您可以指定一个逻辑表达式,该表达式可以计算为特定版本或版本范围。

要有条件地定义符号,请执行以下操作:

  1. 选择程序集定义(Assembly Definition)资产,以便在检查器中查看其属性。
  2. Version Defines 部分,单击 + 按钮以将条目添加到列表中。
  3. 设置属性:
    • Resource:选择必须安装 Unity 或包或模块,以便定义此符号。
    • Define:符号名称。
    • Expression:计算结果为特定版本或版本范围的表达式。有关规则,请参阅 版本定义表达式

Expression outcome 显示表达式计算出的版本。如果结果显示为 Invalid,则表示表达式语法不正确。

以下示例定义了符号 USE_TIMELINE_1_3,如果项目使用 Timeline 1.3;并定义符号 USE_NEW_APIS,如果项目在 Unity 2021.2.0a7 或更高版本中打开:
在这里插入图片描述

  1. 单击 Apply

在程序集定义中定义的符号仅在为该定义创建的程序集中的脚本范围内。

注意:您可以将使用 Version Defines 列表定义的符号作为 Define Constraints 使用。因此,您可以指定仅在项目中也安装了特定版本的给定包时才应使用某个程序集。

版本定义表达式

您可以使用表达式指定确切版本或版本范围。版本定义表达式使用数学范围表示法。

  • 方括号 [] 指定范围包含端点:

[1.3,3.4.1] 计算为 1.3.0 <= x <= 3.4.1

  • 圆括号 () 指定范围不包含端点:

(1.3.0,3.4) 计算为 1.3.0 < x < 3.4.0

  • 可以在单一表达式中混合两种范围类型:

[1.1,3.4) 计算为 1.1.0 <= x < 3.4.0

(0.2.4,5.6.2-preview.2] 计算为 0.2.4 < x <= 5.6.2-preview.2

  • 可以在方括号中使用单一版本指示符来指定确切版本:

[2.4.5] 计算为 x = 2.4.5

  • 作为快捷方式,可以输入不带范围括号的单一版本指示表达式以包含该版本或更高版本:

2.1.0-preview.7 计算为 x >= 2.1.0-preview.7

注意:表达式中不允许有空格。也不支持通配符字符。

Unity 版本号

当前版本的 Unity(以及支持 Assembly Definitions 的所有版本)使用三个部分的版本标识符:MAJOR.MINOR.REVISION,例如 2017.4.25f12018.4.29f12019.4.7f1

  • MAJOR 版本是目标发布年份,例如 2017 或 2021。
  • MINOR 版本是目标发布季度,例如 1、2、3 或 4。
  • REVISION 标识符有三个部分,格式为 RRzNN,其中:
    • RR 是一位或两位数的修订号。
    • z 是表示发布类型的字母:
      • a = alpha 版本
      • b = beta 版本
      • f = 正常公开发布
      • c = 中国发布版本(相当于 f
      • p = 补丁发布
      • x = 实验发布
    • NN 是一位或两位数的增量号

发布类型标识符的比较如下:

a < b < f = c < p < x

换句话说,alpha 版本早于 beta 版本,beta 版本早于正常(f)或中国(c)发布。补丁发布总是晚于具有相同修订号的正常或中国发布,而实验发布晚于任何其他发布类型。注意,实验发布不使用末尾的增量号。

Unity 版本号在 REVISION 组件之后可以有后缀,例如 2019.3.0f11-Sunflower。在比较版本时会忽略任何后缀。

例如,以下表达式包含任何 2017 或 2018 版本的 Unity,但不包括 2019 或更高版本:

[2017,2019)

包和模块版本号

包和模块版本标识符有四个部分,遵循语义版本控制格式:MAJOR.MINOR.PATCH-LABEL。前三个部分始终为数字,但标签是字符串。Unity 预览中的包使用字符串 previewpreview.n,其中 n > 0。有关包版本号的更多信息,请参阅包版本控制

例如,以下表达式包含 MAJOR.MINOR 版本在 3.2 和 6.1 之间(包括两者)的所有版本的包:

[3.2,6.1]

查找脚本所属的程序集

要确定一个 C# 脚本编译到哪个程序集中,请执行以下操作:

  1. 在 Unity Project 窗口中选择 C# 脚本文件以在 Inspector 窗口中查看其属性。
  2. 程序集文件名和程序集定义(如果存在)会显示在 InspectorAssembly Information 部分中。
    在这里插入图片描述

在此示例中,选定脚本会编译到库文件 Unity.Timeline.Editor.dll 中,该文件由 Unity.Timeline.Editor 程序集定义资源进行定义。

特殊文件夹

Unity 对具有某些特殊名称的文件夹中的脚本进行处理的方式不同于其他文件夹中的脚本。但是,在这些文件夹之一中或上级的一个文件夹中创建程序集定义资源时,该文件夹将失去特殊处理。在使用 Editor 文件夹时可能会注意到该变化,这些文件夹可能分散在整个项目中(取决于代码的组织方式以及所用的 Asset Store 包)。

Unity 通常都会将名为 Editor 的文件夹中的所有脚本编译到预定义的 Assembly-CSharp-Editor 程序集中(无论这些脚本位于何处)。但是,如果在下级有一个 Editor 文件夹的文件夹中创建程序集定义资源,则 Unity 不再将这些编辑器脚本放入预定义编辑器程序集中。实际上,这些脚本会进入通过程序集定义创建的新程序集中(脚本可能不属于该程序集)。要管理 Editor 文件夹,可以在每个 Editor 文件夹中创建程序集定义或引用资源,以将这些脚本放置在一个或多个编辑器程序集中。请参阅 为编辑器代码创建程序集

设置程序集属性

可以使用程序集属性 (Assembly Attribute) 为程序集设置元数据属性 (Metadata Property)。按照惯例,程序集属性语句通常置于名为 AssemblyInfo.cs 的文件中。

例如,以下程序集属性指定了一些 .NET assembly metadata values、一个 InternalsVisibleTo 属性(对于测试可能十分有用)以及 Unity 定义的 Preserve attribute(影响构建项目时如何从程序集中移除未使用的代码):

[assembly: System.Reflection.AssemblyCompany("Bee Corp.")][assembly: System.Reflection.AssemblyTitle("Bee's Assembly")][assembly: System.Reflection.AssemblyCopyright("Copyright 2020.")][assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTestAssembly")][assembly: UnityEngine.Scripting.Preserve]

在 build 脚本中获取程序集信息

使用 UnityEditor.Compilation 命名空间中的 CompilationPipeline 类可检索 Unity 为项目构建的所有程序集(包括基于程序集定义资源创建的程序集)的相关信息。

例如,以下脚本使用 CompilationPipeline 类列出项目中的所有当前 Player 程序集:

using UnityEditor;using UnityEditor.Compilation;public static class AssemblyLister{    [MenuItem("Tools/List Player Assemblies in Console")]    public static void PrintAssemblyNames()    {        UnityEngine.Debug.Log("== Player Assemblies ==");        Assembly[] playerAssemblies =            CompilationPipeline.GetAssemblies(AssembliesType.Player);        foreach (var assembly in playerAssemblies)        {            UnityEngine.Debug.Log(assembly.name);        }    }}

程序集定义属性

点击一个程序集定义资产(Assembly Definition Asset),可以在检视器窗口中设置程序集的属性。

程序集定义属性分为以下几个部分:

  • 名称和常规
  • 定义约束
  • 程序集定义引用
  • 程序集引用
  • 平台
  • 版本定义
名称和常规

在这里插入图片描述

属性描述
名称程序集的名称(不带文件扩展名)。程序集名称在整个项目中必须唯一。考虑使用反向DNS命名风格来减少名称冲突的可能性,特别是在多个项目中使用该程序集时。注意:Unity使用分配给程序集定义资产的名称作为名称字段的默认值,但您可以根据需要更改名称。不过,如果您通过名称而不是GUID引用程序集定义,更改名称会导致引用失效。
允许“不安全”代码如果在程序集中的脚本中使用了C#的unsafe关键字,请启用允许“不安全”代码选项。启用此设置后,Unity在编译程序集时会将/unsafe选项传递给C#编译器。
自动引用指定预定义程序集是否应引用此项目程序集。禁用自动引用选项后,Unity在编译过程中不会自动引用该程序集,但这不会影响Unity是否在构建中包含它
无引擎引用启用此属性后,Unity在编译程序集时不会添加对UnityEditor或UnityEngine的引用。
覆盖引用启用覆盖引用设置以手动指定该程序集所依赖的预编译程序集。启用覆盖引用后,检视器会显示程序集引用部分,您可以使用它来指定引用。预编译程序集是一个在Unity项目之外编译的库。默认情况下,您在项目中定义的程序集会引用您添加到项目中的所有预编译程序集,这与预定义程序集引用所有预编译程序集的方式相同。启用覆盖引用后,此程序集仅引用您在程序集引用下添加的预编译程序集。注意:要防止项目程序集自动引用预编译程序集,可以禁用其自动引用选项。有关更多信息,请参阅插件检视器。
根命名空间(Root Namespace)该程序集定义中的脚本的默认命名空间。如果您使用Rider或Visual Studio作为代码编辑器,它们会自动将此命名空间添加到您在该程序集定义中创建的任何新脚本中。有关更多信息,请参阅创建程序集定义资产
定义约束

定义约束指定编译器的#define指令必须定义或未定义,以便Unity编译或引用程序集。
在这里插入图片描述

Unity仅在满足所有定义约束时才编译和引用项目程序集。约束的工作方式类似于C#中的#if预处理指令,但在程序集级别而不是脚本级别。必须在定义约束设置中定义所有符号,才能满足约束。

要指定必须未定义的符号,请在其前面加上否定的!(叹号)符号。例如,如果将以下符号指定为定义约束:

!ENABLE_IL2CPP
UNITY_2018_3_OR_NEWER

当符号 ENABLE_IL2CPP 未定义且符号 UNITY_2018_3_OR_NEWER 定义时,约束得到满足。结果是,Unity仅在非IL2CPP脚本运行时为Unity 2018.3或更新版本时才编译和引用此程序集。

可以使用 ||(OR)运算符指定至少一个约束必须存在才能满足约束。例如:

UNITY_IOS || UNITY_EDITOR_OSX
UNITY_2019_3_OR_NEWER
!UNITY_ANDROID

当定义了UNITY_IOS或UNITY_EDITOR_OSX中的任意一个,且定义了UNITY_2019_3_OR_NEWER且未定义UNITY_ANDROID时,约束得到满足。各行相当于对其中的约束进行逻辑与操作。上例等价于:

(UNITY_IOS OR UNITY_EDITOR_OSX) AND (UNITY_2019_3_OR_NEWER) AND (NOT UNITY_ANDROID)

可以使用任何Unity内置的#define指令、全局编译器响应(.rsp)文件中定义的符号以及项目的脚本定义符号播放器设置中定义的任何符号。有关更多信息,请参阅平台依赖编译,包括内置符号列表。

注意:脚本定义符号设置是平台特定的。如果使用此设置定义Unity是否应使用程序集,请确保在所有相关平台上定义必要的符号。

有关更多信息,请参阅有条件包含程序集

无效或不兼容的约束

Unity根据当前定义的设置标记每个约束(例如,以下一组三个约束表明第一个符号当前定义而另外两个未定义)。由于必须满足每个单独约束才能满足整体约束,因此编辑器将整个定义约束部分标记为当前不兼容或无效。

可以通过更改脚本后端为IL2CPP(在播放器设置中)来满足约束,并删除第三个约束中的无效字符。然而,重要的是在构建项目时如何评估约束,而不是在Unity编辑器中如何显示约束(例如,可能有一个程序集仅希望在使用IL2CPP后端的构建中包含,而不在使用Mono后端的其他构建中包含)。

程序集定义引用(Assembly Definition References)

在这里插入图片描述

属性描述
程序集定义引用指定对使用程序集定义资产创建的其他程序集的引用。Unity使用这些引用来编译程序集并定义程序集之间的依赖关系。
使用GUID此设置控制Unity如何序列化对其他程序集定义资产的引用。启用此属性后,Unity将引用保存为资产的GUID,而不是程序集定义名称。建议使用GUID而不是名称,因为这意味着可以更改程序集定义资产的名称,而无需更新引用它的其他程序集定义文件。
有关更多信息,请参阅创建程序集定义资产

程序集引用(Assembly References)

在这里插入图片描述

仅当启用了覆盖引用属性(在常规部分)时,才会显示程序集引用部分。使用此区域指定该程序集所依赖的任何预编译程序集的引用。

有关更多信息,请参阅引用预编译的插件程序集

平台

设置程序集的平台兼容性。Unity仅在包含的平台(或未排除的平台)上编译或引用此程序集。

有关更多信息,请参阅创建平台特定程序集

版本定义

根据项目中的包和模块的版本指定要定义的符号。

属性描述
资源包或模块
定义当此Unity项目中存在适用版本的资源时要定义的符号。
表达式定义版本或版本范围的表达式。有关规则,请参阅版本定义表达式
表达式结果表达式作为逻辑语句进行评估,其中“x”是检查的版本。如果表达式结果显示为无效,则表示表达式格式错误。
有关更多信息,请参阅根据项目包定义符号

程序集定义文件格式(Assembly Definition File Format)

程序集定义和程序集定义引用资产是 JSON 文件。你可以在 Unity 编辑器中使用 Inspector 窗口编辑资产文件,也可以使用外部工具修改 JSON 内容。

程序集定义 JSON

一个程序集定义是一个包含以下字段的 JSON 对象:

"allowUnsafeCode" : true
  • autoReferenced (bool): 可选。默认为 true。详见自动引用
"autoReferenced": false
  • defineConstraints (string[]): 可选。作为约束的符号,可以为空。详见定义约束
"defineConstraints": [
    "UNITY_2019",
    "UNITY_INCLUDE_TESTS"
]
  • excludePlatforms (string[]): 可选。要排除的平台名称字符串或一个空数组。如果 includePlatforms 包含值,则 excludePlatforms 数组必须为空。你可以使用 CompilationPipeline.GetAssemblyDefinitionPlatforms 函数检索平台名称字符串(调用此函数时必须为当前编辑器安装支持的平台)。详见平台
  "includePlatforms": [],
  "excludePlatforms": [
      "iOS",
      "macOSStandalone",
      "tvOS"
  ]
  • includePlatforms (string[]): 可选。要包含的平台名称字符串或一个空数组。如果 excludePlatforms 包含值,则 includePlatforms 数组必须为空。你可以使用 CompilationPipeline.GetAssemblyDefinitionPlatforms 函数检索平台名称字符串(调用此函数时必须为当前编辑器安装支持的平台)。详见平台
  "includePlatforms": [
      "Android",
      "LinuxStandalone64",
      "WebGL"
  ],
  "excludePlatforms": []
  • name (string): 必需。任何合法的程序集名称。
"name" : "MyAssemblyName" 
  • noEngineReferences (bool): 可选。默认为 false。详见无引擎引用
  "noEngineReferences": false
  • optionalUnityReferences (string[]): 可选。在早期版本的 Unity 中,此字段用于序列化 Unity 引用: 测试程序集选项,用于将程序集指定为测试程序集。从 Unity 2019.3 开始,不再显示该选项。该字段仍然受支持,但如果在较新版本的 Unity 编辑器中重新序列化资产,则该字段会被等效的程序集引用取代。

详见创建测试程序集

"optionalUnityReferences": [
    "TestAssemblies"
]
  • overrideReferences (bool): 可选。如果 precompiledReferences 包含值,则设置为 true。默认为 false。

详见覆盖引用

  "overrideReferences": true
  • precompiledReferences (string[]): 可选。引用的 DLL 库的文件名,包括扩展名,但不包含其他路径元素。可以为空。除非将 overrideReferences 设置为 true,否则此数组将被忽略。

详见程序集引用

  "overrideReferences": true,
  "precompiledReferences": [
      "Newtonsoft.Json.dll",
      "nunit.framework.dll"
  ]
  • references (string[]): 可选。引用使用程序集定义资产创建的其他程序集。你可以使用程序集定义资产文件的 GUID 或程序集名称(由程序集定义的 name 字段定义)。在列表中必须使用相同形式的所有引用。可以为空。

你可以使用 AssetDatabase.AssetPathToGUID 函数检索资产的 GUID。(每个资产关联的元数据中也包含 GUID。)

请注意,编辑器在程序集定义 Inspector 中显示一个“使用 GUID”选项。该选项不会在关联的 JSON 文件中序列化。相反,选择是从文件中找到的引用形式推断出来的。

详见引用另一个程序集

使用 GUID:

  "references": [
      "GUID:17b36165d09634a48bf5a0e4bb27f4bd",
      "GUID:b470eee7144904e59a1064b70fa1b086",
      "GUID:2bafac87e7f4b9b418d9448d219b01ab",
      "GUID:27619889b8ba8c24980f49ee34dbb44a",
      "GUID:0acc523941302664db1f4e527237feb3"
  ]

使用程序集名称:

  "references": [
      "Unity.CollabProxy.Editor",
      "AssemblyB",
      "UnityEngine.UI",
      "UnityEngine.TestRunner",
      "UnityEditor.TestRunner"
  ]
  • versionDefines (object[]): 可选。包含每个版本定义的对象。此对象有三个字段:
    • name:string – 资源名称
    • expression:string – 定义资源版本或版本范围的表达式
    • define:string – 要定义的符号
      详见版本定义
  "versionDefines": [
      {
          "name": "com.unity.ide.vscode",
          "expression": "[1.7,2.4.1]",
          "define": "MY_SYMBOL"
      },
      {
          "name": "com.unity.test-framework",
          "expression": "[2.7.2-preview.8]",
          "define": "TESTS"
      }
  ]
程序集定义 JSON 字符串示例

使用程序集名称引用其他程序集定义和 includePlatforms

{
    "name": "BeeAssembly",
    "references": [
        "Unity.CollabProxy.Editor",
        "AssemblyB",
        "UnityEngine.UI",
        "UnityEngine.TestRunner",
        "UnityEditor.TestRunner"
    ],
    "includePlatforms": [
        "Android",
        "LinuxStandalone64",
        "WebGL"
    ],
    "excludePlatforms": [],
    "overrideReferences": true,
    "precompiledReferences": [
        "Newtonsoft.Json.dll",
        "nunit.framework.dll"
    ],
    "autoReferenced": false,
    "defineConstraints": [
        "UNITY_2019",
        "UNITY_INCLUDE_TESTS"
    ],
    "versionDefines": [
        {
            "name": "com.unity.ide.vscode",
            "expression": "[1.7,2.4.1]",
            "define": "MY_SYMBOL"
        },
        {
            "name": "com.unity.test-framework",
            "expression": "[2.7.2-preview.8]",
            "define": "TESTS"
        }
    ],
    "noEngineReferences": false
}

使用 GUID 引用其他程序集定义和 excludePlatforms

{
    "name": "BeeAssembly",
    "references": [
        "GUID:17b36165d09634a48bf5a0e4bb27f4bd",
        "GUID:b470eee7144904e59a1064b70fa1b086",
        "GUID:2bafac87e7f4b9b418d9448d219b01ab",
        "GUID:27619889b8ba8c24980f49ee34dbb44a",
        "GUID:0acc523941302664db1f4e527237feb3"
    ],
    "includePlatforms": [],
    "excludePlatforms": [
        "iOS",
        "macOSStandalone",
        "tvOS"
    ],
    "allowUnsafeCode": false,
    "overrideReferences": true,
    "precompiledReferences": [
        "Newtonsoft.Json.dll",
        "nunit.framework.dll"
    ],
    "autoReferenced": false,
    "defineConstraints": [
        "UNITY_2019",
        "UNITY_INCLUDE_TESTS"
    ],
    "versionDefines": [
        {
            "name": "com.unity.ide.vscode",
            "expression": "[1.7,2.4.1]",
            "define":

 "MY_SYMBOL"
        },
        {
            "name": "com.unity.test-framework",
            "expression": "[2.7.2-preview.8]",
            "define": "TESTS"
        }
    ],
    "noEngineReferences": false
}
程序集定义引用 JSON

一个程序集定义引用是一个包含以下字段的 JSON 对象:

你可以使用程序集名称或资产的 GUID 来引用程序集定义资产。你可以使用 AssetDatabase.AssetPathToGUID 函数检索资产的 GUID。(每个资产关联的元数据中也包含 GUID。)

使用程序集名称:

  {
      "reference": "AssemblyA"
  }

使用程序集定义资产 GUID:

  {
      "reference": "GUID:f4de40948f4904ecb94b59dd38aab8a1"
  }

详见创建程序集定义引用资产

Unity 中的 .NET

引用附加的类库程序集

如果您的 Unity 项目使用了 .NET 类库 API 中 Unity 默认未编译的部分,您可以在编译过程中为 C# 编译器提供附加程序集列表。行为取决于项目使用的 .NET 配置文件。更多信息,请参见 .NET 配置文件支持

.NET Standard 配置文件

如果您的项目使用 .NET Standard 配置文件,默认情况下会引用 .NET 类库 API 的所有部分。您不能引用附加的程序集。如果某些 API 部分缺失,可能是因为它不包含在 .NET Standard 中。可以尝试使用 .NET Framework 配置文件。为了避免在更改配置文件时出现编译问题,请参见 在配置文件之间切换

.NET Framework 配置文件

默认情况下,当使用 .NET Framework 配置文件时,Unity 会引用以下程序集:

  • mscorlib.dll
  • System.dll
  • System.Core.dll
  • System.Runtime.Serialization.dll
  • System.Xml.dll
  • System.Xml.Linq.dll

要引用其他类库程序集,请使用 csc.rsp 文件:包含可以传递给 C# 编译器的命令行参数列表的响应文件。使用 csc.rsp 文件,请按照以下说明进行操作:

  1. 在 Unity 项目的 Assets 文件夹中创建一个名为 csc.rsp 的文件。
  2. 将要引用的任何程序集文件移到项目的 Assets 文件夹中(如果它们不在此文件夹中)。
  3. 将要引用的程序集的命令行参数填入 csc.rsp 文件中。

例如,如果您的项目使用 HttpClient 类,该类定义在 System.Net.Http.dll 程序集中,如果该程序集不存在,C# 编译器可能会产生如下初始错误消息:

The type `HttpClient` is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.

要解决此错误,将以下命令行参数添加到项目的 csc.rsp 文件中:

-r:System.Net.Http.dll

为每个要引用的程序集添加一行适当的命令行参数。

在配置文件之间切换

当您使用 csc.rsp 文件引用类库程序集并更改 .NET 配置文件时,可能会遇到编译问题。

如果您将 .NET 配置文件从 .NET Framework 更改为 .NET Standard,并且 csc.rsp 文件引用的程序集在 .NET Standard 配置文件中不存在,则编译将失败。要解决此问题,在更改 .NET 配置文件为 .NET Standard 之前,编辑 csc.rsp 文件,删除对 .NET Framework 配置文件独有的程序集的引用。

脚本后端

Mono 概述

Mono 脚本后端

Mono 脚本后端在运行时编译代码,这种技术称为即时编译(JIT)。Unity 使用开源 Mono 项目的一个分支。

一些平台不支持 JIT 编译,因此 Mono 后端无法在所有平台上运行。其他平台支持 JIT 和 Mono 但不支持提前编译(AOT),因此也无法支持 IL2CPP 后端。当一个平台可以支持这两种后端时,Mono 是默认选项。更多信息,请参见脚本限制

Mono 支持托管代码的调试。更多信息,请参见在 Unity 中调试 C# 代码

脚本限制

Unity 在其支持的所有平台上提供了通用的脚本 API 和体验。然而,一些平台有固有的限制。为了帮助您理解这些限制,以下表格描述了每个平台和脚本后端适用的限制:

平台(脚本后端)提前编译(AOT)支持线程
Android(IL2CPP)
Android(Mono)
iOS(IL2CPP)
独立平台(IL2CPP)
独立平台(Mono)
通用 Windows 平台(IL2CPP)
WebGL(IL2CPP)
更多信息,请参见脚本限制

托管代码剥离(Managed code stripping)

在构建过程中,Unity 通过一个称为托管代码剥离的过程移除未使用或不可访问的代码,这可以显著减小应用程序的最终大小。托管代码剥离从托管程序集(包括项目中的 C# 脚本生成的程序集、包和插件中的程序集,以及 .NET Framework 中的程序集)中移除代码。

Unity 使用一个名为 Unity 链接器的工具来对项目程序集中的代码进行静态分析。静态分析识别在执行期间无法到达的任何类、类的部分、函数或函数的部分。此分析仅包括构建时存在的代码,因为运行时生成的代码在 Unity 执行静态分析时并不存在。

您可以使用托管剥离级别设置配置 Unity 对项目执行的代码剥离级别。要防止 Unity 移除代码的特定部分,请使用注释来指示 Unity 链接器应保留代码库的哪些部分。有关详细信息,请参阅 Unity linker

配置托管代码剥离

托管剥离级别属性

托管剥离级别属性确定 Unity 链接器在分析和剥离应用程序代码时遵循的规则集。当您将设置从最小增大到高时,这些规则会使链接器在更多程序集内搜索不可访问的代码。Unity 链接器在更高设置下会移除更多代码,从而减少最终的构建大小,尽管扩展搜索意味着每次构建所需时间更长。

要更改托管剥离级别属性:

  1. 转到 Edit > Project Settings > Player
  2. Other Settings 中,导航到 Optimization 标题。
  3. 将托管剥离级别(Managed Stripping Level)属性设置为所需的值。

通过适当配置托管代码剥离,您可以更好地平衡应用程序的尺寸和性能,同时确保不会意外剥离关键代码。

使用注释保护代码

您可以使用注释来防止 Unity 链接器剥离代码的特定部分。如果您的应用程序生成运行时代码而这些代码在 Unity 执行静态分析时并不存在(例如,通过反射生成的代码),这将非常有用。注释可以为 Unity 链接器提供有关哪些代码模式不应剥离的一般指导,或指示不要剥离特定的代码段。

注释代码以保护其免受托管代码剥离过程的影响

有两种主要方法可以注释代码以保护其免受托管代码剥离过程的影响:

  1. 根注释(Root annotations):将代码的某些部分标记为根。Unity 链接器不会剥离标记为根的任何代码。根注释使用起来较为简单,但也可能导致 Unity 链接器保留一些本应剥离的代码。
  2. 依赖注释(Dependency annotations):定义代码元素之间的连接。与根注释相比,依赖注释可以减少代码的过度保留。

这些技术中的每一种都在更高的剥离级别上提供了更多对 Unity 链接器剥离代码量的控制,并降低了关键代码被剥离的可能性。注释在代码通过反射引用其他代码时尤其有用,因为 Unity 链接器并不总能检测到反射的使用。

通过反射或在运行时生成其他代码来保护代码,可以显著降低应用程序执行时发生意外行为的可能性。有关 Unity 链接器可以识别的反射模式示例,请参阅 Unity 中间语言链接器反射测试套件

根注释(Root Annotations)

根注释强制 Unity 链接器将代码元素视为根,不会在代码剥离过程中剥离它们。根据需要保留单个类型及其构造函数或程序集,有两种类型的根注释可供使用:

  • Preserve Attribute::将单个类型注释为根以保留它们。
  • Link.xml:将程序集及其内的任何类型或其他代码实体注释为根以保留它们。
使用 Preserve 属性注释根

使用 Preserve 属性可以单独排除代码的特定部分,避免被 Unity 链接器的静态分析剥离。要用此属性注释代码,请在要保留的代码的首个部分之前添加 [Preserve]。以下列表描述了 Unity 链接器在注释了不同代码元素时保留的实体:

  • 程序集(Assembly):保留在程序集内使用和定义的所有类型。要将 Preserve 属性分配给程序集,请在程序集内的任何 C# 文件中,在任何命名空间声明之前放置属性声明。
  • 类型(Type):保留类或类型及其默认构造函数。
  • 方法(Method):保留方法、声明该方法的类型、方法返回的类型以及其所有参数的类型。
  • 属性(Property):保留属性、声明该属性的类型、属性的值类型以及获取和设置属性值的方法。
  • 字段(Field):保留字段、字段类型以及声明该字段的类型。
  • 事件(Event):保留事件、声明事件的类型、事件返回的类型、add 访问器和 remove 访问器。
  • 委托(Delegate):保留委托类型以及委托调用的所有方法。

当您希望同时保留类型及其默认构造函数时,请使用 [Preserve] 属性。如果您只想保留其中之一,请使用 link.xml 文件。

您可以在任何程序集和任何命名空间中定义 [Preserve] 属性。您可以使用 UnityEngine.Scripting.PreserveAttribute 类,创建 UnityEngine.Scripting.PreserveAttribute 的子类,或创建自己的 PreserveAttribute 类。例如:

class Foo
{
    [Preserve]
    public void PreservedMethod(){}
}
使用 link.xml 文件

link.xml 文件用于注释程序集及其内的任何类型或其他代码实体,以将它们保留为根。以下是一个简单的 link.xml 示例:

<linker>
  <assembly fullname="Assembly-CSharp">
    <type fullname="Namespace.Foo" preserve="all"/>
  </assembly>
</linker>

此示例将 Namespace.Foo 类型及其所有成员保留在 Assembly-CSharp 程序集中。

依赖注释(Dependency Annotations)

依赖注释定义代码元素之间的连接,从而减少代码的过度保留。与根注释相比,这种方法可以更精细地控制代码剥离。

例如,您可以使用 [PreserveDependency] 属性来定义代码之间的依赖关系。这样可以确保在剥离过程中保留这些依赖项。

using UnityEngine.Scripting;

class Bar
{
    [PreserveDependency("MethodName", "Namespace.Foo")]
    void SomeMethod() {}
}

在此示例中,MethodNameNamespace.Foo 类型中的方法,它在 Bar 类的 SomeMethod 方法中被标记为依赖项。

Unity Linker

Unity 构建过程中使用一个名为 Unity linker 的工具来剥离托管代码。Unity linker 是 IL Linker 的一个版本,经过定制以适应 Unity。Unity linker 中与 Unity 引擎相关的特定部分并未公开。

Unity linker 负责托管代码剥离以及引擎代码剥离(通过 IL2CPP 脚本后端提供),删除未使用的引擎代码。有关更多信息,请参阅 PlayerSettings.StripEngineCode

Unity linker 的工作原理

Unity linker 分析项目中的所有程序集。首先,它标记根类型、方法、属性和字段。例如,在场景中的 GameObjects 上添加的 MonoBehaviour 派生类是根类型。Unity linker 然后分析已标记的根,识别并标记这些根依赖的托管代码(managed code)。完成静态分析后,任何剩余的未标记代码都无法通过应用程序代码的任何执行路径访问,Unity linker 会将其从程序集删除。

Unity linker 剥离程序集的方式

Unity 编辑器创建一个包含项目中任何场景使用的类型的程序集列表,并将此列表传递给 Unity linker。Unity linker 然后处理这些程序集、这些程序集的任何引用、link.xml 文件中声明的任何程序集以及带有 AlwaysLinkAssembly 属性的任何程序集。通常,Unity linker 不会处理项目中包含但不属于这些类别之一的程序集,并将它们排除在 Player 构建之外。

对于 Unity linker 处理的每个程序集,它会根据程序集的分类、程序集是否包含场景中使用的类型以及为构建选择的托管剥离级别,遵循一组规则。

这些规则将程序集分为以下分类:

  • .NET 类库程序集(.NET Class Library assemblies) —— 包括 Mono 类库(如 mscorlib.dll 和 System.dll)以及 .NET 类库 facade 程序集(如 netstandard.dll)。
  • 平台 SDK 程序集 —— 包括特定于平台 SDK 的托管程序集。例如,Universal Windows Platform SDK 中的 windows.winmd 程序集。
  • Unity 引擎模块程序集 —— 包括构成 Unity 引擎的托管程序集,如 UnityEngine.Core.dll。
  • 项目程序集 —— 包括特定于项目的程序集,如:
    • 脚本程序集(如 Assembly-CSharp.dll)
    • 预编译程序集(Precompiled assemblies)
    • 程序集定义程序集(Assembly Definition Assemblies
    • 包程序集(Package assemblies)

标记规则

当您在 Unity 中构建项目时,构建过程会将您的 C# 代码编译为称为**公共中间语言(CIL)**的 .NET 字节码格式。Unity 将此 CIL 字节码打包到称为程序集的文件中。您在项目中使用的 .NET 框架库和任何 C# 库也预先打包为 CIL 字节码的程序集。

当 Unity linker 执行静态分析时,它遵循一系列规则来确定哪些 CIL 字节码部分需要为构建保留。根标记规则确定 Unity linker 如何识别并保留构建中的顶级程序集。依赖项标记规则确定 Unity linker 如何识别并保留根程序集依赖的任何代码。

托管剥离级别属性更改 Unity linker 使用的规则集。以下部分描述了托管剥离级别属性的每个可能设置的标记规则。

根标记规则

以下表格描述了 Unity linker 如何识别程序集中的顶级类型:

程序集类型标记规则(Mininal)
.NET 类库 & 平台 SDK 和 UnityEngine 程序集应用预防性保留(precautionary preservations)和任何在 link.xml 文件中定义的保留。
场景中引用类型的程序集标记程序集中的所有类型和成员。
其他标记程序集中的所有类型和成员。
测试标记带有 [UnityTest] 属性的任何方法和用 NUnit.Framework 中定义的属性注释的任何方法。
依赖项标记规则

在 Unity linker 识别程序集中的根之后,它需要识别这些根依赖的任何代码。以下表格描述了 Unity linker 如何识别程序集中的根类型依赖项:

规则目标各剥离级别的操作(Mininal)
MonoBehaviourUnity linker 标记类型时标记 MonoBehaviour 类型的所有成员。
ScriptableObjectUnity linker 标记类型时标记 ScriptableObject 类型的所有成员。
属性当 Unity linker 标记程序集、类型或其他代码结构时,它还会标记这些结构的所有属性。
调试属性(Debugging Attributes)启用脚本调试时,Unity linker 标记所有带有 [DebuggerDisplay] 属性的成员,即使没有使用这些成员的代码路径。
.NET 外观类库删除外观程序集,因为它们在运行时不是必需的。

link.xml 特性标签排除项(Link XML feature tag exclusions)

link.xml 文件支持一个不常用的features XML 属性。在示例中,mscorlib.xml 文件嵌入在 mscorlib.dll 中,但在适当的时候可以在任何 link.xml 文件中使用它。

当使用高剥离级别时,Unity linker 会根据当前构建的设置排除不支持的特性保留:

  • remoting —— 在针对 IL2CPP 脚本后端进行构建时排除。
  • sre —— 在针对 IL2CPP 脚本后端进行构建时排除。
  • com —— 在不支持 COM 的平台上进行构建时排除。

例如,以下 link.xml 文件在支持 COM 的平台上保留某个类型的方法,并在所有平台上保留另一个方法:

<linker>
    <assembly fullname="Foo">
        <type fullname="Type1">
            <!-- 在支持 COM 的平台上保留 FeatureOne -->
            <method signature="System.Void FeatureOne()" feature="com"/>
            <!-- 在所有平台上保留 FeatureTwo -->
            <method signature="System.Void FeatureTwo()"/>
        </type>
    </assembly>
</linker>

方法体编辑(Editing of method bodies)

使用高剥离级别时,Unity linker 会编辑方法体以进一步减少代码大小。此部分总结了 Unity linker 对方法体所做的一些显著编辑。

Unity linker 仅编辑 .NET 类库程序集中的方法体。方法体编辑后,程序集的源代码不再与程序集中的已编译代码匹配,这可能使调试更加困难

以下列表描述了 Unity linker 可以执行的一些方法体编辑操作:

  • 删除不可达分支 - Unity linker 删除检查 System.Environment.OSVersion.Platform 且对当前目标平台不可达的 If 语句块。
  • 内联仅访问字段的方法 - Unity linker 将获取或设置字段的方法调用替换为直接访问字段。这通常使剥离方法成为可能。在使用 Mono 后端时,Unity linker 仅在方法调用者允许直接访问字段(根据字段的可见性)的情况下进行此更改。对于 IL2CPP,可见性规则不适用,因此 Unity linker 在适当的情况下进行此更改。
  • 内联返回常量值的方法 - Unity linker 内联仅返回常量值的方法调用。
  • 删除空的非返回调用 - Unity linker 删除空的并具有 void 返回类型的方法调用。
  • 删除空作用域 - Unity linker 删除 Finally 块为空的 Try/Finally 块。删除空调用可以创建空的 Finally 块。当在方法编辑期间发生这种情况时,Unity linker 会删除整个 Try/Finally 块。一个可能发生这种情况的场景是编译器生成 Try/Finally 块作为 foreach 循环的一部分,以调用 Dispose()

脚本序列化(Script serialization)

序列化是一个自动过程,将数据结构或 GameObject 的状态转换成Unity可以存储并在以后重建的格式。

你如何组织Unity项目中的数据会影响Unity如何序列化这些数据,这对项目性能有显著影响。本文概述了Unity中的序列化以及如何优化项目的序列化。

序列化规则

Unity中的序列化器专为高效运行时操作设计。因此,Unity中的序列化行为与其他编程环境中的序列化有所不同。Unity中的序列化器直接处理C#类的字段而非属性,因此你的字段必须符合某些规则才能被序列化。以下部分概述了如何在Unity中使用字段序列化。

要使用字段序列化,你必须确保字段

  • 是公共的,或具有SerializeField属性
  • 不是静态的
  • 不是常量
  • 不是只读的
  • 具有可序列化的字段类型:
    • 原始数据类型(如 int, float, double, bool, string 等)
    • 枚举类型(32位或更小)
    • 固定大小的缓冲区
    • Unity内置类型,例如Vector2, Vector3, Rect, Matrix4x4, Color, AnimationCurve
    • 带有Serializable属性的自定义结构体
    • 派生自UnityEngine.Object的对象引用
    • 带有Serializable属性的自定义类(参见自定义类的序列化)
    • 上述字段类型的数组
    • 上述字段类型的List<T>

注意:Unity不支持多级类型(多维数组、锯齿数组、字典和嵌套容器类型)的序列化。如果你想序列化这些类型,有两个选择:

  • 将嵌套类型包装在类或结构体中。
  • 通过实现ISerializationCallbackReceiver接口使用序列化回调执行自定义序列化。

自定义类的序列化

为了让 Unity 序列化自定义类,你必须确保该类:

  • 具有Serializable属性
  • 不是静态的

当你将一个派生自UnityEngine.Object的类的实例分配给一个字段并保存该字段时,Unity会将该字段序列化为对该实例的引用。Unity会独立序列化实例,因此当多个字段分配给该实例时,不会重复序列化。但是,对于未派生自UnityEngine.Object的自定义类,Unity会将实例的状态直接包含在引用它们的MonoBehaviourScriptableObject的序列化数据中。这可以通过两种方式实现:内联和使用[SerializeReference]

  • 内联序列化:默认情况下,当你未在引用该类的字段上指定[SerializeReference]时,Unity会内联按值序列化自定义类。这意味着,如果你在几个不同的字段中存储对自定义类实例的引用,它们在序列化时会成为独立的对象。然后,当Unity反序列化这些字段时,它们会包含不同的独立对象,具有相同的数据。
  • **[SerializeReference]**序列化:如果你指定了[SerializeReference],Unity会将对象建立为托管引用。宿主对象仍然在其序列化数据中直接存储对象,但在专用注册表部分中。
  • [SerializeReference]增加了一些开销,但支持以下情况:
    • 字段可以为null。内联序列化不能表示null,相反,它会用具有未分配字段的内联对象替换null。
    • 对同一对象的多个引用。如果你在几个不同的字段中存储对自定义类实例的引用而不使用[SerializeReference],它们在序列化时会成为独立的对象。
    • 图和循环数据(例如,引用回自己的对象)。内联类序列化不支持null或共享引用,因此数据中的任何循环都可能导致意外结果,如奇怪的Inspector行为、控制台错误或无限循环。
    • 多态。如果你创建一个派生自父类的类并将其分配给使用父类作为类型的字段,如果不使用[SerializeReference],Unity只会序列化属于父类的字段。当Unity反序列化类实例时,它会实例化父类而不是派生类。
    • 当数据结构需要一个稳定的标识符来指向一个特定对象而不硬编码对象的数组位置或搜索整个数组时。参见Serialization.ManagedReferenceUtility.SetManagedReferenceIdForObject

注意:内联序列化更高效,除非你特别需要[SerializeReference]支持的功能,否则应使用内联序列化。有关如何使用[SerializeReference]的详细信息,请参见序列化参考文档

属性的序列化

Unity通常不会序列化属性,除非在以下情况下:

  • 如果属性有一个显式的后备字段,Unity会根据常规序列化规则进行序列化。例如:
public int MyInt
{
  get => m_backing;
  private set => m_backing = value;
}
[SerializeField] private int m_backing;

Unity仅在热重载期间序列化具有自动生成字段的属性。

public int MyInt { get; set; }

如果你不希望 Unity 序列化具有自动生成字段的属性,请使用[field: NonSerialized]属性。

自定义序列化

有时你可能希望序列化Unity的序列化器不支持的内容(例如,C#字典)。最好的方法是在你的类中实现ISerializationCallbackReceiver接口。这样可以实现序列化和反序列化期间在关键点调用的回调:

  • 当对象即将被序列化时,Unity会调用OnBeforeSerialize()回调。在此回调中,你可以将数据转换为Unity能理解的形式。例如,要序列化C#字典,将数据从字典复制到键数组和值数组中。
  • OnBeforeSerialize()回调完成后,Unity会序列化数组。
  • 稍后,当对象被反序列化时,Unity会调用OnAfterDeserialize()回调。在此回调中,你可以将数据转换回内存中方便使用的形式。例如,使用键和值数组重新填充C#字典。

Unity如何使用序列化

保存和加载

Unity使用序列化来加载和保存场景、资源和AssetBundles到设备内存中。这包括在自己的脚本API对象中保存的数据,如MonoBehaviour组件和ScriptableObject

Unity编辑器中的许多功能都是建立在核心序列化系统之上的。特别需要注意的两个方面是Inspector窗口和热重载。

Inspector窗口

Inspector窗口显示被检查对象的序列化字段的值。当你在Inspector中更改值时,Inspector会更新序列化数据并触发反序列化以更新被检查对象。

这适用于Unity内置对象和脚本对象(如派生自MonoBehaviour的类)。

当你在Inspector窗口中查看或更改值时,Unity不会调用任何C#属性的getter和setter方法,而是直接访问序列化的后备字段。

热重载

热重载是指你在编辑器打开时创建或编辑脚本并立即应用脚本行为。你不需要重新启动编辑器即可使更改生效。

当你更改并保存脚本时,Unity会热重载所有当前加载的脚本数据。Unity会存储所有可序列化变量,然后重新加载这些脚本并恢复序列化变量。热重载会丢弃所有不可序列化的数据,因此你无法在此后访问这些数据。

这会影响项目中的所有编辑器窗口和所有MonoBehaviour。与其他序列化情况不同,Unity在重载时会默认序列化私有字段,即使它们没有SerializeField属性。

当Unity重载脚本时:
Unity会序列化并存储所有加载脚本中的变量。
Unity会恢复它们到原始的预序列化值:
  • Unity会恢复所有满足序列化要求的变量,包括私有变量,即使变量没有SerializeField属性。有时,你需要防止Unity恢复私有变量,例如,如果你希望在脚本重载后引用为null。在这种情况下,请使用[field: NonSerialized]属性。
  • Unity永远不会恢复静态变量,因此不要使用静态变量来保存需要在Unity重载脚本后保留的状态,因为重载过程会丢弃它们。

Prefabs

Prefab是一个或多个GameObjects或组件的序列化数据。Prefab

实例包含对Prefab源的引用以及对它的修改列表。修改是Unity需要对Prefab源进行的操作以创建该特定Prefab实例。

Prefab实例仅在你在Unity编辑器中编辑项目时存在。Unity编辑器从两个序列化数据集实例化一个GameObject:Prefab源和Prefab实例的修改。

实例化

当你调用任何存在于场景中的对象(如Prefab或GameObject)的Instantiate方法时:

  • Unity会序列化它。这在运行时和编辑器中都会发生。Unity可以序列化派生自UnityEngine.Object的所有内容。
  • Unity会创建一个新的GameObject并将数据反序列化到新的GameObject上。
  • Unity会以另一种变体运行相同的序列化代码,以报告它引用的其他UnityEngine.Object。它会检查所有引用的UnityEngine.Object,以查看它们是否是Unity实例化的数据的一部分。如果引用指向外部对象(如纹理),Unity会保留该引用不变。如果引用指向内部对象(如子GameObject),Unity会将引用修补到相应的副本。

卸载未使用的资源

EditorUtility.UnloadUnusedAssetsImmediate是原生的Unity垃圾收集器,其目的与标准C#垃圾收集器不同。它在你加载场景后运行,检查不再引用的对象(如纹理),并安全地卸载它们。原生的Unity垃圾收集器以一种变体运行序列化器,其中对象报告所有对外部UnityEngine.Object的引用。这就是为什么一个场景中使用的纹理可以在下一个场景中被垃圾收集器卸载。

编辑器和运行时序列化的区别

大多数序列化发生在编辑器中,而反序列化则主要在运行时进行。Unity在编辑器中只序列化某些功能,而在编辑器和运行时都可以序列化其他功能:

功能编辑器运行时
二进制格式的资源读/写支持仅支持读取
YAML格式的资源读/写支持不支持
保存场景、预制件和其他资源支持,除非在播放模式不支持
使用JsonUtility序列化单个对象JsonUtility支持读/写。支持附加类型的对象使用EditorJsonUtilityJsonUtility支持读/写
SerializeReference支持支持
ISerializationCallbackReceiver支持支持
FormerlySerializedAs支持不支持
对象可以具有仅编辑器序列化的附加字段,例如在UNITY_EDITOR脚本符号内声明字段时:
public class SerializeRules : MonoBehaviour
{
#if UNITY_EDITOR
public int m_intEditorOnly;
#endif
}

在上述示例中,m_intEditorOnly字段仅在编辑器中序列化,并且不会包含在构建中。这允许你通过省略仅在编辑器中需要的数据来节省内存。任何使用该字段的代码也需要有条件地编译,例如在#if UNITY_EDITOR块内,以便类在构建时能够编译。

编辑器不支持仅在运行时序列化的字段(例如,在UNITY_STANDALONE指令内声明的字段)。

脚本序列化错误

脚本序列化可能导致错误。以下列出了一些修复方法:

  • “Find isn’t allowed to be called from a MonoBehaviour constructor (or instance field initializer), call in Awake or Start instead.”
    • MonoBehaviour构造函数或字段初始化器中调用脚本API(如GameObject.Find)会触发此错误。
    • 修复方法:在MonoBehaviour.Start中调用脚本API,而不是在构造函数中调用。
  • “Find isn’t allowed to be called during serialization, call it from Awake or Start instead.”
    • 在标记为System.Serializable的类的构造函数中调用脚本API(如GameObject.Find)会触发此错误。
    • 修复方法:编辑代码,以确保在任何序列化对象的构造函数中不调用任何脚本API。

线程安全的Unity脚本API

上述限制影响大多数脚本API。只有一些Unity脚本API除外,你可以在任何地方调用它们:

  • Debug.Log
  • Mathf函数
  • 简单的自包含结构体;例如数学结构体如Vector3Quaternion

为了减少序列化期间的错误风险,除非没有替代方法,否则只调用自包含且不需要在Unity中获取或设置数据的API方法。

序列化最佳实践

你可以组织数据,以确保最大限度地利用Unity的序列化。

  • 目标是让Unity序列化的数据集尽可能小。这样做的目的是确保与项目的先前版本保持向后兼容性。在开发后期,如果处理大数据集的序列化,向后兼容性可能变得更加困难。
  • 永远不要让Unity序列化重复数据或缓存数据。这会对向后兼容性造成重大问题:由于数据可能不同步,因此存在很高的错误风险。
  • 避免嵌套、递归结构引用其他类。序列化结构的布局始终需要相同;独立于数据,仅依赖于脚本中公开的内容。唯一引用其他类的方式是通过派生自UnityEngine.Object的类。这些类是独立的;它们只引用彼此,不嵌入内容。

参考

  1. https://docs.unity3d.com/Manual/ScriptingConcepts.html
  2. https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.modulebuilder?view=net-8.0
  3. https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/
  4. https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/
  5. https://github.com/Unity-Technologies/linker
  6. https://github.com/dotnet/linker?tab=readme-ov-file
  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值