Unity性能优化

浅谈Unity常用性能优化

因为做项目用到了一些也查了很多资料,所以想记录一下自己的工作经验,希望跟大家分享一下,大佬勿喷。
在我个人理解,性能优化就是找平衡,内存,cpu,渲染之间的一种平衡。优化一定不能盲目的优化,肯定是有方向的,有目的性的优化,否则肯定会出引发出别的问题。优化一定要找到优化的瓶颈在哪,具体是cpu,内存,还是渲染。然后让他们三者之间有一些取舍,从而达到适合自己项目的优化方案。

CPU相关优化

CPU性能优化主要包括四个方向,DrawCalls,物理组件,GC,程序代码质量。

  1. DrawCalls ,首先我们要弄清楚什么是Drawcall,其实DC 就是一条渲染指令,由cpu 发送到gpu的渲染指令。既然是渲染指令,那么里面肯定是为了把渲染数据传输到GPU,其中包括顶点数据,材质,纹理,着色器。因而 DrawCall数量过多就会导致CPU进行大量计算,进而导致CPU的过载,影响游戏运行效率。所以我们要合理优化Drawcall
    1.在游戏项目里面最常用的就是 图集,和精灵 一个是ngui,一个ugui的,这个就不多做描述了。因为我项目用到的多数都是ngui,所以我重点讲诉ngui的。在NGUI框架中,会有一个静态的list用来存放所有的Panel,然后每个单独的Panel下会保存自己的UIWidget和UIDrawCall,就是在每次绘制的时候panle会遍历自己下面的所有层级下的子物体,直到查找结束,或者遇到新的panel会跳出当前分支,继续寻找其他分支,直到全部查找结束。所以panel 就是一个渲染单元。我们要记住,在同一个panel下,同一个图集要保证渲染顺序连续,不能穿插有别的图集的图片,否则就会增加一个dc。比如 同一个panel下面,有三张图,图1,depth =1 ,图集 =A,图2,depth =2 ,图集 =A,图3,depth =3 ,图集 =B 这个时候,就是2个dc,但是如果 图1,depth =1 ,图集 =A,图2,depth =2 ,图集 =B,图3,depth =3 ,图集 =A 那么就是3个dc,因为图集a的渲染顺序被打断了。
    2> 动静分离,把比如说boss血条,以及会更新的文本,放到一个单独的panel下面。
    3> 不显示的UI,比如主界面,往往dc很高,但是打开其他全屏界面的时候,这个时候其实完全可以把它不渲染,这里不是直接setactive = false,因为这样会导致再显示出来,造成额外的消耗,其实可以专门改变一个相机不渲染的layer 。这样就不会导致额外的消耗,但是要注意屏蔽点击事件。
    4>尽量少的使用反光啦,阴影之类的,这些会使物体多次渲染。

    批处理: 我们都知道游戏里面有静态批处理,以及动态批处理 批处理的前提条件就是相同材质的才能合并。静态批处理来说,好处就是自由度很高,限制条件少,但是它会占用更多的内存,并且经过批处理的物体不可以在进行移动.。如果对内存吃紧的,可以减少使用静态批处理。对于动态批处理来说,好处就是一切都是自动处理的,并且物体是可以移动的,但是限制颇多,具体有哪些限制下面会进行分析。
    静态批处理限制:
    【1】需要保持static,不能改变transform
    【2】使用相同材质的物体才能合批
    【3】一个批次上限为~15k个顶点
    动态批处理的限制如下:
    【1】Mesh顶点数量不能超过300以及顶点属性不能超过900,
    【2】使用lightmap的物体不行进行批处理
    【3】使用MultiplePass的shader也不会进行批处理
    【4】接受实时阴影的物体也不会进行批处理
    【5】不要使用缩放。分别拥有缩放大小(1,1,1) 和(2,2,2)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1) 和(1,3,1)的两个物体将可以进行批处理

2.物理组件优化
【1】设置一个合适的Fixed Timestep。
【2】不要使用网格碰撞器(mesh collider);
【3】如果UI物体确定不需要点击事件,最好取消勾选Raycast使用
【3】从性能优化的角度考虑,物理组件能少用还是少用为好。

3.GC(GC用来处理内存,但是由CPU来控制)
什么是GC,GC的全称是Garbage Collection,也就是垃圾回收,是一种自动管理堆内存的机制,管理堆内存上对象的分配和释放。Unity 内存管理机制, 内部自身会进行内存管理 。内存分为 堆内存堆栈内存
1.堆栈内存(stack)主要用来存储较小的和短暂的数据,堆内存(heap)主要用来存储较大的和存储时间较长的数据。
2.unity中的变量只会在堆栈或者堆内存上进行内存分配,值类型变量都在堆栈上进行内存分配,其他类型的变量都在堆内存上分配。
3.只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。
4.一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于堆栈上的内存回 收及其快速,处于堆上的内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态。
5.垃圾回收主要是指堆上的内存分配和回收,unity中会定时对堆内存进行GC操作。

