Unity 快速进入运行模式( Fast Enter Play
介绍
你是否会觉得有时候进入运行模式的时候非常慢,这是因为 Unity 需要保证你每次进入游戏的时候都是一样的,即使在退出游戏的时候你没有进行还原操作。当然这只是一部分原因,还有 Start 内逻辑复杂或者脚本过多等原因。下面介绍如果减少 Unity 自己的初始化操作,下面内容基本翻译于 Configurable Enter Play Mode 和 Domain Reloading。
进入运行模式的配置
运行模式可以让你的项目直接在编辑器中运行,运行效果跟你将它打包出来之后的运行效果一样,在运行模式你所做的修改在退出该模式之后都会被还原(这里只包括场景中的修改,如果你在代码中修改了一个资源,比如一个材质球的基色,那么不会被还原)。
这是因为在开始运行之后,Unity 做了两件事情来保证它看起来是跟打包之后运行的效果一样:
- 它重置了所有的脚本状态(也叫作域重载)
- 它重新加载了这个场景
这两个操作会消耗一些时间,并且当你的脚本和场景更复杂的时候,它们耗时也更长。
提高快速进入、退出运行模式的能力在开发中是非常重要的。更快的进入、退出运行模式也意味着快速的测试项目中的改动。
因为在开发中快速迭代是非常重要的,但这种重置有可能成为速度的瓶颈,所以 Unity 提供了一种方式可以让开发者自己选择在进入运行模式的时候要不要域重载或者场景重载。
下面一张图描述了禁用域重载或者场景重载的影响:
禁用域重载或者场景重载的影响
如何配置
在 2019.3 (或许更低也可以,我试过 2018.4,是没有这个选项的)以上的 Unity 版本中,找到 Edit->Project Settings->Editor->Enter Play Mode Settings。
- 三个选择框都不勾,其实相当于将三个都勾,也就是会启用域重载和场景重载
- 只勾选第一个,则代表不启用域重载和场景重载
- 勾选第一个,下面两个二选一则代表只启用一种重载
这个功能还在试验阶段,后面还有可能会进行改动。
域重载
域重载重置你的脚本状态,它默认是启用的(上面三个勾默认是都不勾)。它会提供一个完全干净的脚本初始化状态,会重置所有的静态变量,将所有 Handler 里面注册的方法都清空。这样可以保证在编辑器中每一次运行都跟将项目打包出来后第一次运行一样。
这会耗费一定时间,随着场景和脚本的复杂程度增加,时间也会增加,不利于项目的快速迭代,所以 Unity 提供了关闭域重载的方法。
当关闭了域重载之后,进入运行模式的速度会变快,因为 Unity 不会再做初始化操作。这时就需要你自己手动完成初始化,你需要自己添加一些代码来保证脚本中某些状态在运行时会被还原。
当域重载被禁用时,当你更新脚本或者重新导入时 Unity 依然会根据你的 auto-refresh settings 刷新脚本状态。
在禁用域重载之后该做什么?
你需要调整脚本中的静态变量和静态事件 Handler 来保证在刚进入运行模式的时候脚本状态是正确的。
静态变量
当域重载被禁用的时候,静态变量的值将不会自动恢复成原来的值,你需要明确的写出让它恢复的代码。
以下面的代码为例,其中有一个静态的变量 counter
,当用户按下了 Jump 键的时候它会自增。当没有禁用域重载的时候,每次运行它的值都是 0;当禁用域重载的时候,每次运行时它的值都是上一次运行结束时的值。
using UnityEngine;
public class StaticCounterExample : MonoBehaviour
{
// this counter will not reset to zero when Domain Reloading is disabled
static int counter = 0;
// Update is called once per frame
void Update()
{
if (Input.GetButtonDown("Jump"))
{
counter++;
Debug.Log("Counter: " + counter);
}
}
}
为了保证即使在禁用了域重载的时候,counter
依然能被正确的归零,我们要使用 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
标签来明确的对 counter
初始化。
using UnityEngine;
public class StaticCounterExampleFixed : MonoBehaviour
{
static int counter = 0;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Init()
{
Debug.Log("Counter reset.");
counter = 0;
}
// Update is called once per frame
void Update()
{
if (Input.GetButtonDown("Jump"))
{
counter++;
Debug.Log("Counter: " + counter);
}
}
}
静态事件处理器
这里的静态事件处理器是由 Static event handlers
翻译而来的。举个例子,就是 public static event Action TestActionHandler;
当禁用域重载的时候,Unity 不会在运行模式退出的时候把静态事件处理器里面已经注册的方法删除。这会导致回调函数的多次执行,比如在第一次运行的时候,回调函数被正常注册,第二次运行的时候,处理器(Handler)里面已经有这个回调函数了,但它又被注册了一遍,所以当处理器去广播的时候(也就是 Handler?.Invoke()
的时候),该回调函数会被执行两遍。
以下面的代码为例,我们在 Start
里面给 Application.quitting
注册了一个回调函数,注意这里的 Application.quitting
是静态的,它的声明如下:
public static event Action quitting;
当域重载没被禁用的时候,Unity 会在运行的时候自动清空 Application.quitting
,所以每次运行回调函数都只会被注册一次。
当域重载被禁用的时候,Application.quitting
将不会被自动清空。看下面的例子,第一次运行结束的时候,Quitting
会被打印一次;第二次运行结束的时候 Quitting
会被打印两次......但实际上我们都希望每次运行结束后, Quitting
都只会被打印一次。
using UnityEngine;
public class StaticEventExample : MonoBehaviour
{
void Start()
{
Debug.Log("Registering quit function");
Application.quitting += Quit;
}
static void Quit()
{
Debug.Log("Quitting!");
}
}
为了保证即使在域重载被禁用的情况下,我们也能正确的使用静态事件处理器,我们需要使用 [RuntimeInitializeOnLoadMethod]
标签,并且明确的写出要删除已经被注册的方法。用法如下:
using UnityEngine;
public class StaticEventExampleFixed : MonoBehaviour
{
[RuntimeInitializeOnLoadMethod]
static void RunOnStart()
{
Debug.Log("Unregistering quit function");
Application.quitting -= Quit;
}
void Start()
{
Debug.Log("Registering quit function");
Application.quitting += Quit;
}
static void Quit()
{
Debug.Log("Quitting the Player");
}
}
对于运行时脚本(非编辑器相关),你需要使用 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
来手动初始化静态变量和静态事件处理器。
对于编辑器脚本,你需要使用 [InitializeOnEnterPlayMode]
来初始化静态变量或者静态事件处理器。
笔者补充
static void Quit()
{
Debug.Log("Quitting!");
}
上面例子中的 Quit
这个回调方法也是静态的,但这并不影响,加不加这个静态结果都一样,依然会被注册多次,因为起决定性作用是 Application.quitting
是静态的。
它的声明是 public static event Action quitting;
,如果变成 public event Action quitting;
那么就不会被注册多次。