刚开始写这篇文章的时候选了一个很土的题目。。。《Unity3D优化全解析》。由于这是一篇临时起意才写的文章,并且陈述的都是既有的事实,于是给本身“文(dou)学(bi)”加工留下的余地就少了不少。但又以为这块是不得不提的一个地方,平时见到不少人对此处也给予了忽略了事,须要时才去网上扒一些只言片语的资料。也恰逢年前,寻思着周末认真写点东西遇到节假日没准也没什么人读,因此索性就写了这篇临时的文章。题目很土,由于用了指向性很明确的“Unity3D”,让人少了遐(瞎)想的空间,同时用了“高大全”这样的构词法,也让匹夫有成为众矢之的的可能。。。因此最后仍是改为了如今各位看到的题目。话很少说,下面就开始正文~正所谓“草蛇灰线,伏脉千里”。那我们首先~~~~~~编程
匹夫印象里遇到的童靴,提Unity3D项目优化则必提DrawCall,这天然没错,但也有很很差影响。由于这会给人一个错误的认识:所谓的优化就是把DrawCall弄的比较低就对了。c#
对优化有这种第一印象的人不在少数,drawcall的确是一个很重要的指标,但绝非所有。为了让各位和匹夫能达成尽量多的共识,匹夫首先介绍一下本文可能会涉及到的几个概念,以后会提出优化所涉及的三大方面:数组
好啦,文中的几个概念提早讲清楚了,其实各位也能看的出来匹夫接下来要说的匹夫关注的优化时须要注意的方面:缓存
因此,这篇文章也会按照CPU---->GPU---->内存的顺序进行。性能优化
上文中说了,drawcall影响的是CPU的效率,并且也是最知名的一个优化点。可是除了drawcall以外,还有哪些因素也会影响到CPU的效率呢?让咱们一一列出暂时能想获得的:ide
前面说过了,DrawCall是CPU调用底层图形接口。好比有上千个物体,每个的渲染都须要去调用一次底层接口,而每一次的调用CPU都须要作不少工做,那么CPU必然不堪重负。可是对于GPU来讲,图形处理的工做量是同样的。因此对DrawCall的优化,主要就是为了尽可能解放CPU在调用图形接口上的开销。因此针对drawcall咱们主要的思路就是每一个物体尽可能减小渲染次数,多个物体最好一块儿渲染。因此,按照这个思路就有了如下几个方案:函数
首先咱们要先理解为什么2个没有使用相同材质的物体即便使用批处理,也没法实现Draw Call数量的降低和性能上的提高。工具
由于被“批处理”的2个物体的网格模型须要使用相同材质的目的,在于其纹理是相同的,这样才能够实现同时渲染的目的。于是保证材质相同,是为了保证被渲染的纹理相同。
所以,为了将2个纹理不一样的材质合二为一,咱们就须要进行上面列出的第二步,将纹理打包成图集。具体到合二为一这种状况,就是将2个纹理合成一个纹理。这样咱们就能够只用一个材质来代替以前的2个材质了。
而Draw Call Batching自己,也还会细分为2种。
看名字,猜使用的情景。
静态?那就是不动的咯。还有呢?额,听上去状态也不会改变,没有“生命”,好比山山石石,楼房校舍啥的。那和什么比较相似呢?嗯,聪明的各位必定以为和场景的属性很像吧!因此咱们的场景彷佛就能够采用这种方式来减小draw call了。
那么写个定义:只要这些物体不移动,而且拥有相同的材质,静态批处理就容许引擎对任意大小的几何物体进行批处理操做来下降描绘调用。
那要如何使用静态批来减小Draw Call呢?你只须要明确指出哪些物体是静止的,而且在游戏中永远不会移动、旋转和缩放。想完成这一步,你只须要在检测器(Inspector)中将Static复选框打勾便可,以下图所示:
至于效果如何呢?
举个例子:新建4个物体,分别是Cube,Sphere, Capsule, Cylinder,它们有不一样的网格模型,可是也有相同的材质(Default-Diffuse)。
首先,咱们不指定它们是static的。Draw Call的次数是4次,如图:
咱们如今将它们4个物体都设为static,在来运行一下:
如图,Draw Call的次数变成了1,而Saved by batching的次数变成了3。
静态批处理的好处不少,其中之一就是与下面要说的动态批处理相比,约束要少不少。因此通常推荐的是draw call的静态批处理来减小draw call的次数。那么接下来,咱们就继续聊聊draw call的动态批处理。
有阴就有阳,有静就有动,因此聊完了静态批处理,确定跟着就要说说动态批处理了。首先要明确一点,Unity3D的draw call动态批处理机制是引擎自动进行的,无需像静态批处理那样手动设置static。咱们举一个动态实例化prefab的例子,若是动态物体共享相同的材质,则引擎会自动对draw call优化,也就是使用批处理。首先,咱们将一个cube作成prefab,而后再实例化500次,看看draw call的数量。
for(int i = 0; i < 500; i++) { GameObject cube; cube = GameObject.Instantiate(prefab) as GameObject; }
draw call的数量:
能够看到draw call的数量为1,而 saved by batching的数量是499。而这个过程当中,咱们除了实例化建立物体以外什么都没作。不错,unity3d引擎为咱们自动处理了这种状况。
可是有不少童靴也遇到这种状况,就是我也是从prefab实例化建立的物体,为什么个人draw call依然很高呢?这就是匹夫上文说的,draw call的动态批处理存在着不少约束。下面匹夫就演示一下,针对cube这样一个简单的物体的建立,若是稍有不慎就会形成draw call飞涨的状况吧。
咱们一样是建立500个物体,不一样的是其中的100个物体,每一个物体的大小都不一样,也就是Scale不一样。
for(int i = 0; i < 500; i++) { GameObject cube; cube = GameObject.Instantiate(prefab) as GameObject; if(i / 100 == 0) { cube.transform.localScale = new Vector3(2 + i, 2 + i, 2 + i); } }
draw call的数量:
咱们看到draw call的数量上升到了101次,而saved by batching的数量也降低到了399。各位看官能够看到,仅仅是一个简单的cube的建立,若是scale不一样,居然也不会去作批处理优化。这仅仅是动态批处理机制的一种约束,那咱们总结一下动态批处理的约束,各位也许也能从中找到为什么动态批处理在本身的项目中不起做用的缘由:
因此,尽可能使用静态的批处理。
曾几什么时候,匹夫在作一个策略类游戏的时候须要在单元格上排兵布阵,而要侦测到哪一个兵站在哪一个格子匹夫选择使用了射线,因为士兵单位不少,并且为了精确每一帧都会执行检测,那时候CPU的负担叫一个惨不忍睹。后来匹夫果断放弃了这种作法,而且对物理组件产生了心理的阴影。
这里匹夫只提2点匹夫感受比较重要的优化措施:
1.设置一个合适的Fixed Timestep。设置的位置如图:
那何谓“合适”呢?首先咱们要搞明白Fixed Timestep和物理组件的关系。物理组件,或者说游戏中模拟各类物理效果的组件,最重要的是什么呢?计算啊。对,须要经过计算才能将真实的物理效果展示在虚拟的游戏中。那么Fixed Timestep这货就是和物理计算有关的啦。因此,若计算的频率过高,天然会影响到CPU的开销。同时,若计算频率达不到游戏设计时的要求,有会影响到功能的实现,因此如何抉择须要各位具体分析,选择一个合适的值。
2.就是不要使用网格碰撞器(mesh collider):为啥?由于实在是太复杂了。网格碰撞器利用一个网格资源并在其上构建碰撞器。对于复杂网状模型上的碰撞检测,它要比应用原型碰撞器精确的多。标记为凸起的(Convex )的网格碰撞器才可以和其余网格碰撞器发生碰撞。各位上网搜一下mesh collider的图片,天然就会明白了。咱们的手机游戏天然无需这种性价比不高的东西。
固然,从性能优化的角度考虑,物理组件能少用仍是少用为好。
在CPU的部分聊GC,感受是否是怪怪的?其实小匹夫不这么以为,虽然GC是用来处理内存的,但的确增长的是CPU的开销。所以它的确能达到释放内存的效果,但代价更加沉重,会加剧CPU的负担,所以对于GC的优化目标就是尽可能少的触发GC。
首先咱们要明确所谓的GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,因此GC也主要是针对Mono的对象来讲的,而它管理的也是Mono的托管堆。 搞清楚这一点,你也就明白了GC不是用来处理引擎的assets(纹理啦,音效啦等等)的内存释放的,由于U3D引擎也有本身的内存堆而不是和Mono一块儿使用所谓的托管堆。
其次咱们要搞清楚什么东西会被分配到托管堆上?不错咯,就是引用类型咯。好比类的实例,字符串,数组等等。而做为int,float,包括结构体struct其实都是值类型,它们会被分配在堆栈上而非堆上。因此咱们关注的对象无外乎就是类实例,字符串,数组这些了。
那么GC何时会触发呢?两种状况:
因此为了达到优化CPU的目的,咱们就不能频繁的触发GC。而上文也说了GC处理的是托管堆,而不是Unity3D引擎的那些资源,因此GC的优化说白了也就是代码的优化。那么匹夫以为有如下几点是须要注意的:
聊到代码这个话题,也许有人会以为匹夫画蛇添足。由于代码质量因人而异,很难像上面提到的几点,有一个明确的评判标准。也是,公写公有理,婆写婆有理。可是匹夫这里要提到的所谓代码质量是基于一个前提的:Unity3D是用C++写的,而咱们的代码是用C#做为脚原本写的,那么问题就来了~脚本和底层的交互开销是否须要考虑呢?也就是说,咱们用Unity3D写游戏的“游戏脚本语言”,也就是C#是由mono运行时托管的。而功能是底层引擎的C++实现的,“游戏脚本”中的功能实现都离不开对底层代码的调用。那么这部分的开销,咱们应该如何优化呢?
2.如上所述,最好不要频繁使用GetComponent,尤为是在循环中。
3.善于使用OnBecameVisible()和OnBecameVisible(),来控制物体的update()函数的执行以减小开销。
4.使用内建的数组,好比用Vector3.zero而不是new Vector(0, 0, 0);
5.对于方法的参数的优化:善于使用ref关键字。值类型的参数,是经过将实参的值复制到形参,来实现按值传递到方法,也就是咱们一般说的按值传递。复制嘛,总会让人感受很笨重。好比Matrix4x4这样比较复杂的值类型,若是直接复制一份新的,反而不如将值类型的引用传递给方法做为参数。
好啦,CPU的部分匹夫以为到此就介绍的差很少了。下面就简单聊聊其实匹夫并非十分熟悉的部分,GPU的优化。
GPU与CPU不一样,因此侧重点天然也不同。GPU的瓶颈主要存在在以下的方面:
那么针对以上4点,其实仔细分析咱们就能够发现,影响的GPU性能的无非就是2大方面,一方面是顶点数量过多,像素计算过于复杂。另外一方面就是GPU的显存带宽。那么针锋相对的两方面举措也就十分明显了。
那么第一个方面的优化也就是减小顶点数量,简化复杂度,具体的举措就总结以下了:
第二个方向呢?压缩图片,减少显存带宽的压力。
这里匹夫要着重介绍一下MipMap究竟是啥。由于有人说过MipMap会占用内存呀,但为什么又会优化显存带宽呢?那就不得不从MipMap是什么开始聊起。一张图其实就能解决这个疑问。
上面是一个mipmap 如何储存的例子,左边的主图伴有一系列逐层缩小的备份小图
是否是很一目了然呢?Mipmap中每个层级的小图都是主图的一个特定比例的缩小细节的复制品。由于存了主图和它的那些缩小的复制品,因此内存占用会比以前大。可是为什么又优化了显存带宽呢?由于能够根据实际状况,选择适合的小图来渲染。因此,虽然会消耗一些内存,可是为了图片渲染的质量(比压缩要好),这种方式也是推荐的。
既然要聊Unity3D运行时候的内存优化,那咱们天然首先要知道Unity3D游戏引擎是如何分配内存的。大概能够分红三大部分:
第3类不是咱们关注的重点,因此接下来咱们会分别来看一下Unity3D内部内存和Mono托管内存,最后还将分析一个官网上Assetbundle的案例来讲明内存的管理。
Unity3D的内部内存都会存放一些什么呢?各位想想,除了用代码来驱动逻辑,一个游戏还须要什么呢?对,各类资源。因此简单总结一下Unity3D内部内存存放的东西吧:
由于咱们的游戏脚本是用C#写的,同时还要跨平台,因此带着一个Mono的托管环境显然必须的。那么Mono的托管内存天然就不得不放到内存的优化范畴中进行考虑。那么咱们所说的Mono托管内存中存放的东西和Unity3D内部内存中存放的东西究竟有何不一样呢?其实Mono的内存分配就是很传统的运行时内存的分配了:
而Mono托管堆中的那些封装的对象,除了在在Mono托管堆上分配封装类实例化以后所须要的内存以外,还会牵扯到其背后对应的游戏引擎内部控件在Unity3D内部内存上的分配。
举一个例子:
一个在.cs脚本中声明的WWW类型的对象www,Mono会在Mono托管堆上为www分配它所须要的内存。同时,这个实例对象背后的所表明的引擎资源所须要的内存也须要被分配。
一个WWW实例背后的资源:
如图:
那么下面就举一个AssetBundle的例子:
如下载Assetbundle为例子,聊一下内存的分配。匹夫从官网的手册上找到了一个使用Assetbundle的情景以下:
IEnumerator DownloadAndCache (){ // Wait for the Caching system to be ready while (!Caching.ready) yield return null; // Load the AssetBundle file from Cache if it exists with the same version or download and store it in the cache using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){ yield return www; //WWW是第1部分 if (www.error != null) throw new Exception("WWW download had an error:" + www.error); AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分 if (AssetName == "") Instantiate(bundle.mainAsset);//实例化是第3部分 else Instantiate(bundle.Load(AssetName)); // Unload the AssetBundles compressed contents to conserve memory bundle.Unload(false); } // memory is freed from the web stream (www.Dispose() gets called implicitly) } }
内存分配的三个部分匹夫已经在代码中标识了出来:
那就分别解析一下:
WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)
AssetBundle bundle = www.assetBundle;
Instantiate(bundle.mainAsset);
最后各位可能看到了官网中的这个例子使用了:
using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){ }
这种using的用法。这种用法其实就是为了在使用完Web Stream以后,将内存释放掉的。由于WWW也继承了idispose的接口,因此可使用using的这种用法。其实至关于最后执行了:
//删除Web Stream www.Dispose();
OK,Web Stream被删除掉了。那还有谁呢?对Assetbundle。那么使用
//删除AssetBundle bundle.Unload(false);
ok,写到这里就先打住啦。写的有点超了。有点赶也有点临时,往后在补充编辑。
这篇文章当时写的时候略显仓促,所以并无特别介绍Unity Profiler工具,也更谈不上用Unity Profiler工具来监测内存的使用状态了。可是使用Unity Profiler工具来监测仍是十分必要的,下面就简单补充一下这方面的知识。
在Profiler工具中提供了两种模式供咱们监测内存的使用状况,即简易模式和详细模式。在简易模式中,咱们能够看到总的内存(total)列出了两列,即Used Total(使用总内存)和Reserved Total(预约总内存)。Used Total和Reserved 均是物理内存,其中Reserved是unity向系统申请的总内存,Unity底层为了避免常常向系统申请开辟内存,开启了较大一块内存做为缓存,即所谓的Reserved内存,而运行时,unity所使用的内存首先是向Reserved中来申请内存,当不使用时也是先向Reserved中释放内存,从而来保证游戏运行的流畅性。通常来讲,Used Total越大,则Reserved Total越大,而当Used Total降下去后,Reserved Total也是会随之降低的(但并不必定与Used Total同步)。
Unity3D的内存从大致上能够分为如下几个部分:
而在简易模式下的监视器最下方,则列出了常见的一些资源以及它们所消耗的内存。
而详细模式则须要点击“Take Sample”按钮来捕获详细的内存使用状况。须要注意的是,因为得到数据须要花费必定的时间,所以咱们没法得到实时的详细内存的使用状况。在详细模式中,咱们能够观察每一个具体资源和游戏对象的内存使用状况。
若是各位看官以为文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~