Unity 5.X 编辑器新功能Frame Debugger简介

发表于2016-12-10
评论1 1.01w浏览

本文基于Unity5.4.1NGUI3.9来演示帧调试,找出幕后黑手,意在分享解决问题的思考过程

第一个问题来了,为什么3个共用图集的UISprite+1UILabel会有3Batches?

  看看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.LateUpdateCPU占用率偶尔跳的很高,到底干了啥 

  继续看源码

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:注意depthNGUI定义的深度概念,RenderQunity定义的深度,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占用率计算,那么batchescpu占用到底控制在多少呢?

  假定目标设备是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原理,如何有效降低

· 重视每一帧处理的数据是否过载,保证游戏流畅

· 不要过度优化和过早优化

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引