UGUIDrawCall优化

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UGUIDrawCall优化相关的知识,希望对你有一定的参考价值。

参考技术A 1、UI DrawCall分析工具

最近只针对项目中主界面的UI进性DrawCall的优化,主要用到的工具就是Unity自带的Frame Debugger,我们项目使用的是Sprite Packer打图集,如果是从本地加载资源,如图1-1,首先要确保Project Settings中Sprite Packer Mode为Always Enabled,保证游戏运行时所有的图标打到对应的图集中。如果使用AssetBundle方式加载,打包的时候应该已经打成图集了,该项设置不设置就无所谓了。

Frame Debugger打开方式Window->Analysis->Frame Debugger(我用的Unity版本是2018.4.8),打开后界面如下:

点击后就会展示游戏中所有的DrawCall信息,如图1-3,Unity UI绘制调用的位置在CameraRender下Render的子组Canvas.RenderSubBatch下,后边的68即为UI占用的DrawCall数量。Unity UI绘制调用的位置取决于Canvas的Render Mode,我们使用的Render Mode为Screen Space-Camera。如果Render Mode设置为Screen Space - Overlay,则Ui绘制的位置在Canvas.RenderOverlay组中,如图1-4所示;如果Render Mode设置为World Space时也是在Camera.Render下Render的子组中,虽然都是子Render,但是和Screen Space-Camera模式会在不同的子组中。

选中某个Draw Mesh右侧就会显示该详细信息,如图1-5,选中的是一个Image,其中1表示该Image渲染的渲染层级为203,2表示该组件使用的shader为UI/Default,3表示Image所在的图集为Common(Group 1)。

再看一个Text的详细信息,如图1-6,基本与Image相同,只是使用的纹理不同。

2、影响Draw Call的因素

(1) 针对Image,需要保证使用的图片在同一图集且同一个Group中,且使用的材质相同。

a、 如下图所示,一般的Image都不会设置Material即Material为None,此时使用的就是UGUI默认的材质,如图1-4中的2标记的位置,都是默认UI/Default,所以材质的问题一般不需要考虑;

b、 重点考虑的就是图集问题,这是比较麻烦的问题,需要根据图片使用的范围来规划图集。开始我们把所有主界面用到的图片都放到一个图集中,但是由于图片个数太多,而图集又有大小上限(我们项目中设置的为1024*1024),虽然会打到同一个图集中,但是会被分到不同的Group中。通过Frame Debugger查看即使在同一个图集中,不在同一个Group也不能合批。所以直接把所有图片放在一个图集中并不能解决问题,还要根据UI进行细分,同一个UI中使用的图片打到一个图集里,或几个UI中用到的图集打到一个图集中。好多个UI公用的图片单独放到一个公用的图集中。为了便于规划图集,我还写了个小工具统计每个图片在各个UI中的使用情况。统计结果如图2-2,每一行显示某个图片的引用次数,次数后紧跟引用该图片的UI。

[MenuItem("策划/UI/收集UI中图片引用情况")]

