您想找什么?
Hero background image
高级编程和代码架构
探索您的代码架构,进一步优化图形的渲染。这是为 Unity 项目解包优化技巧的系列文章中的第四篇。将它们用作以更少的资源以更高的帧速率运行的指南。一旦你尝试了这些最佳实践,请务必查看系列中的其他页面:配置 Unity 项目以获得更强大的性能 针对高端显卡的性能优化 管理 PC 和控制台游戏的 GPU 使用情况 增强的物理性能以实现流畅的游戏
此页面为机器翻译。如需查看原文以确保准确性并作为权威参考,
了解 Unity PlayerLoop

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

了解 Unity 框架执行顺序非常重要。每个 Unity 脚本都按预定顺序运行多个事件函数。了解唤醒启动更新和其他创建脚本生命周期以增强性能的功能之间的区别。

一些例子包括在处理僵化体时使用FixedUpdate代替Update,或者在游戏开始前使用Awake代替Start初始化变量或游戏状态。使用这些代码可以最小化每个帧上运行的代码。Awake 在脚本分层的生存期内只被调用一次,并且总是在 Start 函数之前调用。这意味着你应该使用Start来处理你知道可以和其他对象说话的对象,或者在它们被初始化时查询它们。

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

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

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

Update 或 LateUpdate 的常见使用模式是仅在满足某些条件时运行逻辑。这可能导致许多每帧回调,除了检查此条件外,这些回调实际上不运行任何代码。

每当 Unity 调用 Update 或 LateUpdate 这样的消息方法时,它都会进行互操作调用 — — 这意味着从 C/C++ 端到托管 C# 端的调用。对于少数对象,这不是问题。当你有成千上万的对象时,这个开销开始变得很大。

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

有关实现示例,请参阅游戏引擎特定优化技术

最小化运行每一帧的代码

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

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

如果需要使用 Update,则可能每隔 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 的低效使用:

void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}

相反,由于函数的结果被缓存,所以只调用一次GetComponent。缓存的结果可以在 Update 中重用,而无需进一步调用 GetComponent。

了解有关事件函数执行顺序的详细信息。

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

Log 语句(特别是在 Update、LateUpdate 或 FixedUpdate 中)可能会降低性能,因此在生成之前禁用日志语句。要快速做到这一点,请考虑在预处理指令的同时创建条件属性

例如,您可能希望创建如下所示的自定义类。

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

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

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

#if UNITY_EDITOR
void Update()
{
}
#endif

在这里,您可以使用更新编辑器进行测试,而不会将不必要的开销滑入构建中。

这篇关于 10,000 Update 调用的博文解释了 Unity 如何执行 MonoBehaviour.Update

禁用堆栈跟踪记录

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

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

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

Unity 不使用字符串名称在内部寻址 AnimatorMaterialShader 属性。为加快速度,所有属性名称都散列到属性 ID 中,这些 ID 用于寻址属性。

在动画器、材质或着色器上使用 SetGet 方法时,请使用整数值方法而不是字符串值方法。字符串值方法执行字符串哈希,然后将哈希 ID 转发给整数值方法。

对 Animator 属性名称使用 Animator.StringToHash,对 Material 和 Shader 属性名称使用 Shader.PropertyToID

与此相关的是数据结构的选择,当您每帧重复数千次时,这会影响性能。按照 C# 中的 MSDN 数据结构指南作为选择正确结构的一般指南。

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

InstantiateDestroy可以生成垃圾收集(GC)峰值。这通常是一个缓慢的过程,因此与其定期实例化和销毁 GameObjects(例如从枪中射出子弹 ) , 不如使用可重复使用和回收的预分配对象

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

同样,避免在运行时添加组件;调用AddComponent会有一些代价。Unity 必须在运行时添加组件时检查是否有重复组件或其他必需的组件。用已经设置好的所需组件实例化预制性能更好,因此将此与对象池结合使用。

相关,移动"变换"时,使用 Transform.SetPositionAndRotation 一次更新位置和旋转。这样就避免了修改两次变换的开销。

如果需要在运行时实例化 GameObject,父级并重新定位以进行优化,请参见下文。

有关 Object.Instantiate 的详细信息,请参阅脚本 API

这里学习如何在 Unity 中创建一个简单的对象池系统。

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

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

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

请观看本脚本对象介绍操作指引,并在此处查找相关文档。

Unity key art 21 11
获取免费的电子书

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

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