Unity PlayerLoop 包含与游戏引擎核心交互的功能。这种结构包括许多处理初始化和每帧更新的系统。你所有的脚本都将依赖此 PlayerLoop 来创建游戏。分析时,您会在 PlayerLoop 下看到项目的用户代码 — — 在 EditorLoop 下看到编辑器组件。
了解 Unity 框架的执行顺序非常重要。每个 Unity 脚本都按预定顺序运行多个事件函数。了解唤醒、启动、更新和其他创建脚本生命周期以增强性能的功能之间的区别。
一些例子包括在处理僵化体时使用FixedUpdate代替Update,或者在游戏开始前使用Awake代替Start初始化变量或游戏状态。使用这些代码可以最小化每个帧上运行的代码。Awake 在脚本分层的生存期内只被调用一次,并且总是在 Start 函数之前调用。这意味着你应该使用Start来处理你知道可以和其他对象说话的对象,或者在它们被初始化时查询它们。
事件函数的具体执行顺序请参见脚本生命周期流程图。
如果您的项目有苛刻的性能要求(例如,开放世界的游戏 ) , 请考虑使用Update、LateUpdate或FixedUpdate创建一个自定义的更新管理器。
Update 或 LateUpdate 的常见使用模式是仅在满足某些条件时运行逻辑。这可能导致许多每帧回调,除了检查此条件外,这些回调实际上不运行任何代码。
每当 Unity 调用 Update 或 LateUpdate 这样的消息方法时,它都会进行互操作调用 — — 这意味着从 C/C++ 端到托管 C# 端的调用。对于少数对象,这不是问题。当你有成千上万的对象时,这个开销开始变得很大。
当活动对象需要回调时订阅此更新管理器,当不需要时取消订阅。这种模式可以减少对MonoBehaviour对象的许多互操作调用。
有关实现示例,请参阅游戏引擎特定优化技术。
考虑代码是否必须运行每一帧。您可以将不必要的逻辑移出 Update、LateUpdate 和 FixedUpdate。这些 Unity 事件函数是放置必须更新每一帧的代码的方便场所,但您可以提取不需要以该频率更新的任何逻辑。
只在情况发生变化时执行逻辑。记住利用事件形式的观察者模式等技术来触发特定的函数签名。
如果需要使用 Update,则可能每隔 n 帧运行代码。这是应用时间片的一种方式,时间片是在多个帧之间分配繁重工作负载的常用技术。
在本例中,我们每三帧运行一次 ExampleExpensiveFunction。
诀窍是将此与其他帧上运行的其他工作交织在一起。在本例中,您可以在 Time.frameCount % interval == 1 或 Time.frameCount % interval == 2 时“安排”其他昂贵的函数。
或者,使用自定义更新管理器类,每隔 n 帧更新订阅的对象。
在2020.2之前的Unity版本中,GameObject.Find、GameObject.GetComponent和Camera.main可能价格昂贵,因此最好避免在Update方法中调用它们。
此外,如果经常调用昂贵方法,请尽量避免将其置于 OnEnable 和 OnDisable 中。经常调用这些方法会导致CPU尖峰。
尽可能运行昂贵的函数,如MonoBehaviour.Awake和MonoBehaviour.Start在初始化阶段启动。缓存所需的引用,稍后重复使用。请查看我们前面关于 Unity PlayerLoop 的章节,了解脚本顺序执行的详细信息。
下面是一个示例,演示了重复调用 GetComponent 的低效使用:
void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}
相反,由于函数的结果被缓存,所以只调用一次GetComponent。缓存的结果可以在 Update 中重用,而无需进一步调用 GetComponent。
了解有关事件函数执行顺序的详细信息。
Log 语句(特别是在 Update、LateUpdate 或 FixedUpdate 中)可能会降低性能,因此在生成之前禁用日志语句。要快速做到这一点,请考虑在预处理指令的同时创建条件属性。
例如,您可能希望创建如下所示的自定义类。
使用自定义类生成日志消息。如果在播放器设置>脚本定义符号中禁用 ENABLE_LOG 预处理器,您的所有日志语句将一下子消失。
处理字符串和文本是 Unity 项目中常见的性能问题。这就是为什么删除日志语句及其昂贵的字符串格式可能会带来巨大的性能胜利。
同样,空的 MonoBehaviour 也需要资源,因此您应该删除空的 Update 或 LateUpdate 方法。如果您采用以下方法进行测试,请使用预处理器指令:
#if UNITY_EDITOR
void Update()
{
}
#endif
在这里,您可以使用更新编辑器进行测试,而不会将不必要的开销滑入构建中。
这篇关于 10,000 Update 调用的博文解释了 Unity 如何执行 MonoBehaviour.Update。
使用播放器设置中的堆栈跟踪选项来控制日志消息的显示类型。如果您的应用程序正在版本构建中记录错误或警告消息(例如,在野外生成崩溃报告),请禁用堆栈跟踪以提高性能。
了解有关堆栈跟踪日志记录的详细信息。
Unity 不使用字符串名称在内部寻址 Animator、Material 或 Shader 属性。为加快速度,所有属性名称都散列到属性 ID 中,这些 ID 用于寻址属性。
在动画器、材质或着色器上使用 Set 或 Get 方法时,请使用整数值方法而不是字符串值方法。字符串值方法执行字符串哈希,然后将哈希 ID 转发给整数值方法。
对 Animator 属性名称使用 Animator.StringToHash,对 Material 和 Shader 属性名称使用 Shader.PropertyToID。
与此相关的是数据结构的选择,当您每帧重复数千次时,这会影响性能。按照 C# 中的 MSDN 数据结构指南作为选择正确结构的一般指南。
Instantiate和Destroy可以生成垃圾收集(GC)峰值。这通常是一个缓慢的过程,因此与其定期实例化和销毁 GameObjects(例如从枪中射出子弹 ) , 不如使用可重复使用和回收的预分配对象池。
在游戏中的某个点创建可重用实例,比如在菜单屏幕或加载屏幕期间,CPU 峰值不太明显。用集合跟踪此对象“池”。在游戏过程中,只需在需要时启用下一个可用分层,禁用对象而不是销毁它们,然后将其返回到池中。这样可以减少项目中托管分配的数量,并可以防止GC问题。
同样,避免在运行时添加组件;调用AddComponent会有一些代价。Unity 必须在运行时添加组件时检查是否有重复组件或其他必需的组件。用已经设置好的所需组件实例化预制件性能更好,因此将此与对象池结合使用。
相关,移动"变换"时,使用 Transform.SetPositionAndRotation 一次更新位置和旋转。这样就避免了修改两次变换的开销。
如果需要在运行时实例化 GameObject,父级并重新定位以进行优化,请参见下文。
有关 Object.Instantiate 的详细信息,请参阅脚本 API。
在这里学习如何在 Unity 中创建一个简单的对象池系统。
我们最全面的指南之一收集了超过80个关于如何优化PC和游戏机游戏的可操作提示。这些深入的技巧由我们的专家 Success and Accelerate Solutions 工程师创建,将帮助您充分利用 Unity 并提高游戏性能。