Unity 5.X 编辑器新功能Frame Debugger简介
本文基于Unity5.4.1、NGUI3.9来演示帧调试,找出幕后黑手,意在分享解决问题的思考过程
第一个问题来了,为什么3个共用图集的UISprite+1个UILabel会有3个Batches?
看看frameDebug,你会发现多出了一个同样的DrawMeshCardsAtlas,仔细再看看你会发现他们深度有猫腻
Batches:3 sprite(1) depth=0 ,sprite(2)depth=1,label depth=2,sprite(3)depth=3
Batches:2 sprite(1) depth=0 ,sprite(2)depth=1,label depth=5,sprite(3)depth=3
我们来分析下
1、UISprite和UILabel都继承自UIWidget
2、他们都为UIPanel的子对象
3、他们都和depth有关
已经找到了共同点,那么我们来看看源码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public int depth { get { return mDepth; } set { if (mDepth != value) { if (panel != null ) panel.RemoveWidget( this ); mDepth = value; if (panel != null ) { panel.AddWidget( this ); if (!Application.isPlaying) { panel.SortWidgets(); panel.RebuildAllDrawCalls(); } } #if UNITY_EDITOR NGUITools.SetDirty( this ); #endif } } } |
注意UIWidget中的depth属性,set中的两个方法:
panel.SortWidgets:把UIPanel下的所有UIWidget对象进行从小到大的深度排序
panel.RebuildAllDrawCalls:把相邻的UIWidget进行材质、贴图、shader合并,并把数据存入UIDrawCall并添加drawCalls列表中用于渲染。
很快就找到问题了嘛,因为UILabel的深度在3个UISprite之间,所以增加了一个Batches
Ps:Batches批处理,Unity5.X取消了DrawCall显示,因为单独计算DrawCall没有意义
既然知道了原因,那么我们把可以合并的UIWidget深度按一定规则设置不就好啦
UITexture 最底层用于背景, depth>=0&&depth<10
UISprite 中层 ,icon:depth>=1000&&depth<1500btn:depth>=1500&&depth<2000
UILabel 上层, depth>=2000 && depth<3000
ps:注意夹层不同图集情况,按业务逻辑去求最优解,不必太过耗时在合并Batches上
Profiler捕捉到UIPanel.LateUpdate的CPU占用率偶尔跳的很高,到底干了啥 ?
继续看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | void LateUpdate () { #if UNITY_EDITOR if (mUpdateFrame != Time.frameCount || !Application.isPlaying) #else if (mUpdateFrame != Time.frameCount) #endif { mUpdateFrame = Time.frameCount; // Update each panel in order for ( int i = 0, imax = list.Count; i < imax; ++i) list[i].UpdateSelf(); int rq = 3000; // Update all draw calls, making them draw in the right order for ( int i = 0, imax = list.Count; i < imax; ++i) { UIPanel p = list[i]; if (p.renderQueue == RenderQueue.Automatic) { p.startingRenderQueue = rq; p.UpdateDrawCalls(); rq += p.drawCalls.Count; } else if (p.renderQueue == RenderQueue.StartAt) { p.UpdateDrawCalls(); if (p.drawCalls.Count != 0) rq = Mathf.Max(rq, p.startingRenderQueue + p.drawCalls.Count); } else // Explicit { p.UpdateDrawCalls(); if (p.drawCalls.Count != 0) rq = Mathf.Max(rq, p.startingRenderQueue + 1); } } } } |
1、更新自己,和所有UIPanel子对象
2、更新被改变的Transform矩阵、layer、widgets
3、是否重新填充UIDrawCall并绘制
4、更新UIScrollView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | void UpdateSelf () { UpdateTransformMatrix(); UpdateLayers(); UpdateWidgets(); if (mRebuild) { mRebuild = false ; FillAllDrawCalls(); } else { for ( int i = 0; i < drawCalls.Count; ) { UIDrawCall dc = drawCalls[i]; if (dc.isDirty && !FillDrawCall(dc)) { UIDrawCall.Destroy(dc); drawCalls.RemoveAt(i); continue ; } ++i; } } if (mUpdateScroll) { mUpdateScroll = false ; UIScrollView sv = GetComponent(); if (sv != null ) sv.UpdateScrollbars(); } } |
似乎重点在mRebuild上,它掌管了是否需要重新绘制,那是不是说我的动态UIWidget的改变导致UIPanel下的所有UIWidget进行了重新绘制?
顺着摸下去,在UpdateWidget下的逻辑控制是否需要Rebuild,关键在于UIWiget的UpdateGeometry判断,这一切都和mChanged 参数有关。
我找到了2个关键点,还有其他的原因导致重新绘制不浪费篇目列出了
UISprite,当spriteName发生改变时,mChanged =true
UILabel,当text被改变时,mChanged = true
原来倒计时的UILabel和打出的卡牌UISprite的改变导致UIPanel的所有UIWidget开始绘制
那么进行动静分离,把动态元素放入UIPanelDynamic,静态元素放在UIPanelStatic里面,这样即使重绘也只是少量元素
ps:注意depth是NGUI定义的深度概念,RenderQ是unity定义的深度,UIPanel的默认startingRenderQueue=3000,如果非NGUI对象(比如特效)就需要用到修改层级的脚本。
为什么我们如此惧怕高Batches,那么多少才安全呢?
不能凭感觉,我们要用科学公式来说明问题,看看英伟达给出的答案
https://www.nvidia.com/docs/IO/8228/BatchBatchBatch.pdf
总结这个文档给出的结论(有兴趣强烈建议阅读以下)
· 只是Batches操作也会把cpu占用到100%
· gpu的处理速度远远高于cpu,所以瓶颈一般在cpu
· 1GHz的cpu每秒处理25kbatches,cpu占用率为100%
· Formula:25k*GHz*Percentage/Framerate
GHZ:设备cpu主频 Percentage: cpu占用率 Framerate:目标设备的固定帧率
基于公式我做了几个设备的cpu占用率计算,那么batches和cpu占用到底控制在多少呢?
假定目标设备是iphone5s,固定帧率是30,如果游戏品类是ARPG一般主界面Batches<=80,战斗场景<=150
像我们这款UNO游戏<=50,能在更多的低端机上畅跑
ps:注意这里的cpu占用率非整个游戏占用率,实际中还要考虑代码等因素导致的cpu占用,一般ARPG手游cpu占用平均值控制在20%以内,MMORPG、MOBA会高一些。
具体多少根据游戏品类和目标机型来限定,当游戏开发中后期可以借助 http://wetest.qq.com/ 进行详细评测
为什么如此注重性能优化?
1、发烫,用户流失
2、卡,用户流失
3、闪退,用户流失
Unity在5.X版本后Statistics用Batches代替DC,加入DebugFrame,可见对每一帧能处理的数据的重视
Batches就像一个箱子,把能够合并的数据都放入其中,打包发驱动,一般情况下瓶颈不在带宽上,所以我们应该着重排查CPU和GPU每一帧处理的数据时候过载,还有一些参数例如tris、verts也很重要,但没有具体公式去计算,一般都是根据经验来限定,不同项目不同的限定,比如一个主角<2000面,场景<10000面。
扯远了,总结一下
· 根据项目类型提前制定符合目标机型的性能方案,在UI开发中就带有这种思维,但别过早优化导致开发效率下降
· 理解Batches原理,如何有效降低
· 重视每一帧处理的数据是否过载,保证游戏流畅
· 不要过度优化和过早优化