
高级编程和代码架构
了解 Unity PlayerLoop
Unity PlayerLoop包含与游戏引擎核心互动的函数。该结构包含许多处理初始化和每帧更新的系统。所有脚本都将依赖PlayerLoop来创建游戏。进行性能分析时,您会在 PlayerLoop 下看到项目的用户代码 - 在 EditorLoop 下看到编辑器组件。
了解Unity FrameLoop的执行顺序很重要。每个Unity脚本都会以预定的顺序运行多个事件函数。了解 Awake、Start、Update 以及创建脚本生命周期以提高性能的其他函数之间的区别。
比如在处理刚体时用FixedUpdate代替Update,或在游戏开始前用Awake代替Start来初始化变量或游戏状态。使用这些功能可以最大限度减少每一帧上运行的代码。Awake在脚本分层的生命周期内只会被调用一次,并且总是在Start函数之前。这意味着你应该使用Start来处理已知可以与其他对象交谈的对象,或者在初始化时查询它们。
有关事件函数的特定执行顺序,请参阅脚本生命周期流程图。

构建自定义 Update Manager
如果您的项目有苛刻的性能要求(例如开放世界游戏 ) , 请考虑使用 Update、LateUpdate 或 FixedUpdate 创建自定义 Update Manager。
Update或LateUpdate的常见用法是仅在满足某些条件时运行逻辑。这可能导致进行大量每帧回调,这些回调实际上不会运行任何代码,除非检查此条件。
每当 Unity 调用 Update 或 LateUpdate 等消息方法时,就会进行互操作调用 – 这意味着从 C/C++ 端到托管 C# 端的调用。对于少数对象,这不是问题。当对象数以千计时,开销会变得很大。
当活动对象需要回调时订阅该 Update Manager,当不需要时取消订阅。这种模式可以减少对MonoBehaviour对象的许多互操作调用。
请参考游戏引擎优化技术来了解应用实例。
减少每一帧的代码
考虑是否必须在每一帧上运行代码。你可以从Update、LateUpdate和FixedUpdate中移除不必要的逻辑。这些 Unity 事件函数是放置必须每帧更新的代码的便利场所,但您可以提取任何不需要以该频率更新的逻辑。
仅在发生更改时执行逻辑。记住利用事件形式的观察者模式等技术来触发特定的函数签名。
如果需要使用Update,可以每隔n帧运行代码。这是应用时间片的一种方式,这是一种将繁重的工作负载分配到多个帧的常见技术。
在本例中,ExampleExpensiveFunction每三帧运行一次。
诀窍是将该功能与其他帧上运行的其他工作交织在一起。在本例中,您可以在 Time.frameCount % interval == 1 或 Time.frameCount % interval == 2 时“调度”其他开销较大的函数。
或者,使用自定义 Update Manager 类来每 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();
ExampleFunction(myRenderer);
}
而GetComponent只会被缓存一次。缓存的结果可以在Update中重复使用,而无需进一步调用GetComponent。
阅读更多关于Order of execution for event functions的内容。
避免空Unity事件和调试日志语句
Log 语句(尤其是在 Update、LateUpdate 或 FixedUpdate 中)可能会降低性能,因此在构建之前禁用 log 语句。要想快速做到这点,你可以考虑在预处理指令里加上一个Conditional Attribute。
比如,你可以创建一个自定义类,如下所示。
使用自定义类生成日志消息。如果你在Player Settings > Scripting DefineSymbols 中禁用 ENABLE_LOG 预处理器,所有的 log 语句都会一下子消失。
处理字符串和文本是 Unity 项目中常见的性能问题来源。因此,删除 log 语句及其代价高昂的字符串格式可能会获得巨大的性能提升。
类似地,空MonoBehaviour也需要占用资源,所以你应该删除空的Update或LateUpdate方法。如果您采用以下方法来进行测试,请使用前处理器指令:
#if Unity_EDITOR
void Update()
{
}
#endif
这时,你可以在编辑器内用Update进行测试,避免不必要的开销。
这篇关于10,000次Update调用的博客文章解释了Unity如何执行MonoBehaviour.Update。
禁用堆栈跟踪记录
使用 Player Settings 中的 Stack Trace 选项控制日志消息的类型。如果应用程序正在记录发布版本中的错误或警告消息(例如,在野外生成崩溃报告),请禁用堆栈跟踪以提高性能。
了解有关堆栈跟踪日志记录的更多信息。
使用哈希值而不是字符串参数
Unity不会在内部使用字符串名称来命名Animator、Material或Shader属性。出于速度考虑,所有属性名称都哈希为属性 ID,这些 ID 用于寻址属性。
在Animator、Material或Shader上使用Set或Get方法时,请使用整数值方法而非字符串值方法。字符串值方法执行字符串哈希,然后将哈希 ID 转发给整数值方法。
将 Animator.StringToHash 用于 Animator 属性名称,将 Shader.PropertyToID 用于 Material 和 Shader 属性名称。
与此相关的是数据结构的选择,它会影响性能,因为每帧会迭代数千次。请遵循C#的MSDN数据结构指南来选择合适的结构。

集中对象
实例化和销毁会产生垃圾收集高峰(GC)。这通常是一个缓慢的过程,因此与其定期实例化和销毁游戏对象(例如,从枪中发射子弹 ) , 不如使用可重复使用和回收的预分配对象池。
在游戏的某个时间点(如菜单界面或加载界面,此时 CPU 峰值不太明显)创建可重用的实例。通过集合跟踪此对象“池”。在游戏过程中,你可以根据需要启用下一个分层对象,禁用而不是销毁它们,然后再将其送回池中。这样可以减少项目中托管分配的数量,并防止 GC 问题。
同样地,避免在运行时添加组件;调用AddComponent也会带来一些成本。每当在运行时添加组件时,Unity 必须检查重复项或其他所需的组件。使用已经设置好的组件实例化预制件性能更好,所以要与对象池结合使用。
与此相关,在移动Transform时,使用Transform.SetPositionAndRotation一次更新位置和旋转。这可避免两次修改变换的开销。
如果需要在运行时实例化游戏对象,请将其父对象重新定位以进行优化,请参阅下文。
有关Object.Instantiate的详情,见Scripting API。
在这里学习如何在Unity中创建一个简单的对象池系统。

利用ScriptableObject的强大功能
将不变的值或设置存储在 ScriptableObject 中,而不是 MonoBehaviour。ScriptableObject是项目里的资产。它只需设置一次,不能直接连接到游戏对象。
在ScriptableObject中创建字段来存储值或设置,然后在MonoBehaviour中引用ScriptableObject。使用ScriptableObject的字段可以避免每次使用该MonoBehaviour实例化对象时产生不必要的数据重复。
观看介绍ScriptableObjects操作指引并在此查找相关文档。

我们有史以来最全面的指南之一收集了 80 多个关于如何为 PC 和游戏主机优化游戏的可操作技巧。这些深入的技巧由我们的 Success 和 Accelerate Solutions 专家工程师创建,可帮助您充分利用 Unity 并提高游戏性能。