GC 操作的过程:
 当堆内存上一个变量不再处于激活状态的时候,其所占用的内存并不会立刻被回收,不再使用的内存只会在GC的时候才会被回收。其操作如下

  【1】GC会检查堆内存上的每个存储变量;
  【2】对每个变量会检测其引用是否处于激活状态;
  【3】如果变量的引用不再处于激活状态,则会被标记为可回收;
  【4】被标记的变量会被移除,其所占有的内存会被回收到堆内存上。
  【5】GC操作是一个极其耗费的操作,堆内存上的变量或者引用越多则其运行的操作会更多,耗费的时间越长。

何时触发GC:
主要有三个操作会触发垃圾回收:
1.在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存;
2.GC会自动的触发,不同平台运行频率不一样;
3.GC可以被强制执行。

GC操作带来的问题:
1.需要大量的时间来运行,可能会使得游戏运行缓慢。其次GC可能会在关键时候运行,例如在CPU处于游戏的性能运行
关键时刻,此时任何一个额外的操作都可能会带来极大的影响,使得游戏帧率下降。
2.堆内存的碎片划。当一个内存单元从堆内存上分配出来,其大小取决于其存储的变量的大小。
当该内存被回收到堆内存上的时候,有可能使得堆内存被分割成碎片化的单元。也就是说堆内存总体可以使用的内存单元较大,
但是单独的内存单元较小,在下次内存分配的时候不能找到合适大小的存储单元,这也会触发GC操作或者堆内存扩展操作。

堆内存的碎片会造成两个结果, 一个是游戏内存占用越来越高,内存泄露,一个是 GC会更加频繁地被触发 ,导致帧率越来越低
*优化方案

降低GC影响的方法
	1.减少GC的运行次数;
	2.减少单次GC的运行时间;
	3.将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC

主要策略为
1. 对游戏进行重构,减少堆内存的分配和引用的分配。更少的变量和引用会减少GC操作中的检测个数从而提高GC的运行效率。
2.降低堆内存分配和回收的频率,尤其是在关键时刻。也就是说更少的事件触发GC操作,同时也降低堆内存的碎片化。
我们可以试着测量GC和堆内存扩展的时间,使其按照可预测的顺序执行。当然这样操作的难度极大,但是这会大大降低GC的影响。

4.代码质量(这个是我们最常用到的)
【1】减少装箱拆箱的写法,比如string.format
【2】减少空方法的使用,例如Update(),GUI(),因为这都会导致额外的消耗。
【3】减少Getcomponent 的使用,多使用缓存组件,以及缓存transfor
【4】使用内建的数组,比如用Vector3.zero而不是new Vector(0, 0, 0);
【5】对于方法的参数的优化:善于使用ref关键字。
【6】不要在Update里面new 对象,以及修改字符串。
【7】yield return 0 替换为 yield return null,减少装箱的操作。
【8】yield return new waitforsecond(1)在方法外使用缓存new waitforsecond();
【9】减少AddComponent的操作,因为这个函数特别费,会进行额外的gc。
【10】优化数学计算。比如,如果可以避免使用浮点型(float),尽量使用整形(int),尽量少用复杂的数学函数比如 Sin 和 Cos 等等
【11】在Awake 函数里面不建议有过于复杂的计算,否则会导致load时间边长。
【12】合理选用数据结构,能用数组的就不用list.
【13】.tag 可以用CompareTag 替代。
【14】 减少协程的嵌套使用。
【15】对频繁创建的物体使用对象池技术。
【16】学会使用脏标记模式,当内容发生变化的时候再去更改对应的变量,不是在update里面一直刷新。
【17】通用的方法抽出来做成静态方法。防止每次都各种复制方法到一个类中。
协程的停止:

 StopCoroutine()
 坑点:必须和启动的时候调用一致 “字符串” IEnumerator Coroutin
 禁用脚本 协程不会停止。
 删除脚本,或则Setactive =false 会停止协程。

Unity内存相关优化

