图集
什么是图集?
首先,Unity会为场景中的每个纹理发出一次绘制调用,即DrawCall(CPU发出命令让GUP开始渲染)
一个个纹理进行绘制,那性能必须大消耗,所以,把多个纹理合并成一个,那么DrawCall也只会有一个,这就是合并纹理,即图集。
(补充一下Unity的Batches,即DrawCall的封装,代表每帧有多少DrawCall)
关于图集的渲染
Unity的Canvas会按照深度Depth从下向上渲染,所以最下面的UI一般会渲染在最前面。 但是如果UI之间产生了交叉重叠的情况,图集的合批会在不同图集重叠的地方被打断(如果两个相同图集之间穿插了另一个不同的图集,则合批会被打断)
合批处理
如果场景中存在大量的琐碎的物体,那么 CPU 在做视锥体剔除 和 收集这些物体的渲染信息时的耗时就会增加。对于合批通常使用这三种方法:建模时就手动合并网格、静态合批、动态合批。
建模合批
这里不建议在建模时就把各种物体的网格合并为一个,因为这样做会产生一个巨大的且有很多顶点数的物体,由于其体积过大,CPU无法在视锥体剔除阶段将其剔除,那么就会有过多的网格信息需要从CPU传到GPU,此时容易引发带宽问题。这里推荐的做法是建模时要思考个体与整体的可见性问题,例如:对于书架里的书,当里面一本书可见时,通常一组书都可见,那么就可以把一组书的网格合并为一个,甚至当一本书可见时通常整个书架都可见,所以书和书架的网格都可以在建模时就合并在一起。
静态合批
对于静态不会动的物体,在Unity中我们可以将其标记为 Static,即静态物体,那么在游戏运行时,Unity就会对使用相同材质的静态物体的网格进行合并。
本质上是牺牲内存换取性能,VBO更大了:把静态物体变换到世界空间下,然后为它们构建一个更大的顶点和索引缓存。对于使用了同材质的物体统一绘制,对于使用了不同材质的物体,静态批处理可以减少这些 draw call 之间的状态切换;处理平行光的BasePass部分会被静态批处理,但是处理其他光源的额外Pass不会。
动态合批
些顶点数较小的网格,在运行时Unity会动态的对它们的网格进行合并,因此即使这些物体是动态的也没有关系。
动态合批对网格的顶点数有限制。
动态合批通常的应用场景有两个:粒子系统 和 UI 。
UI的动态合批
- 不同 Canvas 下的 UI 无法动态合批
- 不同层级下的UI无法动态合批(这里的层级可以理解为 一个 UI 其下面垫了几层的UI,这一块可以根据 FrameDebuger 的合批结果来进行调整)
- alpha = 0,depth = -1 的 UI 无法进行合批
- depth = x 的 UI,只能与 depth ≤ x 的 UI 进行合批
- 动态合批本身也是有代价的。 当调用 Canvas.BuildBatch 或者,UI 元素产生变化时 就会重新进行 动态合批。 因此我们应该尽量把经常产生变化的 UI 和 不怎么产生变化的静态UI 分别放在两个 Canvas 下,这样可以降低 UI 动态合批的复杂度,加快合批运行的时间。
LOD
LOD允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能Unity中使用LOD Group组件,对同一个物体用不同细节程度的模型赋给组件中的不同等级, Unity 就会自动判断当前位置上需要使用哪个等级的模型。
Occlusion Culling(遮挡剔除)
遮挡剔除的原理就是当一个物体被其他物体遮挡住,不在摄像机的可视范围内时不对其进行渲染,可以将Inspector面板中的static下拉菜单中勾选Occlusion Static 和 Occludee Static。
减少渲染对象的渲染次数
Lightmap(光照贴图)
Unity灯光默认是实时光照,也就是说物体在灯光下不同位置会产生不同灯光效果,由于动态光源在实时光照下会友大量的Setpass Calls,为了减小Setpass Calls,我们可以烘焙灯光效果,Unity会为我们生成光照贴图,这样大大减少了Setpass Calls。
阴影
在unity中,阴影相关的优化我们可以在质量设置中进行,通过设置调整阴影的质量。
代码
Unity API
SendMessage:用反射,费性能,低效率,垃圾玩意,观察者模式取而代之。
Extern Call Caching:把transform、gameobject之类的属性访问器进行缓存使用,指的就是外部调用缓存,我们把一些常用或者频繁使用到的外部组件以全局变量或者局部变量缓存起来可以减少外部调用的次数。
FindObjects:
FindObjectsOfType比FindGameObjectsWithTag更耗时,是最耗性能以及效率最低的一种。
Transform.Find():用这个方法查找物体时,根节点一定要处于“显示”状态。
GameObject.Find():使用简单,不会因为重名报错,但是不能查找被隐藏的物体,全局查找,所以效率低,很耗性能。Physics.OverlapSphere VS Physics.OverlapSphereNonAlloc
Physics.OverlapSphere: 计算并存储接触球体或位于球体内部的碰撞体,会产生GC Alloc。
Physics.OverlapSphereNonAlloc: 计算与球体接触或位于球体内部的碰撞体,并将它们存储到提供的缓冲区中,耗时比在OverlapSphere长。
设计
GameObject的SpawnPool应支持“移出屏幕”功能
GameObject(比如特效)可能会被频繁的在“使用中”、“不使用”的状态间切换。我们的SpawnPool不应过快地把“刚刚不使用”的GameObject立刻Deactivate掉,否则会引起不必要的Deactivate/Activate的性能消耗。应有一个“从热变冷”的过程:“刚刚不使用”只是移出屏幕;只有“不使用一段时间”的GameObject,才会得以Deactivate。可能的实现方式如下Transform的孩子不应过多
当Transform包含不该有的孩子Transform或其他组件时,为该Transform进行position、rotation赋值,会引起消耗,特别是包含粒子系统的时候。应减少粒子系统的Play()的调用次数
每次调用ParticleSystem.Play()都会有消耗,如果粒子系统本身没有明显“前摇”阶段,应先检查ParticleSystem.isPlaying。应减少每帧Material.GetXX()/Material.SetXX()的次数
每次调用Material.GetXX()或Material.SetXX()都会有消耗,应减少调用该API的频率。比如使用C#对象变量来记录Material的变量状态,从而规避Material.GetXX();
在Shader里把多个uniform half变量合并为uniform half 4,从而把4个Material.SetXX()调用合并为1个Material.SetXX()。应使用支持Conditional的日志输出机制
简单使用 Debug.Log(ToString() + “hello “ + “world”); 其实参会造成CPU消耗及GC。使用支持Conditional的日志输出机制,则无此问题,只需在构建时,取消对应的编译参数即可。减少GetComponent()的频率
GetComponent()会有一定的GC产生,有少量的CPU消耗。减少UnityEngine.Object的null比较
因为Unity overwrite掉了Object.Equals(),《CUSTOM == OPERATOR, SHOULD WE KEEP IT?》也说过unityEngineObject==null事实上和GetComponent()的消耗类似,都涉及到Engine层面的机制调用,所以UnityEngine.Object的null比较,都会有少许的性能消耗。对于基础功能、调用栈叶子节点逻辑、高频功能,我们应少null比较,使用assertion来处理。只有在调用栈根节点逻辑,有必要的时候,才进行null比较。减少不必要的Transform.position/rotation等访问
每次访问Transform.position/rotation都有相应的消耗。应能cache就cache其返回结果。减少创建C#堆内存对象
建议使用成员变量,或者Pool来规避高频创建C#堆内存对象的创建。而且堆内存对象创建本身就是个相对较慢的过程。应为struct对象重载所有object函数
为了普适性,C#的struct的默认Equals()、GetHashCode()和ToString()都是较慢实现,甚至涉及反射。用户自定义的struct,都应重载上述3个函数。尽量用Queue/Stack来代替List
我们会习惯用List来实现数据集合的需求。但好一些情况下,我们事实上是不需对其进行随机访问,而仅仅是“增加”、“删除”操作。此时,我们应该使用增删复杂度都是O(1)的Queue或者Stack。注意容器的初始化capacity
Capacity增长时,除了O(n)的复杂度,也有GC消耗。尽量为类或函数声明为sealed
IL2CPP就sealed的类或函数会有优化,变虚函数调用为直接函数调用。减少Dictionary的冗余访问
myDictionary.Contains(oneKey) ==> myDictionary.TryGetValue(oneKey, out myValue)
Profiler工具
长期性能数据监控工具
长期性能数据监控工具会至少每天都对游戏单局、或游戏资源进行自动化性能测试,并上报结果到服务器。能从“整体”去对比不同时段、不同版本间的性能差别。Unity Profiler
Unity Profiler能定量地找到C#的GC Alloc问题;其Timeline视图也能从地整体(但不太定量)找到CPU瓶颈。XCode
XCode的GPU Report视图能从整体(但不太定量)找到游戏的瓶颈阶段。当Frame Time中CPU大于GPU时,表示CPU是瓶颈,否则表示GPU是瓶颈;当Utilization中的TILER比RENDERER高时,表示顶点处理是GPU的瓶颈,否则表示像素处理是GPU的瓶颈。帧率受限于瓶颈,应优先优化瓶颈阶段,非瓶颈阶段优化得再快都无法提高帧率。
Reference
[1] Unity性能优化 -劳烦叫我小小泽
[2] Unity游戏项目性能优化总结 -DonaldW
[3] Unity渲染性能优化—经验总结 白菊花瓣丶