浅谈Unity常用性能优化
因为做项目用到了一些也查了很多资料,所以想记录一下自己的工作经验,希望跟大家分享一下,大佬勿喷。 在我个人理解,性能优化就是找平衡,内存,cpu,渲染之间的一种平衡。优化一定不能盲目的优化,肯定是有方向的,有目的性的优化,否则肯定会出引发出别的问题。优化一定要找到优化的瓶颈在哪,具体是cpu,内存,还是渲染。然后让他们三者之间有一些取舍,从而达到适合自己项目的优化方案。
CPU相关优化
CPU性能优化主要包括四个方向,DrawCalls,物理组件,GC,程序代码质量。
-
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 会停止协程。
本人辛苦原创,转载请标注。 沟通交流 qq 625498140
|