前言
Unity的项目优化已经是老生常谈,很多人在项目完成之后,即便创意新颖,也会觉得差强人意,原因就在于没有做详细的项目优化。众所周知,Unity是一个综合性的3D开发引擎,其中包含图像渲染,逻辑处理,数据存储,发布测试等等各方面的内容。因此Unity各个方面都存在的待优化的内容,也可以说项目优化是项目开发中必不可少的一项工作。本篇文章会从项目的各个方面分析Unity待优化的内容,并给出优化方案,全面优化你的项目。优化项目无非是减轻系统的功耗负担,故下面从CPU、GPU、内存三方面的优化来讲解。
CPU优化
Draw Calls
Draw Call Batching(批处理)
两个或多个纹理相同或材质相同的网格模型可以批量处理他们的材质,这样就可以将多个模型的材质DrawCall合并为一个,从而达到减少DrawCall的目的。批处理是系统工作范畴,我们只需要选择即可,批处理又分为静态批处理和动态批处理。
Static Batching 静态批处理
场景中有很多游戏对象,其中静态对象(Inspector勾选Static的)可以通过静态批处理来优化DrawCall。
下面我们通过具体实例来验证:
在场景中创建多个游戏对象Cube,Sphere, Capsule, Cylinder,默认为静态。此时运行游戏,DrawCall如下。
Dynamic Batching 动态批处理
上面的静态批处理需要给对象设置成静态,而动态批处理,则不需要,非静态的对象,系统自动做批处理。
同样,我们举例说明,通过预设体创建25个cube。
for (int y = 0; y < 5; y++) { for (int x = 0; x < 5; x++) { transform.position = new Vector3(x*3, y*3, 10); Instantiate(cu, transform.position, transform.rotation); } }
- 批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。
如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你>- 只能批处理180顶点以下的物体。- 不要使用缩放。分别拥有缩放大小(1,1,1) 和(2,2,2)的两个物体将不会进行批处理。
- 统一缩放的物体不会与非统一缩放的物体进行批处理。
- 使用缩放尺度(1,1,1) 和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1) 和(1,3,1)的两个物体将可以进行批处理。
- 使用不同材质的实例化物体(instance)将会导致批处理失败。
拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。- 多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。
- 预设体的实例会自动地使用相同的网格模型和材质。
打包图集
每个材质/纹理的渲染一定是会产生DrawCall的,这个DrawCall只能通过打包图集来进行优化
图集一般遵循几个规则:
从功能角度进行划分,例如UI可以划分为公共部分,以及每个具体的界面,功能上,显示上密切相关的图片打包到一起
不要一股脑把所有东西打包到一个图集里,特别是那些不可能同时出现的东西,它们就不应该在一个图集里,这样的图集意义不大,减少不了DrawCall,并且一个你不需要显示的图片,会一直占用你的内存,这让我非常不爽
注意控制图集的大小,不要让图集太大,一个超级大图集的DrawCall消耗或许顶的上十几个小图集的消耗
字符图集,在使用BMFont或者其他工具生成图片字的时候,我们往往是直接导入一大串文字,然后直接生成图片,但实际上这上面的操作也有优化空间,例如BMFont生成的图片大小,是可以设置的,有两个规则,一个规则是导出的图片尽量小,另一个是导出的图片尽量少,默认的大小应该是512x512,假设你生成的图片256x256就可以容纳,那么多做一个操作你可以节省这么多空间,另外当你输入多几个字,就导致增加一张图片时,例如1024变成2048,那么你可以考虑使用3张512的图片,这样也会节省空间
经过精心划分的图集在加上精心规划的渲染顺序,DrawCall会有一个质的优化
物理组件
物理组件是我们在游戏开发中经常用到的组件,比起设计超级复杂的伤害计算算法,一个Trigger就能解决很多问题,还有那可以模拟一切物理效果的Rigidbody。但如果物理组件使用过多,计算量过大,也会造成CPU过载。对于物理组件的优化,有以下两点。
- 设置一个合适的Fixed Timestep。
我们都知道在计算物理逻辑的时候通常会将代码放到FixedUpdate里面,然而FixedUpdate的执行频率,就由Fixed Timestep决定,并不是所有的游戏中物理计算都需要0.02秒执行一次。因此这个值,可以针对项目慢慢调试,设置出一个比较合适的值,这样即完成了物理计算,也可以减轻CPU的负担。所以,想想自己上高中物理的时候做的物理大题,让CPU少做几道,确实可以轻松很多。换位思考也可以这样对吧,换到CPU的角度考虑。
GC(Garbage Collection垃圾回收)
GC是用来处理内存的,为什么会影响到CPU的开销呢?因为GC是CPU调度的。大量的调用GC确实可以回收内存,但如果内存占用量不是很大的情况下,调用GC的性价比就很低,因为GC对CPU的开销所造成的代价更大。所以优化GC,就是减少对GC的调用。
那么GC什么时候会触发呢?两种情况:
首先当然是我们的堆的内存不足时,会自动调用GC。
其次呢,作为编程人员,我们自己也可以手动的调用GC。
所以为了达到优化CPU的目的,我们就不能频繁的触发GC。
而上文也说了GC处理的是托管堆,而不是Unity3D引擎的那些资源,
所以GC的优化说白了也就是代码的优化。
代码质量
以物体的Transform组件为例,我们应该只访问一次,之后就将它的引用保留,而非每次使用都去访问。这里有人做过一个小实验,就是对比通过方法GetComponent()获取Transform组件, 通过MonoBehavor的transform属性去取,以及保留引用之后再去访问所需要的时间:
1.GetComponent = 619ms
2.Monobehaviour = 60ms
3.CachedMB = 8ms
4.Manual Cache = 3ms
所以最好不要频繁使用GetComponent,尤其是在循环中。
善于使用OnBecameVisible()和OnBecameVisible(),来控制物体的update()函数的执行以减少开销。
使用内建的数组,比如用Vector3.zero而不是new Vector(0, 0, 0);
对于方法的参数的优化:善于使用ref关键字。值类型的参数,是通过将实参的值复制到形参,来实现按值传递到方法,也就是我们通常说的按值传递。复制嘛,总会让人感觉很笨重。比如Matrix4x4这样比较复杂的值类型,如果直接复制一份新的,反而不如将值类型的引用传递给方法作为参数。
GPU优化
GPU主要处理图像渲染,与CPU不同,侧重点自然也不同。GPU需要优化的点主要有以下几点:
- 填充率,可以简单的理解为图形处理单元每秒渲染的像素数量。
- 像素的复杂度,比如动态阴影,光照,复杂的shader等等
- 几何体的复杂度(顶点数量)
- GPU的显存带宽
减少绘制的数目
减少顶点数量,简化复杂度,举措如下。
- 保持材质的数目尽可能少。这使得Unity更容易进行批处理。
- 使用纹理图集(一张大贴图里包含了很多子贴图)来代替一系列单独的小贴图。它们可以更快地被加载,具有很少的状态转换,而且批处理更友好。
- 如果使用了纹理图集和共享材质,使用Renderer.sharedMaterial 来代替Renderer.material 。
- 使用光照纹理(lightmap)而非实时灯光。
- 使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略。
- 遮挡剔除(Occlusion culling)
- 使用mobile版的shader,简单。
优化显存带宽
压缩图片,减小显存带宽的压力
- OpenGL ES 2.0使用ETC1格式压缩等等,在打包设置那里都有。
- 使用MipMap。
Mipmap中每一个层级的小图都是主图的一个特定比例的缩小细节的复制品。因为存了主图和它的那些缩小的复制品,所以内存占用会比之前大。但是为何又优化了显存带宽呢?因为可以根据实际情况,选择适合的小图来渲染。所以,虽然会消耗一些内存,但是为了图片渲染的质量(比压缩要好),这种方式也是推荐的。
内存优化
- Unity3D内部的内存
- Mono的托管内存
- 若干我们自己引入的DLL或者第三方DLL所需要的内存。
Unity内部内存
Unity3D的内部内存都会存放一些什么呢?
- 资源:纹理、网格、音频等等
- GameObject和各种组件。
- 引擎内部逻辑需要的内存:渲染器,物理系统,粒子系统等等
mono托管内存
因为我们的游戏脚本是用C#写的,同时还要跨平台,所以带着一个Mono的托管环境显然必须的。那么Mono的托管内存自然就不得不放到内存的优化范畴中进行考虑。那么我们所说的Mono托管内存中存放的东西和Unity3D内部内存中存放的东西究竟有何不同呢?其实Mono的内存分配就是很传统的运行时内存的分配了:
- 值类型:int型啦,float型啦,结构体struct啦,bool啦之类的。它们都存放在堆栈上(注意额,不是堆所以不涉及GC)。
- 引用类型:其实可以狭义的理解为各种类的实例。比如游戏脚本中对游戏引擎各种控件的封装。其实很好理解,C#中肯定要有对应的类去对应游戏引擎中的控件。那么这部分就是C#中的封装。由于是在堆上分配,所以会涉及到GC。
结束语
项目优化至关重要,因为现在的产品大同小异的很多,游戏也是如此,无非就是那么几种类型,在保持创新精神的基础上,还要着重关注的就是用户体验。用户体验这个词不是新词了,所以怎样提高用户体验呢,流畅、明朗、便捷。当然,这些都取决与项目优化。所以,是你的产品就好好优化它,因为每个产品都可以变得更好。