您想找什么?
Hero background image
高级编程和代码架构
探索您的代码架构以进一步优化图形渲染。这是系列文章的第四篇,介绍了 Unity 项目的优化技巧。使用它们作为以更少资源以更高帧速率运行的指南。尝试这些最佳实践后,请务必查看该系列的其他页面:配置 Unity 项目以获得更强大的性能 针对高端图形的性能优化 管理 PC 和主机游戏的 GPU 使用情况 增强的物理性能以实现流畅的游戏体验
了解 Unity PlayerLoop

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

理解 Unity 的 FrameLoop执行顺序 非常重要。每个 Unity 脚本都按照预定的顺序运行多个事件函数。了解 AwakeStartUpdate以及其他创建脚本生命周期以增强性能的函数之间的区别。

一些示例包括在处理 Rigidbody 时使用 FixedUpdate 而不是 Update,或者在游戏开始前使用 Awake 而不是 Start 来初始化变量或游戏状态。使用这些来最小化每帧上运行的代码。Awake 在脚本实例的生命周期内仅被调用一次,并且总是在 Start 函数之前被调用。这意味着您应该使用 Start 来处理您知道可以与其他对象对话的对象,或者在它们已初始化时查询它们。

请参阅 脚本生命周期流程图 以了解事件函数的具体执行顺序。

自定义更新管理器图
构建自定义更新管理器

如果您的项目对性能有严格的要求(例如,开放世界游戏),请考虑使用 UpdateLateUpdateFixedUpdate创建自定义更新管理器。

Update 或 LateUpdate 的常见使用模式是仅当满足某些条件时才运行逻辑。这会导致大量的每帧回调,除了检查这个条件之外,实际上不运行任何代码。

每当 Unity 调用诸如 Update 或 LateUpdate 之类的消息方法时,它都会进行互操作调用 - 即从 C/C++ 端到托管 C# 端的调用。对于少数物体来说,这不是问题。当你有数千个对象时,这种开销就开始变得显著。

当活动对象需要回调时,将其订阅到此更新管理器,当不需要回调时,则取消订阅。这种模式可以减少对 Monobehaviour 对象的许多互操作调用。

请参阅 游戏引擎特定的优化技术 以获取实现示例。

最小化每帧运行的代码

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

仅当情况发生变化时执行逻辑。记住要利用事件形式的观察者模式等技术来触发特定的函数签名。

如果您需要使用更新,您可能每 n 帧运行一次代码。这是应用 时间分片的一种方法,时间分片是一种将繁重的工作负载分配到多个帧的常用技术。

在这个例子中,我们每三帧运行一次 ExampleExpensiveFunction

诀窍是将其与在其他帧上运行的其他工作交错进行。在此示例中,你可以在 Time.frameCount % interval == 1Time.frameCount % interval == 2时“安排”其他昂贵的函数。

或者,使用自定义更新管理器类每 n 帧更新一次订阅的对象。

缓存昂贵函数的结果

2020.2 之前的Unity 版本中,GameObject.FindGameObject.GetComponentCamera.main 可能很昂贵,因此最好避免在 Update 方法中调用它们。

此外,如果经常调用 OnEnableOnDisable,请尽量避免在其中放置昂贵的方法。频繁调用这些方法可能会导致 CPU 峰值。

尽可能在初始化阶段运行昂贵的函数,例如 MonoBehaviour.AwakeMonoBehaviour.Start。缓存所需的引用并在稍后重新使用它们。请参阅我们之前关于 Unity PlayerLoop 的部分以了解脚本顺序执行的更多详细信息。

以下示例演示了重复使用 GetComponent 调用的低效性:

无效更新()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}

相反,只需调用一次 GetComponent,因为该函数的结果已被缓存。缓存的结果可以在 Update 中重复使用,无需进一步调用 GetComponent。

阅读有关 事件函数执行顺序的更多信息。

避免空的 Unity 事件和调试日志语句

日志语句(特别是在 Update、LateUpdate 或 FixedUpdate 中)可能会降低性能,因此在进行构建之前请禁用日志语句。为了快速完成此操作,请考虑制作一个 条件属性 以及预处理指令。