在Unity运行过程中,内存主要占用其实包括三部分:1,三方库代码 (Unity库文件,第库) 2 Native堆(游戏资源) 3 Mono堆 (C#代码) 。所以我们主要针对的优化点也是基于这三块。
1.库代码优化: 减少第三方库的使用,工程里面减少不必要的插件,以及第三方库的引用。
2 Native堆(游戏资源):我们在项目里面主要针对的就是这一块。因为这个是见效最快的优化手段。

	 控制游戏内Resources 分配,Texture,Mesh  AnimationClip  AudioClip  使用相应的资源优化。
	 【Texture】:分辨率大小,如果只是简单的UI图片,可以取消勾选 Read/Write 和Mipmap, 安卓使用ETC1 +Alpha, IOS :PVRTC,
	 1. 减小 Max Size :使用能生成视觉上可接受的结果的最低设置。这种非破坏性方式,可以快速降低纹理内存
	 2. 使用 2 的幂 (POT) :Unity 要求移动端纹理压缩格式,如果不是就叫NPOT
	 3. 制作纹理图集:使用精灵,或者ngui图集。
	 4. 关闭 Read/Write Enabled 选项 :如果启用,此选项在 CPU 和 GPU 可寻址内存中都会创建副本,纹理会
	 占用双倍内存。大多数情况下,应保持此选项为禁用状态。如果要在运行时生成纹理,请通过 
	 Texture2D.Apply 强制执行,并且传入设置为 true 的 makeNoLongerReadable。
	 5. 禁用不必要的 Mip Map :对于在屏幕上大小保持不变的纹理(如 2D 精灵和 UI 图形),Mip Map 
	 不是必需的,对于与摄像机的距离会变化的 3D 模型,请保留 Mip Map为启用状态。
	 
	 【网格】:
	  1. 压缩网格 :高性能压缩可以减少占用磁盘空间(但不会影响运行时的内存)。请注意,网格量化可能造成
	  不准确,因此应试验不同的压缩级别,从而找到适合模型的压缩级别。
	  2. 禁用 Read/Write :如果启用此选项,内存中会有重复网格,网格的一个副本在系统内存中,另一个在 
	  GPU 内存中。
	  3. 禁用骨骼和 BlendShape :如果网格不需要骨架或 BlendShape 动画,请尽可能禁用这些选项。
	  4. 尽可能禁用法线和切线 :如果确信网格的材质不需要法线或切线,请取消选中这些选项,以节省更多内存。
	  
	 【AudioClip】尽管音频通常不会造成性能瓶颈,还是可以进行优化以及节省内存。
	 1. 请尽可能使用单声道,或者启用Force To Mono设置
	 2. 尽可能使用原始未压缩WAV 文件作为源资源(这个一般看项目情况,如果想保证音质可以用未压缩的。)
	 3.  压缩剪辑并降低压缩比特率(对大多数声音使用 Vorbis,对常用的短声音使用 ADPCM)
	 4. 选择正确的加载类型
	 			*小剪辑 (< 200 kb) 应采用 Decompress on Load。将声音解压缩为原始 16 位
				PCM 音频数据,会导致 CPU 开销和内存占用,因此,这仅适用于短声音。
				*中等剪辑 (>= 200 kb) 应保持为 Compressed in Memory。
				*大文件(背景音乐)应设置为 Streaming。否则,整个资源会一次性加载到内存中。
	  长音频的LoadType 改为Streaming,开启后,音频加载方式变为边播放边读取,内存明显降低,
	  背景音乐使用这种方式。
	  
	  【Animator】
	   1. 尽可能使用通用骨架,而不是人形骨架 。人形骨架每一帧(即使未使用)都计算反向动力学和动画重定
	    向,占用的 CPU 时间比等效的通用骨架多 30-50%。如果不需要这些特定人形骨架功能,请使用通用骨架。
	   2. 避免过多使用 Animator。
	   3. 开启 OptimizeGameObjects:OptimizeGameObject 可以有效降低动画开销
  1. Mono堆(C#代码优化)
    【1】严格控制代码质量,不要频繁的new instainte,以及destroy 对象,建议使用对象池
    【2】不要在Update里面操作拼接字符串
    【3】.tag 的调用 替换成CompareTag 进行替换
    【4】减少对象的数量
    【5】缓存:对于能缓存的数据,尽量缓存
    【6】List用Clear代替new ,举例,不要在返回list的函数里面每次都去new一个list。
    【7】LINQ和正则表达式
    【8】手动强制GC

UnityGPU相关优化

影响GPU的情况主要就是
1. 填充率 图形处理单元每秒渲染的像素数量
2. 像素的复杂度 动态阴影,光照,复杂的shader等等
3. 顶点数量
4. GPU的显存带宽
针对以上4点 就是优化方向

减少顶点数量, 压缩图片以适应显存带宽, 减少绘制数据
保持材质的数目尽可能少。这使得Unity更容易进行批处理。
使用纹理图集(一张大贴图里包含了很多子贴图)来代替一系列单独的小贴图。它们可以更快地被加载,具有很少的状态转换,而且批处理更友好。
使用光照纹理(lightmap)而非实时灯光。
使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略。
使用mobile版的shader。因为简单。

===================================================================
本人辛苦原创,转载请标注。
沟通交流 qq 625498140


版权声明:本文为Debug_ss原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。