public static void GetSpriteReferCount()



            Dictionary<string, Dictionary<string, bool>> spriteToPrefab = new Dictionary<string, Dictionary<string, bool>>();

            List<string> allFullPath = MUEditorUtility.GetAllFullPathIn(UI_ASSET_DIR, ".prefab");

            for (int i = 0; i < allFullPath.Count; i++)

           

                EditorUtility.DisplayProgressBar("图片检测", "正在收集UI中的图片引用情况……", i / (float)allFullPath.Count);

                string assetPath = MUEditorUtility.FullPathToAssetPath(allFullPath[i]);

                GameObject uiGameObj = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);

                if (uiGameObj == null)

               

                    continue;

               

                Component[] allImageComponents = uiGameObj.transform.GetComponentsInChildren(typeof(Image), true);

                for (int j = 0; j < allImageComponents.Length; j++)

               

                    Image t = allImageComponents[j].GetComponent<Image>();

                    if (t.sprite != null)

                   

                        string spriteName = t.sprite.name;

                        if (spriteToPrefab.ContainsKey(spriteName) == false)

                       

                            spriteToPrefab[spriteName] = new Dictionary<string, bool>();

                       

                        if (spriteToPrefab[spriteName].ContainsKey(uiGameObj.name) == false)

                       

                            spriteToPrefab[spriteName][uiGameObj.name] = true;

                       

                   

               

           

            Dictionary<string, Dictionary<string, bool>> spriteToPrefab1 = spriteToPrefab.OrderBy(o => o.Value.Count).ToDictionary(p => p.Key, o => o.Value);

            DirectoryInfo dirInfo = Directory.CreateDirectory(Application.dataPath + "/Res/Gui");

            FileInfo[] files = dirInfo.GetFiles("UI_Sprite_Refer.txt");

            foreach (var file in files)

           

                File.Delete(file.FullName);

           

            using (FileStream fileStream = File.Create(dirInfo.FullName + "/UI_Sprite_Refer.txt"))

           

                string txt = "";

                string temp = "";

                foreach (KeyValuePair<string, Dictionary<string, bool>> kv1 in spriteToPrefab1)

               

                    temp = kv1.Key + ":";

                    //temp += " " + GetParentDir(kv1.Key);

                    temp += "  " + kv1.Value.Count;

                    foreach (KeyValuePair<string, bool> kv2 in kv1.Value)

                   

                        temp += "  " + kv2.Key;

                   

                    txt += temp;

                    txt += "\n";

               

                StreamWriter writer = new StreamWriter(fileStream);

                writer.Write(txt);

                writer.Flush();

                writer.Close();

                fileStream.Close();

           

            EditorUtility.ClearProgressBar();

       

c、 在打图集的过程中还遇到一个问题,同一图集中的一个图片,在总大小没有超过图集上限的情况下,却被打到了该图集的另一个Group中。使用Frame Debugger也看不出问题。然后我就用Sprite Packer在本地打一下图集看一下,这两个Group区别在哪儿。通过对比发现两个Group中图片的格式不一样,一个为RGBA Compressed DXT5,一个为RGB Compressed DXT1,如图2-3和2-4。

原来Unity打图集时只能将相同格式的图片打到同一Group中。查看被打到另一个Group中的图片,发现这张图没有Alpha通道,工程中所有平台的纹理Format都设置为Automatic,Alpha Source设置为Input Texture Alpha,即Unity会根据原图是否有Alpha通道自动设置纹理格式,如图2-5。

我们可以通过重写不同平台上的格式设置,保证该图片格式与其他图片格式一致,如图2-6。

(2) 针对Text,保证相邻的Text使用相同字体,相同材质,这个一般情况都可以做到。

(3) 保证可以合批的元素在同一个Canvas下。

为了防止某些动态元素(如倒计时、技能CD等)的改变导致整个UI的网格重建,一般采用动静分离的方式,将动态元素放在单独的Canvas下。这时可能误将一些本可以合批的静态元素放到了不同的Canvas下。我在优化时就遇到了这种情况。如图2-7所示,Image_Kuang和Image_Parent位于同一渲染层级,且他们挂的图片在同一图集里,但由于Image_Parent节点下有动态变化元素,所以单独挂了Canvas,导致这两个Image不能合批。解决方式就是将Image_Parent的Image移除挂到一个新建的与Image_Kuang位于同一Canvas的节点下,如图2-8所示,在Image_Kuang下单独建了一个Image_PosBack节点挂载Image。

(4) Mask和Rect Mask 2D会将内外元素分离导致不能合批。另外能用Rect Mask 2D解决的尽量不要用Mask,Mask除了使内外元素不能合批,本身也会占用2个DrawCall。Mask和Rect Mask 2D比较如下:

a、 Mask占用两个DrawCall,一个在底下设置Stencil Buffer,一个在顶上还原Stencil Buffer,Mask下的子元素夹在中间本身不占DrawCall;Rect Mask 2D本身不占DrawCall

b、 如果多个Mask绑定的Image组件,属于同一个Atlas,那么Mask之间的元素可以进行合并(包括Mask自己产生的2个DrawCall);否则不能合并如果RectMask2D上绑定了Image,那么多个RectMask2D的Image如果属于同一个Atlas可以合并

c、内外元素 无法合批无法合批

