Unity之高级编程和代码架构

1.了解Unity PlayerLoop

在这里插入图片描述

Unity PlayerLoop 包含用于与游戏引擎核心交互的函数。此结构包括许多处理初始化和每帧更新的系统。您的所有脚本都将依赖此 PlayerLoop 来创建游戏玩法。分析时,您将在 PlayerLoop 下看到项目的用户代码,并在 EditorLoop 下看到 Editor 组件。
了解 Unity 的 FrameLoop 的执行顺序非常重要。每个 Unity 脚本都按预定顺序运行多个事件函数。了解唤醒、启动、更新和其他创建脚本生命周期以增强性能的函数之间的区别。
一些示例包括在处理刚体时使用 FixedUpdate 而不是 Update,或者在游戏开始之前使用 Awake 而不是 Start 来初始化变量或游戏状态。使用这些代码可以最大程度地减少在每一帧上运行的代码。在脚本实例的生存期内,Awake 仅调用一次,并且始终在 Start 函数之前调用。这意味着,您应该使用 Start 来处理您知道可以与其他对象对话的对象,或者在初始化它们时查询它们。
请参阅脚本生命周期流程图,了解事件函数的特定执行顺序。
在这里插入图片描述

2.构建自定义Update Manager

如果您的项目具有苛刻的性能要求(例如,开放世界游戏),请考虑使用 Update、LateUpdate 或 FixedUpdate 创建自定义 Update Manager。
Update 或 LateUpdate 的常见使用模式是仅在满足某些条件时运行逻辑。这可能会导致许多每帧回调,除了检查此条件外,这些回调实际上不运行任何代码。
每当 Unity 调用消息方法(如 Update 或 LateUpdate)时,它都会进行互操作调用,即从 C/C++ 端到托管 C# 端的调用。对于少数对象,这不是问题。当您拥有数千个对象时,这种开销就会变得很大。
当活动对象需要回调时,将活动对象订阅到此 Update Manager,在不需要回调时取消订阅。此模式可以减少对 Monobehaviour 对象的许多互操作调用。
有关实现示例,请参阅特定于游戏引擎的优化技术。
3.最小运行每一帧的代码
考虑代码是否必须运行每一帧。您可以将不必要的逻辑移出 Update、LateUpdate 和 FixedUpdate。这些 Unity 事件函数是放置必须更新每一帧的代码的方便位置,但您可以提取任何不需要以该频率更新的逻辑。
仅在情况发生变化时执行逻辑。请记住以事件的形式利用观察者模式等技术来触发特定的函数签名。
如果需要使用 Update,可以每 n 帧运行一次代码。这是应用时间切片的一种方法,时间切片是一种在多个帧之间分配繁重工作负载的常用技术。
在此示例中,我们每三帧运行 ExampleExpensiveFunction 一次。
诀窍是将其与在其他帧上运行的其他工作交错。在此示例中,您可以在 Time.frameCount % interval == 1 或 Time.frameCount % interval == 2 时“调度”其他昂贵的函数。
或者,使用自定义 Update Manager 类每 n 帧更新一次订阅的对象。
4.缓存昂贵函数的结果
在 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。
阅读有关事件函数的执行顺序的更多信息。
在这里插入图片描述

5.避免空的Unity事件和调试日志语句
日志语句(尤其是在 Update、LateUpdate 或 FixedUpdate 中)可能会降低性能,因此请在进行生成之前禁用日志语句。若要快速执行此操作,请考虑将条件属性与预处理指令一起创建。
例如,您可能希望创建一个自定义类,如下所示。
使用自定义类生成日志消息。如果在“播放器设置”>“脚本定义符号”中禁用 ENABLE_LOG 预处理器,则所有日志语句都会一举消失。
处理字符串和文本是 Unity 项目中性能问题的常见来源。这就是为什么删除日志语句及其昂贵的字符串格式可能会带来巨大的性能优势。
同样,空的 MonoBehaviours 需要资源,因此应删除空白的 Update 或 LateUpdate 方法。如果采用以下方法进行测试,请使用预处理器指令:

#if UNITY_EDITOR
void Update(){
    
}
#endif

在这里,您可以使用编辑器中的更新进行测试,而不会将不必要的开销滑入您的构建中。
这篇关于 10,000 次更新调用的博客文章介绍了 Unity 如何执行 Monobehaviour.Update。
在这里插入图片描述

6.禁用堆栈跟踪日志记录
使用“播放器设置”中的“堆栈跟踪”选项来控制显示的日志消息类型。如果您的应用程序在发布版本中记录错误或警告消息(例如,在野外生成崩溃报告),请禁用堆栈跟踪以提高性能。
详细了解堆栈跟踪日志记录。
7.使用哈希值而不是字符串参数
Unity 不使用字符串名称在内部对 Animator、Material 或 Shader 属性进行寻址。为了提高速度,所有属性名称都散列到属性 ID 中,这些 ID用于对属性进行寻址。
在 Animator、Material 或 Shader 上使用 Set 或 Get 方法时,请使用整数值方法而不是字符串值方法。字符串值方法执行字符串哈希,然后将哈希 ID 转发给整数值方法。
将 Animator.StringToHash 用于 Animator 属性名称,将 Shader.PropertyToID 用于材质和着色器属性名称。
与此相关的是数据结构的选择,这会影响性能,因为每帧迭代数千次。按照 MSDN C# 数据结构指南作为选择正确结构的一般指南。
在这里插入图片描述

8.使用对象池
Instantiate 和 Destroy 可以生成垃圾回收 (GC) 峰值。这通常是一个缓慢的过程,因此与其定期实例化和销毁游戏对象(例如,从枪中射击子弹),不如使用可重复使用和回收的预分配对象池。
在游戏中的某个时间点创建可重用的实例,例如在菜单屏幕或加载屏幕期间,此时 CPU 峰值不太明显。使用集合跟踪此对象“池”。在游戏过程中,只需在需要时启用下一个可用实例,并禁用对象而不是销毁它们,然后再将它们返回池中。这样可以减少项目中的托管分配数量,并可以防止 GC 问题。
同样,避免在运行时添加组件;调用 AddComponent 会带来一些成本。每当在运行时添加组件时,Unity 都必须检查重复项或其他必需的组件。使用已设置的所需组件实例化预制件的性能更高,因此请将其与对象池结合使用。
与此相关的是,在移动 Transform 时,请使用 Transform.SetPositionAndRotation 同时更新位置和旋转。这样可以避免两次修改 Transform 的开销。
如果您需要在运行时实例化游戏对象,请将其作为父级并重新定位以进行优化,请参见下文。
有关 Object.Instantiate 的详细信息,请参阅脚本 API。
在此处了解如何在 Unity 中创建简单的对象池系统。
在这里插入图片描述

9.利用ScriptableObjects的强大功能
将不变的值或设置存储在 ScriptableObject 中,而不是 MonoBehaviour 中。ScriptableObject 是位于项目内部的资产。它只需要设置一次,不能直接附加到游戏对象。
在 ScriptableObject 中创建字段以存储您的值或设置,然后在 MonoBehaviours 中引用 ScriptableObject。使用 ScriptableObject 中的字段可以防止每次使用该 MonoBehaviour 实例化对象时不必要的数据重复。
观看此 ScriptableObjects 简介教程,并在此处查找相关文档。

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

番茄炒蛋鱼香肉丝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值