Unity性能优化-脚本代码优化
Unity性能优化-脚本代码优化
参考文献:https://unity3d.com/cn/learn/tutorials/topics/performance-optimization
脚本代码优化
托管代码和托管运行时简介
在Unity工程的 构建(Build) 过程中,C# 代码首先会被编译成 CIL(Common Intermediate Language),进而根据工程的目标运行平台被编译成相应的 本地代码(Native Code)。
被编译成CIL的代码(即C#),称为 托管代码(Managed Code)。当托管代码被编译成本地代码时,它被集成到 托管运行时(Managed Runtime)。托管运行时将会处理自动内存管理和安全性检查等事项。
引起代码性能低下的原因
- 第一种可能的原因:代码结构欠佳。例如,重复调用无需多次执行的方法。
- 第二种可能的原因:调用其他代码引起的不必要开销。例如,托管代码和引擎代码(C++)之间不必要的调用。
- 第三种可能的原因:调用不需要调用的代码。例如,在玩家离敌人(AI)很远时调用模拟敌人视线的方法。
- 最后的可能的原因:代码过分严谨(过犹不及)。例如,对大量使用复杂AI的代理进行十分细致的模拟。
提升代码效率
- 减少循环的执行次数和循环中的指令数目。
- 将不需要频繁执行的方法从
Update()
中移出。 - 只在变化发生时才去执行代码。例如,不对未变化的量进行反复的输出。
将需要频繁执行且不能通过事件触发的方法改为每隔几帧执行一次。例如,记录帧数(Time.frameCount)并通过模运算跳过某些帧,通过该方式,也能做到让几个方法不在同一帧中执行。
使用缓存存储需要反复获取和使用的值。例如,将GetComponent()方法的返回值存储到变量中,避免反复获取。
根据需求选用正确的数据结构,尤其是在数据驱动的开发模式中。相关教程
使用对象池优化内存使用和降低CPU开销。创建和销毁对象的开销通常要比激活(Active)和取消激活对象的开销更高,尤其是对于那些含有初始化内容的对象。相关教程
避免调用高开销的Unity API
SendMessage()和BroadcastMessage()。在了解项目结构的前提下,SendMessage()和BroadcastMessage()方法使用起来非常灵活而且容易实现,但它使用了反射,而反射会造成更多的CPU开销。在清楚要调用哪个组件的哪个方法时应该通过组件的引用直接调用方法;在不清楚具体要用的组件和方法时,考虑使用 事件 和 委托 替代。
Find()。Find()方法会遍历内存中的每个GameObject和组件,随着项目规模的扩张,它的开销将会越来越大。不要频繁的使用Find()和与其类似的方法,可以考虑在Inspector中设置对对象的引用,或者创建一个专门用于管理需要搜索的对象的引用的脚本。
Transform。设置position和rotation会触发内部的OnTransformChanged事件并传播到所有的子级对象中,对于含有非常对子物体的对象来说,这种操作开销很大,应该 减少对position和rotation的修改。如果某个方法中要分别设置position的xyz,可以创建一个Vector3然后分别将xyz设置到该Vector3中,最后将Vector3设置给position,而不要每次分开设置position的xyz。尝试使用localPosition替代position。localPosition存储在transform中,访问该值时,Unity会直接将其返回,而position在每次访问时都会重新计算,如果要经常获取position,可以将其缓存起来。
Update()。Update()和LateUpdate()等事件方法的每次调用都需要引擎代码与托管代码之间进行通信,还要Unity进行安全检查(GameObject状态是否合法等),即使这些事件方法的方法体是空的,引擎任然会对其进行调用。因此,为避免浪费CPU时间,应该 删除空的事件方法。如果游戏中含有非常多的带有Update()方法的MonoBehavior,应该尝试改变代码结构来减少开销,这有一篇相关的博客。
Vector2和Vector3()。向量的数学运算要比普通的浮点数和整数的数学运算更加复杂,访问向量的某些属性可能会带来隐含的开销,例如magnitude属性(不仅针对向量,例如上文提到的Transform.position)。每次访问magnitude,引擎都会进行开平方运算,虽然单次计算并不会消耗多少时间, 但当数量级足够大时,这种开销将变得明显。可以尝试使用sqrMagnitude(即magnitude的平方)替代magnitude,减少开平方操作。
Camera.main。Camera.main所引起的问题与Find()方法类似,应该避免使用Camera.main并且手动管理对相机的引用。
其他Unity API调用和优化。
对象的某些成员看起来像是个变量,但其实它是个访问器(get,set),除了返回值,它其中还包含了额外的操作,例如数学计算、触发事件或在托管代码中调用引擎代码。上文中提到的Transform中的position、向量中的magnitude和Camera.main等都属于访问器,在编码过程中,应该注意经常访问的数据中是否含有访问器。
这里对Unity的性能优化进行了更多的指导。
仅在有需要时才去运行代码
视截体剔除(Frustum Culling)。仅在对象可见时才执行与其视觉效果相关的代码,可以通过GetComponent().isVisible得知对象是否可见,或者通过事件方法OnBecameInvisible()和OnBecameVisible()来修改对象变得可见/不可见时的代码执行方式。
细节层次(Level of Detail,LOD)。通过LOD技术,为离玩家近的对象渲染高精度的网格和贴图,为离玩家远的对象渲染低精度的网格和贴图。可以在代码中进行类似于LOD的优化,例如,只为离玩家近的AI进行高精度的模拟操作,而远处的AI则不进行复杂的模拟运算。CullingGroup手册介绍了更多关于如何使用LOD的内容。