例如,您可能想要创建一个自定义类,如下所示。

使用您的自定义类生成您的日志消息。如果你在 播放器设置 > 脚本定义符号中禁用 ENABLE_LOG 预处理器,那么所有的日志语句都会一下子消失。

处理字符串和文本是 Unity 项目中性能问题的常见来源。这就是为什么删除日志语句及其昂贵的字符串格式可能会带来巨大的性能提升。

类似地,空的 MonoBehaviour 需要资源,因此您应该删除空白的 Update 或 LateUpdate 方法。如果您采用以下方法进行测试,请使用预处理器指令:

#如果 UNITY_EDITOR
无效更新()
{
}
#endif

在这里,您可以使用编辑器内的更新进行测试,而不会给您的构建带来不必要的开销。

这篇关于 10,000 次更新调用的 博客文章解释了 Unity 如何执行 Monobehaviour.Update

禁用堆栈跟踪日志记录

使用 播放器设置 中的 堆栈跟踪 选项来控制出现的日志消息类型。如果您的应用程序在发布版本中记录错误或警告消息(例如,在野外生成崩溃报告),请禁用堆栈跟踪以提高性能。

了解有关 堆栈跟踪日志记录的更多信息。

使用哈希值而不是字符串参数

Unity 不使用字符串名称来在内部寻址 AnimatorMaterialShader 属性。为了提高速度,所有属性名称都被散列为 属性 ID,并且这些 ID 用于寻址属性。

在 Animator、Material 或 Shader 上使用 SetGet 方法时,请利用整数值方法而不是字符串值方法。字符串值方法执行字符串散列,然后将散列的 ID 转发给整数值方法。

使用 Animator.StringToHash 作为 Animator 属性名称,使用 Shader.PropertyToID 作为 Material 和 Shader 属性名称。

相关的是数据结构的选择,它会影响性能,因为每帧迭代数千次。遵循 MSDN 的 C# 数据结构指南 作为选择正确结构的一般指南。

对象池脚本接口
池化你的对象

实例化销毁 可能会产生 垃圾收集 (GC) 峰值。这通常是一个缓慢的过程,因此,不要定期实例化和销毁游戏对象(例如,用枪射出子弹),而是使用可以重用和回收的预分配对象

在游戏中的某个时间点创建可重复使用的实例,例如在菜单屏幕或加载屏幕期间,此时 CPU 峰值不太明显。使用集合来跟踪这个对象“池”。在游戏过程中,只需在需要时启用下一个可用实例,并禁用对象而不是销毁它们,然后将它们返回到池中。这会减少项目中管理分配的数量并可以防止 GC 问题。

同样,避免在运行时添加组件; 调用 AddComponent 会带来一些成本。每次在运行时添加组件时,Unity 都必须检查重复或其他所需组件。实例化已设置所需组件的 预制件性能更高,因此请将其与 对象池结合使用。

相关的,当移动 Transforms时,使用 Transform.SetPositionAndRotation 同时更新位置和旋转。这避免了两次修改 Transform 的开销。

如果您需要在运行时 实例化一个 GameObject、将其设置为父级并重新定位以进行优化,请参见下文。

有关 Object.Instantiate的更多信息,请参阅 脚本 API

在此了解如何在 Unity 中创建一个简单的对象池系统。

可编写脚本的对象池
利用 ScriptableObjects 的强大功能

将不变的值或设置存储在 ScriptableObject 中,而不是 MonoBehaviour 中。ScriptableObject 是存在于项目内部的资产。它只需要设置一次,并且不能直接附加到GameObject。

在 ScriptableObject 中创建字段来存储您的值或设置,然后在 MonoBehaviours 中引用 ScriptableObject。使用 ScriptableObject 中的字段可以防止每次使用该 MonoBehaviour 实例化对象时不必要的数据重复。

观看 ScriptableObjects 简介教程在此处查找相关文档。

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

我们迄今为止最全面的指南之一收集了 80 多条有关如何针对 PC 和游戏机优化游戏的可行技巧。这些深入的提示由我们的专业成功和Accelerate Solutions工程师创建,将帮助您充分利用 Unity 并提升游戏性能。

您喜欢本文吗?
是的!
还行。