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

https://www.drflower.top/posts/aad79bf1/

https://gwb.tencent.com/community/detail/120374

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注