Unity性能优化概述
Unity3D优化入手方向
优化涉及的三大方面
1. CPU方面。例如,DrawCall调用。DrawCall是CPU对底层图形程序接口的调用,用以在屏幕上绘制内容。
2. GPU方面。Fragment优化,涉及GPU;Batching合并。将批处理之前需要很多次调用(Draw Call)的物体合并,之后只需要调用一次底层图形程序的接口就行。
3. 内存方面。
CPU方面的优化
1. 一些常见的影响CPU效率的项目:
- DrawCalls。
- 物理组件(Physics)。
- GC(用来处理内存)
- 脚本中的代码质量。
对DrawCalls进行优化
1. 对DrawCall的优化,主要就是为了尽量解放CPU在调用图形接口上的开销。所以针对DrawCall主要的思路就是每个物体尽量减少渲染的次数,多个物体最好一起渲染。
2. 优化方案:
- 使用DrawCallBatching。Unity在运行时可以将一些物体进行合并,从而用一个描绘调用来渲染他们。
- 通过将纹理打包成图集尽量减少材质的使用。
- 尽量少使用反光,阴影之类的效果,因为那会使物体多次渲染。
使用DrawCallBatching批处理
1. 使用DrawCallBatching需要保证物体之间的材质相同,这样才能有效降低DrawCall数量。
2. 静态批处理:
- 只要场景中物体不移动,并且拥有相同的材质,静态批处理就允许引擎对任意大小的几何物体进行批处理操作来降低绘制调用。
-
如果想要使用静态批处理,需要明确指出哪些物体是静止的,并且在游戏中永远不会移动,旋转和缩放。可以在“Inspector”视图中勾选“Static"复选框即可。
- 静态批处理的好处很多,其中之一就是与动态批处理相比,约束要少得多。一般推荐使用DrawCall的静态批处理来减少DrawCall的次数。
3. 动态批处理。
- Unity中DrawCall动态批处理机制是引擎自动进行的,无需像静态批处理那样手动设置Static。
- 例如动态实例化Prefab的例子,如果动态物体共享相同的材质,则引擎会自动对DrawCall优化,进行批处理。
- 批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。
- 如果着色器使用顶点位置,发现和UV值3种属性,那么只能批处理300顶点以下的物体;如果着色器需要使用顶点位置,法线,UV0,UV1和切向量,那只能批处理180顶点以下的物体。
- 不要使用缩放。不同缩放值得两个物体将不会进行动态批处理。统一缩放的物体不会与非统一缩放的物体进行批处理。
- 使用不同材质的实例化物体将会导致批处理失败。
- 拥有lightmap的物体含有额外(隐藏)的材质属性,例如lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非它们指向lightmap的同一部分)
- 预设体的实例化会自动地使用相同的网格模型和材质。
- 多通道的Shader会妨碍批处理操作。
对物理组件的优化
从性能优化的角度考虑,物理组件能少用还是少用为好。
设置一个合适的FixedTimestep
1. FixedTimestep这个指标和物理计算相关。如果计算频率太高,自然会影响到CPU的开销。同时,若计算评率达不到游戏设计时的要求,又会影响到功能的实现,所以如何抉择需要开发人员具体分析,选择一个合适的值。
尽量不要使用网格碰撞器(Mesh Collider)
1. Mesh Collider非常复杂,利用一个网格资源并在其上构建碰撞器。对于复杂网状模型上的碰撞检测,它要比应用原型碰撞器精确得多。标记为凸起的网格碰撞器才能够和其他网格碰撞器发生碰撞。
2. 手机游戏无须这种性价比不高的东西。
处理内存,却让CPU受伤的GC
GC概述
1. 虽然GC时用来处理内存的,但的确增加的时CPU的开销。因此他的确能达到释放内存的效果,但代价更加沉重,会加重CPU的负担。因此对于GC的优化目标就是尽量少的触发GC。
2. GC是Mono运行时的机制,而非Unity游戏引擎的机制。所以GC主要针对Mono的对象来说的,而不是用来处理引擎的assets(纹理,音效)的内存释放的,它管理的也是Mono的托管堆。
3. 分配到托管堆上的是引用类型。比如类的实例,字符串,数组等。而作为int类型,float类型,包括结构体其实都是值类型,他们会被分配到堆栈上而非堆上。
4. GC的触发情况:
- 堆的内存不足时,会自动调用GC。
- 作为编程人员,也可以手动调用GC。
优化方案
1. 字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程,而之前旧字符串就成为了垃圾;而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧字符串的空间就会被GC当做垃圾回收。
2. 尽量不要使用foreach语句,而使用for语句。foreach语句其实会涉及到迭代器的使用,而据说每一次循环所产生的迭代器会带来24Bytes的垃圾。
3. 不要直接访问GameObject的Tag属性。因为访问物体的tag属性会在堆上额外的分配空间。不要使用诸如if(go.tag == "human"),最好换成if(go.CompareTag("human"))。
4. 使用“池”以实现空间的重复利用。
5. 最好不用LINQ的命令,因为他们会分配临时的空间,同样也是GC收集的目标。而且它有可能在某些情况下不能很好地进行AOT编译。
对代码质量进行优化
1. 获取对象,组件等应该只访问一次并保留引用。下图为某个试验中对比方法获取属性的时间。
2. 最好不要频繁地使用GetComponent,尤其是在循环中。
3. 使用内建的数组,例如用Vector3.zero,而不是new Vector(0, 0, 0)。
4. 对于方法的参数的优化,善于使用ref关键字。
5. 善于使用OnVecameVisible()和OnBecameInvisible()来控制物体的update()函数的执行以减少开销。
GPU方面优化
1. GPU的瓶颈主要存在以下4个方面:
- 填充率。图形处理单元每秒渲染的像素数量。
- 像素的复杂度。比如动态阴影,光照,复杂的Shader等。
- 几何体的复杂度(顶点数量)。
- GPU的显存带宽。
2. 据上可知:影响GPU性能的无法就是两大方面,一方面是顶点数量过多,像素计算过于复杂;另一方面就是GPU的显存带宽。
- 减少顶点数量,简化计算复杂度。
- 压缩图片,适应显存带宽。
减少绘制的数目
1. 保持材质的数目尽可能少,这使得Unity更容易进行批处理。
2. 使用纹理图集(一张大贴图包含了很多子贴图)来替代一系列单独的小贴图。他们可以更快地被加载,具有很少的状态转换,而且批处理更友好。
3. 如果使用了纹理图集和共享材质,使用Renderer.sharedMaterial来代替Renderer.material。
4. 使用光照纹理(lightmap)而非实时灯光。
5. 使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略。
6. 遮挡剔除。
7. 使用mobile版的shader,因为简单。
优化显存带宽
1. OepnGL ES2.0使用ETC1格式压缩等,或者在OpenGL ES3.0使用ETC2格式压缩图片。
2. 使用Mipmap。
内存的优化
1. Unity中主要包含三类内存:
- Unity内部内存。
- Mono托管内存。
- 若干我们自己引入的DLL或者第三方DLL所需要的内存。
Unity内部内存
1. Unity内部内存存放的内容:
- 资源:纹理,网格,音频。
- GameObject和各种组件。
- 引擎内部逻辑需要的内存:渲染器,物理系统,例子系统等。
Mono的托管内存
1. Mono的内存分配就是很传统的运行时内存分配。
- 值类型:int型,float型,结构体struct,bool之类的。他们都放在堆栈上(注意不是堆,所以不涉及GC)。
- 引用类型:可以狭义地理解为各种类的实例。由于是在堆上分配,会涉及到GC。
2. Mono托管堆中的那些封装的对象,除了在Mono托管堆上分配封装类实例化之后所需要的内存之外,还会牵扯到其背后对应的游戏引擎内部控件在Unity3D的内部内存上的分配。