之前,我们已经对本地资源检测中和资源/Prefab 的内容做了总结,后续 UWA 也会和大家一起努力,进一步丰富这些检测内容。今天我们要聚焦的是本地资源检测中的 C# 代码相关的检测项。

要保证游戏在流畅的帧率下运行,就要保证 CPU 和 GPU 能够及时地完成它们在一帧当中的 “任务”。而本文我们讲解的这些 C# 代码的性能,就会影响到每帧 CPU 自身的耗时。当游戏出现帧率降低、间歇性卡顿甚至卡死等现象时,我们就需要考虑这是否是由开发者自己所编写的脚本性能较差所造成的。

性能的优化问题,实际上是个 “时空问题”:我们要尽可能地节省运算所需的时间,节约内存上占用的空间。对 C# 代码的优化亦是如此,一方面是对 CPU 耗时的优化,一方面是对内存分配的优化。而 “空间问题” 与 “时间问题” 又常常会相互转化——优化内存的目的之一,是减少 GC,这又可以归结为减少 CPU 耗时。本文要讲解的一系列规则,就是主要针对 CPU 耗时的规则。

1、类中存在 OnGUI 方法


规则里涉及到的 OnGUI,它是 Unity 的 IMGUI 系统绘制 UI 所调用的方法。该方法如果写在继承了 Monobehavior 的脚本上,那么 Unity 会在每一帧自动对其进行调用。

使用 IMGUI 进行 UI 绘制,想要更改任何内容,整个图形用户界面都要重新绘制,OnGUI 会在一帧当中调用多次,这会导致 CPU 耗时增加。此外,如果 OnGUI 函数使用不当,容易造成堆内存的持续分配。因此,在游戏项目中,一般不使用 IMGUI 进行 UI 开发(常用的有 UGUI、NGUI 等)。IMGUI 一般用于编辑器扩展开发、游戏调试面板绘制等。


2、类中存在空的 Update、LateUpdate 和 FixedUpdate 方法


我们在上面的规则中简单说到了 “Monobehavior”,Unity 中的脚本其实默认都是继承自这个 Monobehavior。Update、LateUpdate 和 FixedUpdate 属于 Monobehaviour 类的 “Messages”,虽然不由 Monobehavior 类继承而来,但是在 Monobehaviour 类的脚本中会生效——如果脚本中写上了这些方法,相应的脚本放到场景中,并且 enable 为 true,那么游戏运行过程中每帧都会对其进行调用。

即使这些方法为空,在运行时,它们依然会因为被调用而造成 CPU 时间的开销,其原因主要有两点:

  1. 这些方法是 Native 层对托管层的调用,C++ 与 C# 之间的通信本身存在一定的开销。
  2. 当调用这些方法时,Unity 会进行一系列安全检测(比如确保 GameObject 没有被销毁等)导致 CPU 时间的消耗。 ***

3、该类的方法中存在 Camera.main 的调用


Camera.main 实际上是一个实现了 Get 方法的属性,每次调用它,都会寻找场景中第一个 Tag 为 “MainCamera” 的相机并将其返回。使用旧版本的 Unity 对 Camera.main 的调用,需要遍历所有带 Tag 的 GameObject、进行 Tag 比较、查找 Camera 组件等操作,耗时较高,并且引擎不会自动缓存其结果。

不过 Unity 2020.2 版本中已经对 Camera.main 进行了优化,避免了它较高的 CPU 耗时,使用 2020.2 及以上 Unity 版本的团队可以忽略该规则。


4、该类的方法中存在 ComputeBuffer.GetData 调用


ComputeBuffer.GetData 会从 GPU 的 Buffer 中读取对应的计算结果并输入到相应的数组中,由于整个的过程是一个同步操作,调用时会堵塞调用线程,直到 GPU 返回数据为止,所以在数据量较大的时候会导致 ComputeBuffer.GetData 消耗很大一部分的 CPU 时间以及相应的堆内存空间。可以尝试通过其他的异步操作来达到相同的取值效果。

5、该类的方法中存在对纹理 SetPixels 的调用


SetPixels 可用于对纹理特定的 mipmap 层的像素进行修改,它会将一组数组的像素值赋值到贴图的指定 mipmap 层,调用 Apply() 后会将像素传至显卡。需注意的是,由于 Color32 比 Color 类型所占的空间更小,使用 SetPixels32 比 SetPixels 造成的 CPU 耗时也更小。所以在效果允许的情况下,我们推荐使用 SetPixels32() 方法来取代 SetPixels()。

6、该类的方法中存在 GameObject.SendMessage 调用


