本文共 8112 字,大约阅读时间需要 27 分钟。
从DrawCall 到GC
一:首先优化从哪里着手?
1.drawcall 是啥?就是CPU对底层图形程序(如OpenGL ES)接口的调用,以在屏幕上画出东西。
2.fragment是啥?经常有人说vf啥的,vertex我们都知道是顶点,那fragment呢?像素是构成数码影像 的基本单元,而fragment是有可能成为像素的东西,意思就是不一定会被画出来的潜在像素。涉及 GPU.
3.batching是啥?批处理之前需要很多次调用(drawcall)的物体合并,之后只需调用一次底层图形程序 的接口就行。
4.内存分配:除了unity自身,还有mono,还有托管,还有引入的dll。
二:影响CPU效率的因素。
1.DrawCall。
2.物理组件(Physics).
3.GC
4.代码质量
DrawCall是CPU调用底层图形接口。比如有上千个物体,每一个的渲染都需要去调用一次底层接口,而每一次的调用CPU都需要做很多工作,
那么CPU必然不堪重负。但是对于GPU来说,图形处理的工作量是一样的。所以对DrawCall的优化,主要就是为了尽量解放CPU在调用图形接口上的
开销。所以针对drawcall我们主要的思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染。
动态批处理的约束:
所以,尽量使用静态的批处理。
物理组件:
1.设置一个合适的Fixed Timestep。设置的位置如图:
那何谓“合适”呢?首先我们要搞明白Fixed Timestep和物理组件的关系。物理组件,或者说游戏中模拟各种物理效果的组件,最重要的是什么呢?计算啊。对,需要通过计算才能将真实的物理效果展现在虚拟的游戏中。那么Fixed Timestep这货就是和物理计算有关的啦。所以,若计算的频率太高,自然会影响到CPU的开销。同时,若计算频率达不到游戏设计时的要求,有会影响到功能的实现,所以如何抉择需要各位具体分析,选择一个合适的值。
2.就是不要使用网格碰撞器(mesh collider):为啥?因为实在是太复杂了。网格碰撞器利用一个网格资源并在其上构建碰撞器。对于复杂网状模型上的碰撞检测,它要比应用原型碰撞器精确的多。标记为凸起的(Convex )的网格碰撞器才能够和其他网格碰撞器发生碰撞。各位上网搜一下mesh collider的图片,自然就会明白了。我们的自然无需这种性价比不高的东西。
当然,从性能优化的角度考虑,物理组件能少用还是少用为好。
处理内存,却让CPU受伤的GC
在CPU的部分聊GC,感觉是不是怪怪的?其实小匹夫不这么觉得,虽然GC是用来处理内存的,但的确增加的是CPU的开销。因此它的确能达到释放内存的效果,但代价更加沉重,会加重CPU的负担,因此对于GC的优化目标就是尽量少的触发GC。
首先我们要明确所谓的GC是Mono运行时的机制,而非引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 搞清楚这一点,你也就明白了GC不是用来处理引擎的assets(纹理啦,音效啦等等)的内存释放的,因为引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。
其次我们要搞清楚什么东西会被分配到托管堆上?不错咯,就是引用类型咯。比如类的实例,字符串,数组等等。而作为int,float,包括结构体struct其实都是值类型,它们会被分配在堆栈上而非堆上。所以我们关注的对象无外乎就是类实例,字符串,数组这些了。
那么GC什么时候会触发呢?两种情况:
所以为了达到优化CPU的目的,我们就不能频繁的触发GC。而上文也说了GC处理的是托管堆,而不是Unity3D引擎的那些资源,所以GC的优化说白了也就是代码的优化。那么匹夫觉得有以下几点是需要注意的:
代码?脚本?
聊到代码这个,也许有人会觉得匹夫多此一举。因为代码质量因人而异,很难像上面提到的几点,有一个明确的评判标准。也是,公写公有理,婆写婆有理。但是匹夫这里要提到的所谓代码质量是基于一个前提的:Unity3D是用C++写的,而我们的代码是用C#作为脚本来写的,那么问题就来了~脚本和底层的交互开销是否需要考虑呢?也就是说,我们用Unity3D写游戏的“游戏脚本语言”,也就是C#是由mono运行时托管的。而功能是底层引擎的C++实现的,“游戏脚本”中的功能实现都离不开对底层代码的调用。那么这部分的开销,我们应该如何优化呢?
1.以物体的Transform组件为例,我们应该只访问一次,之后就将它的引用保留,而非每次使用都去访问。这里有人做过一个小实验,就是对比通过方法GetComponent<Transform>()获取Transform组件, 通过MonoBehavor的transform属性去取,以及保留引用之后再去访问所需要的时间:
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 如何储存的例子,左边的主图伴有一系列逐层缩小的备份小图
是不是很一目了然呢?Mipmap中每一个层级的小图都是主图的一个特定比例的缩小细节的复制品。因为存了主图和它的那些缩小的复制品,所以内存占用会比之前大。但是为何又优化了显存带宽呢?因为可以根据实际情况,选择适合的小图来渲染。所以,虽然会消耗一些内存,但是为了图片渲染的质量(比压缩要好),这种方式也是推荐的。
既然要聊Unity3D运行时候的内存优化,那我们自然首先要知道Unity3D是如何分配内存的。大概可以分成三大部分:
第3类不是我们关注的重点,所以接下来我们会分别来看一下Unity3D内部内存和Mono托管内存,最后还将分析一个官网上Assetbundle的案例来说明内存的管理。
Unity3D内部内存
Unity3D的内部内存都会存放一些什么呢?各位想一想,除了用代码来驱动逻辑,一个游戏还需要什么呢?对,各种资源。所以简单总结一下Unity3D内部内存存放的东西吧:
Mono托管内存
因为我们的游戏脚本是用C#写的,同时还要跨平台,所以带着一个Mono的托管环境显然必须的。那么Mono的托管内存自然就不得不放到内存的优化范畴中进行考虑。那么我们所说的Mono托管内存中存放的东西和Unity3D内部内存中存放的东西究竟有何不同呢?其实Mono的内存分配就是很传统的运行时内存的分配了:
而Mono托管堆中的那些封装的对象,除了在在Mono托管堆上分配封装类实例化之后所需要的内存之外,还会牵扯到其背后对应的游戏引擎内部控件在Unity3D内部内存上的分配。
举一个例子:
一个在.cs脚本中声明的WWW类型的对象www,Mono会在Mono托管堆上为www分配它所需要的内存。同时,这个实例对象背后的所代表的引擎资源所需要的内存也需要被分配。
一个WWW实例背后的资源:
如图:
那么下面就举一个AssetBundle的例子:
Assetbundle的内存处理
以下载Assetbundle为例子,聊一下内存的分配。匹夫从官网的手册上找到了一个使用Assetbundle的情景如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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) } } |
内存分配的三个部分匹夫已经在代码中标识了出来:
那就分别解析一下:
1 | WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version) |
1 | AssetBundle bundle = www.assetBundle; |
1 | Instantiate(bundle.mainAsset); |
通过AssetBundle获取资源,实例化对象
最后各位可能看到了官网中的这个例子使用了:
1 2 | using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){ } |
这种using的用法。这种用法其实就是为了在使用完Web Stream之后,将内存释放掉的。因为WWW也继承了idispose的接口,所以可以使用using的这种用法。其实相当于最后执行了:
1 2 | //删除Web Stream www.Dispose(); |
OK,Web Stream被删除掉了。那还有谁呢?对Assetbundle。那么使用
1 2 | //删除AssetBundle bundle.Unload( false ); |
======这里讲讲从渲染优化,包体大小,再到内存,深入分析与解释如何进行游戏优化。========
一.渲染级别。
(1)NGUI部分:
它对动态移动、旋转、缩放GUI支持的是比较差的,所以我们尽量不要把过多的移动旋转缩放部分写在GUI中。
但,很多情况下是避免不了的,如:大量的伤害数字,物品掉落,图标的移动和旋转等。
为了不让GUI去控制这些渲染物体,一小部分用3D面片代替,大部分用程序去生成面片渲染,脱离了GUI的控制。另外,在那些静态的GUI中,使用静态物体优化的属性,加上排除不必要的GUI设置,使得GUI部分效率足够高。
(2)3D部分:
2.1:特效是对画面效果最有影响的部分,尽量少使用粒子或者将粒子的数量减少到最小。
2.2:尽量减少灯光的使用,而使用烘培后的图代替。但烘培贴图多了内存就爆了,要谨慎。
2.3:使用静态批处理,将那些不动的物体设置为静态,以降低CPU。但,unity说静态批处理会增加 更多内存,要谨慎。
2.4:尽可能的将多个物体使用同一个材质球,可以降低drawcall。
2.5:将多个不动的,使用相同材质球的物体合并mesh,更有效降低drawcall。
2.6:尽量关闭阴影渲染。会降低不少drawcall,减少对阴影的计算。
(3)2D部分:
3.1:将同一页面或同一类型的ui合并成一个图集,减少渲染时的drawcall.
3.2:将需要alpha和不需要alpha的图拆分开来做图集。不需要alpha的图集可以大幅度的做压缩处 理。
3.3:在以上基础上,尽量将图集做压缩处理,只有那些需要高精度的图才不做压缩,可以大幅度减 少内存占用。
二:减少占用空间大小。
1.使用动态资源加载方式,WWW方式。在导出资源时使用 BuildAssetBundleOptions.DisableWriteTypeTree的打包方式,减少包的大小。
2.对每张Textrue,都设置对图片进行压缩。
3.让美术减少模型面片数,并在fbx模式设置中,设置对动画进行压缩。
4.让美术减少动画帧数,并在动画设置中,设置对动画进行压缩。
三:内存。
1。最基本的就是对需要的资源进行加载,使用完毕后,释放。
2。GUI部分在GUIManager管理类中增加对GUI进行定时检查的部分,对不展示的GUI资源进行释 放。对于一些ui出现时的释放资源卡顿问题,可以在ui出现后1秒再释放资源。
3。有时一次加载资源过多引起内存膨胀、IO过慢导致崩溃,可选择资源异步加载。
4.内存释放。
转载地址:http://lpmwb.baihongyu.com/