UGUIDrawCall优化
1、UI DrawCall分析工具
最近只针对项目中主界面的UI进性DrawCall的优化,主要用到的工具就是Unity自带的Frame Debugger,我们项目使用的是Sprite Packer打图集,如果是从本地加载资源,如图1-1,首先要确保Project Settings中Sprite Packer Mode为Always Enabled,保证游戏运行时所有的图标打到对应的图集中。如果使用AssetBundle方式加载,打包的时候应该已经打成图集了,该项设置不设置就无所谓了。
图1-1Frame Debugger打开方式Window->Analysis->Frame Debugger(我用的Unity版本是2018.4.8),打开后界面如下:
图1-2点击后就会展示游戏中所有的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模式会在不同的子组中。
图1-3 图1-4选中某个Draw Mesh右侧就会显示该详细信息,如图1-5,选中的是一个Image,其中1表示该Image渲染的渲染层级为203,2表示该组件使用的shader为UI/Default,3表示Image所在的图集为Common(Group 1)。
图1-5再看一个Text的详细信息,如图1-6,基本与Image相同,只是使用的纹理不同。
图1-62、影响Draw Call的因素
(1)针对Image,需要保证使用的图片在同一图集且同一个Group中,且使用的材质相同。
a、如下图所示,一般的Image都不会设置Material即Material为None,此时使用的就是UGUI默认的材质,如图1-4中的2标记的位置,都是默认UI/Default,所以材质的问题一般不需要考虑;
图2-1b、重点考虑的就是图集问题,这是比较麻烦的问题,需要根据图片使用的范围来规划图集。开始我们把所有主界面用到的图片都放到一个图集中,但是由于图片个数太多,而图集又有大小上限(我们项目中设置的为1024*1024),虽然会打到同一个图集中,但是会被分到不同的Group中。通过Frame Debugger查看即使在同一个图集中,不在同一个Group也不能合批。所以直接把所有图片放在一个图集中并不能解决问题,还要根据UI进行细分,同一个UI中使用的图片打到一个图集里,或几个UI中用到的图集打到一个图集中。好多个UI公用的图片单独放到一个公用的图集中。为了便于规划图集,我还写了个小工具统计每个图片在各个UI中的使用情况。统计结果如图2-2,每一行显示某个图片的引用次数,次数后紧跟引用该图片的UI。
图2-2[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。
图2-3 图2-4原来Unity打图集时只能将相同格式的图片打到同一Group中。查看被打到另一个Group中的图片,发现这张图没有Alpha通道,工程中所有平台的纹理Format都设置为Automatic,Alpha Source设置为Input Texture Alpha,即Unity会根据原图是否有Alpha通道自动设置纹理格式,如图2-5。
图2-5我们可以通过重写不同平台上的格式设置,保证该图片格式与其他图片格式一致,如图2-6。
图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。
图2-7 图2-8(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。
图2-9下面我们调整一下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的合批。
图2-10Unity自带的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所示。
图2-11 图2-12另外看到一个Cumulative Batch Count:累计批次数量。如图2-11,我们可以看到该列的总值为82,我们再看一下Frame Debugger中 Draw Mesh的个数,如图2-13为69。这两个数不一样,Frame Debugger中显示的是当前帧Batch个数,而Cumulative Batch Count是从开始游戏运行总的批次个数,在游戏过程中有些元素的改变导致批处理的变化,所以这两个数会不一样。
图2-13