UGUI性能消耗和管理
文章的知乎链接 https://zhuanlan.zhihu.com/p/678478209
本来打算只放在知乎的,想想还是在自己网站也多留一份
引
参考文献几篇感觉写的都比我强太多,建议大家都去看看。
对于大部分游戏来说,相比Gameplay可能UI的消耗是不值一提的。但如今手机游戏也越来越多的引入了主机上比较现代的UI设计理念,育碧经典的《全境封锁》《幽灵行动》,有点机能风并且结合3D的UI,在很多游戏上都可以见到类似的设计;与此同时我们也会有用UGUI实现的Gameplay的部分的存在,所以UGUI的性能消耗也会变得明显起来。
稍微跑题了一下,接下来我们先说说我们怎样去衡量性能消耗。
性能消耗的指标
Drawcall
渲染过程中CPU也是要干活的,对于加载到游戏中的资源和对象等,CPU需要计算其顶点相关的矩阵,游戏中很多物体的材质和贴图都不一样,此时CPU的主要工作就是设置这些物体的渲染状态。Drawcall就是一个命令,由CPU发起GPU接受,告诉GPU开始一个渲染过程。为什么要减少Drawcall?因为CPU如果把精力全都放在发起drawcall上,就没空去设置渲染状态了,而GPU的渲染效率是要比CPU设置状态高很多的,如果大量的drawcall挤占了CPU的运行,那么CPU就会变成性能的瓶颈。
Batches(Drawcalls)
意为批次,对于UGUI来说,Batching是以Canvas为单位的,这里之前的表述有些问题,一次Batching会做很多事情,例如按照深度排序来做合批操作,从而降低Drawcall的频率。所以Batches其实就是打包(Batching)产生的次数,每一个Batch会调用一次Drawcall。一个Canvas下会有多个Batch,也就是Batches的数量是大于等于1的。
补充 Drawcall Batching
是一种优化的方法,通常Unity中使用了两种:静态批处理和动态批处理,而UGUI Batching的过程就很符合动态批处理的概念,但是UI中的图片是一个很特殊的存在,通常优化需要我们手动去做(打包图集)。
性能消耗的痛点
Rebatch
对mesh的操作,发生在C++层,指Canvas分析UI节点生成最优批次的过程。
节点数量过多会导致耗时较长,Canvas中包含的mesh发生改变时就会触发,例如SetActive transform的改变,颜色改变文本改变等,每个Canvas独立处理,互不影响。在unity5.2之后改用多线程,大大减少了主线程的压,但是该做的优化还是要做的。
不同材质的重叠会导致Batching的中断,所以也要尽量避免UI元素的重叠。
ReBuild
对material和layout的操作。
例如Layout组件调整RectTransform尺寸,Graphic组件更新材质,Mask执行Cull等。
性能消耗的针对性优化
Rebatch的针对性优化
减少Canvas下的顶点数量,对于Image首选Simple模式,其次是Sliced模式且不勾选FillCenter。
Text考虑用TextMeshPro代替,如果一定要用,尽量避免使用Shadow和Outline。
减少重新计算的顶点数量(动静分离),拆分成多个Canvas,会发生变化的UI部分放到一个Canvas下面,不会发生变化的UI部分放到另一个Canvas下面。实际执行的时候为了方便管理通常不会执行到这么极限,注意Canvas的数量不能太多,否则还是会增加DrawCall。
ReBuild的针对性优化
能不用Layout就不用,简单的布局RectTransform手动摆好来代替。
Mask和RectMask2D
RectMask2D相对比较友好,不会额外增加Drawcall,但是只能遮挡矩形区域。
Mask相对性能负担较重,会打乱合批的过程,但是没有形状限制。
圆形的Mask边缘锯齿比较明显,而且遮挡效果是显示上的,射线层不会因此变化。
打包图集
一般来说打图集的标准是灵活多变的,大家各有各的说法。
对于经常用到的零碎的小图片,最好打包到一起,例如各种各样的图标。过大的最好不要都塞一块,例如人物立绘,背景,还是一张一张来,原因在于图集中的任何一个被引用的加载了,整个图集都会load进来,所以要避免常用的和不常用的被塞到一块。
UnityProfiler数据分析
- Canvas.BuildBatch
- Canvas.UpdateBatches
如果这两项占用比较大,最好要做Canvas的拆分。
- Canvas.SendWillRenderCanvases
这一项如果占用较大,Graphic的重构可能有问题,如果每一帧都在执行,证明动静分离的部分没有做好。
- WillRenderCanvas/IndexedSet_Sort
- CanvasUpdateRegistry_SortLayoutList
考虑使用RectTransform替代Layout。
- Text_OnPopulateMesh
- Shadow_ModifyMesh
- Outline_ModifyMesh
考虑换掉或者优化Text。
UGUI的面板管理策略
个人认为的面板管理策略,具体情况还是会有所不同,仅供参考。
gal式的(局内局外UI一致)UI可以考虑完全不卸载,因为你也不知道玩家什么时候就会去sl所以可以保持常驻。
手游式的UI(局内局外UI不一致),局外UI可以根据大小决定是否动态加载,动态卸载。
局外UI通常作为打包后的游戏入口(当然Load之前还是要有个没啥东西的场景来跑网络验证和资源加载)。
以面板为单位,可以考虑set active(Rebatch),也可以考虑缩放到000(Rebuild)。
面板中的元素,如果是背包这样含有滚动列表的,最好使用循环列表来做优化,减少同屏元素,至于元素数量多的情况下,也许缩放会比显隐来说是个更好的选择。
一部分内容可以不需要通过UGUI,转换成2D的sprite,做特效也方便。例如人物立绘,背景,通常会有类似HD2D的效果,这部分完全可以通过摄像机的调度来实现UGUI和2D的相结合。
贴一个我之前自己瞎写的面板管理的做法,其实还可以加个List用来记录面板的移动顺序,跳转进栈,回退出栈,这样就实现跨逻辑设定的跳转记忆,例如你从背包里的道具详情跳转到了关卡,关卡的上一级是主界面,但回退要求回退到背包。
//------------------------------------------------------------ // 脚本名: MainUI.cs // 作者: 海星 // 描述: # //------------------------------------------------------------ using System.Collections; using System.Collections.Generic; using UnityEngine; using StarFramework.Runtime; public class MainUI : MonoSingleton<MainUI> { private Dictionary<string, GameObject> panels; public GameObject nowPanel; public GameObject startMenu; void Start() { panels = new(); //跳过主菜单 for (int i = 1; i < transform.childCount; i++) { panels.Add(transform.GetChild(i).gameObject.name, transform.GetChild(i).gameObject); } } public void NextPanel(string panelName = null) { //不管怎样 只要现在有面板就关了 if (nowPanel != null) { nowPanel.SetActive(false); } //空就代表返回到主菜单 不为空就打开对应面板 if (!string.IsNullOrEmpty(panelName)) { if (panels.ContainsKey(panelName)) { nowPanel = panels[panelName]; nowPanel.SetActive(true); } else { Debug.LogError("面板名不对,找不到对应面板"); } } //返回主菜单的时候要把当前面板置空 else { nowPanel = null; } } public void ShowMenu(bool active) { startMenu.SetActive(active); } }
参考文献
https://www.jianshu.com/p/8aa6f49e7f41