GameObject.SendMessage 用于调用相应 GameObject 上的脚本中的给定名称的函数。该函数会遍历 GameObject 上的所有组件以及组件脚本中的所有函数,这会导致较高的 CPU 开销。所以开发者要减少不必要的 SendMessage 的使用。

7、该类的方法中存在 GetComponentsInChildren 调用/该类的方法中存在 GetComponentsInParent 调用


GetComponentsInChildren 用于获得当前 GameObject 及其子节点的所有给定类型的组件,返回的是一个包含所有符合条件的对象的数组;而 GetComponentsInParent 则是用于获得当前对象及其父节点上的所有给定类型的组件。当然这里我们在说法上忽略了对组件隐藏情况的讨论。

这两者的使用都会涉及到较大范围内的搜索遍历,会挤占 CPU 较大的计算资源,所以开发团队应当尽量减少相关的调用,可以尝试缓存调用的结果,避免使其出现在 Update 这样的频繁调用的函数当中。此外,对于这两个函数,我们建议开发团队使用接受 List类型的引用作为参数的版本,这样就可以避免每次调用都造成堆内存的分配。


8、该类的方法中存在 FindObjectsOfType 调用


如果使用 FindObjectsOfType,它会对场景中的 GameObject 和 Component 进行遍历,并将与目标 Type 类型相同的组件以数组的方式返回。“Find” 类的操作在小型项目当中可能不会有明显的影响,但随着项目体量的增大,场景中物体数量的增加,该操作造成的 CPU 耗时也将变得不容忽视。并且该函数会造成堆内存的分配。所以我们建议尽量避免这样的函数调用,或者通过调用一次,缓存结果的方式减少其带来的对项目性能的影响。

9、该类的方法中存在 Reflection 相关函数的调用


反射(Reflection)是一项用来在代码运行时做绑定的技术。如果代码需要获取的类型、调用的函数等信息是在运行时才能被明确的,那么就需要用到反射。

但运行时绑定就意味着更高的性能开销:项目在调用反射相关的方法时,需要获取类型与函数等信息,并且进行参数校验、错误处理、安全性检查等。这会导致相应的 CPU 计算开销较高,并且容易造成堆内存分配。因此我们建议在游戏项目中,尽可能避免使用反射。


10、该类的方法中存在对 Renderer 进行 Material/Materials 的获取


在 Unity 中,如果对 Renderer 类型调用.material 和.materials,那么 Unity 就会生成新的材质球实例。其主要影响如下:
通过.material,创建材质实例,并修改属性的方式实现多样的渲染效果,时间开销会较高。这里可以参见《使用 MaterialPropertyBlock 来替换 Material 属性操作》

使用相同 Shader,但因为 Material 实例不同的 GameObject,所以无法进行合批,导致 Draw Call 增加,变相造成了 CPU 耗时的增加。

每次对新的 GameObject 的 Renderer 调用.material,都会生成一个新的 Material 实例,且 GameObject 销毁后,Material 实例无法自动销毁,这会对资源管理造成一定的成本,想要处理的话就需要手动调用 “UnloadUnusedAssets” 来卸载,但这样就造成了性能开销;管理不好可能会造成材质球大量冗余甚至泄露,极端情况下甚至会导致过高的内存。

建议通过主动 MaterialPropertyBlock 的方式修改材质属性,或者人为对有限个材质实例进行管理,效果相同的物体通过 sharedMaterial 来共用材质实例。


希望以上这些知识点能在实际的开发过程中为大家带来帮助。需要说明的是,每一项检测规则的阈值都可以由开发团队依据自身项目的实际需求去设置合适的阈值范围,这也是本地资源检测的一大特点。同时,也欢迎大家来使用 UWA 推出的本地资源检测服务,可帮助大家尽早对项目建立科学的美术规范。

万行代码屹立不倒,全靠基础掌握得好!

《场景检测:面片、光影和物理属性》
《场景检测:Audio Listener、RigidBody 和 Prefab 连接》
《场景检测:雾效、Canvas 和碰撞体》
《特效优化 2:效果与性能的博弈》
《特效优化:发现绚丽背后的质朴》
《Prefab 优化:预制体中的各种细节选择》

相关推荐
多线程统计 | GOT Online 新功能上线

性能黑榜相关阅读

《那些年给性能埋过的坑,你跳了吗?》
《那些年给性能埋过的坑,你跳了吗?(第二弹)》
《掌握了这些规则,你已经战胜了 80% 的对手!》


↙↙↙阅读原文可查看相关链接,并与作者交流