d、完全裁剪元素:Mask 依然占据DrawCall;Rect Mask 2D不占用DrawCall,也不参与Depth计算;

e、Hierarchy 中被分割元素:Mask 可以正常合批;Rect Mask 2D:如果Depth、Atlas与RectMask2D下的某元素相同,则无法合批。

f、裁剪掉的部分:Mask 还会影响其他元素的Depth计算,而它自己的也会受到其他元素的影响。Rect Mask 2D 依然参与Depth计算

g、 多个Mask内的UI节点间如果符合合批条件,可以合批。RectMask2D之间无法合并DrawCall。

(5) UI元素Position或者Rotation改变

a、 Position的Z值不为0,元素会被视为3DUI,不参与合批,如果父节点Z!=0,其下的子元素都无法合批。

b、 Rotation的X或Y修改,导致元素不在UI平面,则无法合批,原因和Position的Z值改变一样。
(6) UI元素重叠和层级深度

这个最终要的结果就是使多个元素的渲染层级相同,就达到合批目的了,如图1-4中1标记的位置。当然这一项的调整也是最耗时最需要耐心的,这就需要我们去分析每个UI中各个元素在Hierarchy中的层级。

下面拿一个例子说明一下,如图2-9所示,图中左侧红框内4个元素,其中Image_Back、Drop_Image、Btn_Image所用图片在一个图集中,Drop down使用的图片在其它图集中。这时所有的元素占用3个DrawCall,查看右侧Draw Mesh发现Image_Back和Drop_Image共占一个DrawCall,而Btn_Image和Dropdown分别单占一个DrawCall。

下面我们调整一下Btn_Image的位置,如图2-10所示,整体的Draw Call变成2,Image_Back、Drop_Image、Btn_Image三个元素共占一个DrawCall。Dropdown单独占一个DrawCall。这是因为开始时Dropdown夹在Drop_Image和Btn_Image中间,由于它属于另外的图集,破坏了Drop_Image和Btn_Image的合批。

Unity自带的Profiler工具中可以查看合批中断的原因。如图2-11所示,打开Profiler,滑动到最底部可以看到两个和UI相关的选项,一个UI,一个UI Details,选中任意一个,下边有全部Canvas的一个列表,点击展开任意一个Canvas,就可以看到该Canvas下所有的批处理信息,其中有一列Batch Breaking Reason显示了该批次不能与上一个批次合批处理的原因,合批被破坏的主要原因有两个:Different Texture:即使用的纹理不同,可能是图片不在同一个图集,也可能是Image和Text相邻导致;Different Material Instance:材质不同,一般由于自己给Image使用了自定义材质或者游戏中使用了Text Mesh Pro UGUI。另外GameObject Count为该批次处理对象的个数;GameObjects列列出了所有对象,可能由于对象太多只显示有几个Objects,这是可以双击这一行,会直接选中该批次处理的所有对象,如图2-12所示。

另外看到一个Cumulative Batch Count:累计批次数量。如图2-11,我们可以看到该列的总值为82,我们再看一下Frame Debugger中 Draw Mesh的个数,如图2-13为69。这两个数不一样,Frame Debugger中显示的是当前帧Batch个数,而Cumulative Batch Count是从开始游戏运行总的批次个数,在游戏过程中有些元素的改变导致批处理的变化,所以这两个数会不一样。

(7) 对于Alpha为0的Image,需要勾选其CanvasRender组件上的Cull Transparent Mesh选项,否则依然会产生DrawCall且容易打断合批。

(8)空Image

有时会使用空的Image来接受点击事件,空Image也会产生DrawCall。我们可以使用Empty4Raycast组件替代空Image。

以上是关于UGUIDrawCall优化的主要内容,如果未能解决你的问题,请参考以下文章

优化技巧汇总_通用优化+Linux 优化+HDFS 优化+MapReduce 优化+HBase 优化+内存优化+JVM 优化+Zookeeper 优化

iOS性能优化总结

MySql性能优化查询优化

SEO优化:网站页面优化URL优化内部链接优化

MySQL 进阶 索引 -- SQL优化(插入数据优化:导入本地文件数据主键优化order by优化group by优化limit优化count优化update优化)

优化性能和优化内存有啥区别?