您想找什么?
Hero background image

高级编程和代码架构

探索代码架构,进一步优化图形渲染。 本文属Unity项目优化技巧系列文章的第四篇。你可以用它们指导用更少的资源运行更高的帧率。尝试了这些最佳实践后,请务必查看系列的其他页面: 配置Unity项目以获得更强的性能 高端图形的性能优化 管理PC和主机游戏的GPU使用情况 更强的物理性能,流畅的游戏体验
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

了解 Unity PlayerLoop

Unity PlayerLoop包含与游戏引擎核心互动的函数。该结构包含许多处理初始化和每帧更新的系统。所有脚本都将依赖PlayerLoop来创建游戏。进行性能分析时,您会在 PlayerLoop 下看到项目的用户代码 - 在 EditorLoop 下看到编辑器组件。

了解Unity FrameLoop执行顺序很重要。每个Unity脚本都会以预定的顺序运行多个事件函数。了解 AwakeStartUpdate 以及创建脚本生命周期以提高性能的其他函数之间的区别。

比如在处理刚体时用FixedUpdate代替Update,或在游戏开始前用Awake代替Start来初始化变量或游戏状态。使用这些功能可以最大限度减少每一帧上运行的代码。Awake在脚本分层的生命周期内只会被调用一次,并且总是在Start函数之前。这意味着你应该使用Start来处理已知可以与其他对象交谈的对象,或者在初始化时查询它们。

有关事件函数的特定执行顺序,请参阅脚本生命周期流程图。

自定义更新管理器图

构建自定义 Update Manager

如果您的项目有苛刻的性能要求(例如开放世界游戏 ) , 请考虑使用 UpdateLateUpdateFixedUpdate 创建自定义 Update Manager。

Update或LateUpdate的常见用法是仅在满足某些条件时运行逻辑。这可能导致进行大量每帧回调,这些回调实际上不会运行任何代码,除非检查此条件。

每当 Unity 调用 Update 或 LateUpdate 等消息方法时,就会进行互操作调用 – 这意味着从 C/C++ 端到托管 C# 端的调用。对于少数对象,这不是问题。当对象数以千计时,开销会变得很大。

当活动对象需要回调时订阅该 Update Manager,当不需要时取消订阅。这种模式可以减少对MonoBehaviour对象的许多互操作调用。

请参考游戏引擎优化技术来了解应用实例。

减少每一帧的代码

考虑是否必须在每一帧上运行代码。你可以从Update、LateUpdate和FixedUpdate中移除不必要的逻辑。这些 Unity 事件函数是放置必须每帧更新的代码的便利场所,但您可以提取任何不需要以该频率更新的逻辑。

仅在发生更改时执行逻辑。记住利用事件形式的观察者模式等技术来触发特定的函数签名。

如果需要使用Update,可以每隔n帧运行代码。这是应用时间片的一种方式,这是一种将繁重的工作负载分配到多个帧的常见技术。

在本例中,ExampleExpensiveFunction每三帧运行一次。

诀窍是将该功能与其他帧上运行的其他工作交织在一起。在本例中,您可以在 Time.frameCount % interval == 1Time.frameCount % interval == 2 时“调度”其他开销较大的函数。

或者,使用自定义 Update Manager 类来每 n 帧更新订阅的对象。

缓存开销较大的函数的结果

2020.2之前的Unity版本中,GameObject.FindGameObject.GetComponentCamera.main的成本可能很高,所以最好不要在Update方法中调用它们。

此外,如果经常调用OnEnableOnDisable方法,请尽量避免在它们中放置开销较大的方法。频繁调用这些方法可能会导致 CPU 峰值。

尽可能在初始化阶段运行代价高昂的函数,如MonoBehaviour.AwakeMonoBehaviour.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不会在内部使用字符串名称来命名AnimatorMaterialShader属性。出于速度考虑,所有属性名称都哈希为属性 ID,这些 ID 用于寻址属性。

在Animator、Material或Shader上使用SetGet方法时,请使用整数值方法而非字符串值方法。字符串值方法执行字符串哈希,然后将哈希 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操作指引并在查找相关文档。

Unity 关键艺术 21 11
获取免费电子书

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