UGUI性能消耗和管理
引
建议大家都去看看参考文献的原文,会很有帮助。
对于大部分游戏来说,相比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
意为批次,对于UGUI来说,Batching是以Canvas为单位的,这里之前的表述有些问题,一次Batching会做很多事情,例如按照深度排序来做合批操作,从而降低Drawcall的频率。所以Batches其实就是打包(Batching)产生的次数,每一个Batch会调用一次Drawcall。一个Canvas下会有多个Batch,也就是Batches的数量是大于等于1的。
补充 Drawcall Batching
https://docs.unity3d.com/Manual/DrawCallBatching.html
是一种优化的方法,通常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)。
这里提到了一种方案,就是直接禁用Canvas组件,好处在于这样做只会阻止Canvas去调用GPU,又不会丢失已经缓存好的顶点信息,重新启用只会重新进行绘制而不再触发重建过程。当然,这只是对于常驻的UI来说比较合适的,毕竟太多面板堆积在内存里仍然是不好的。
面板中的元素,如果是背包这样含有滚动列表的,最好使用循环列表来做优化,减少同屏元素,至于元素数量多的情况下,也许缩放会比显隐来说是个更好的选择。
一部分内容可以不需要通过UGUI,转换成2D的sprite,做特效也方便。例如人物立绘,背景,通常会有类似HD2D的效果,这部分完全可以通过摄像机的调度来实现UGUI和2D的相结合。
贴一个我自己瞎写的面板管理的做法,主要还是按照池子的概念来调度的,打开的面板被关闭一段时间后就会被回收。你还可以加上很多限制,例如面板的最大打开数目,或者用一个栈来记录面板的层级,来实现跨不同界面返回上一级的效果。
// ViewManager.cs by 海星
//
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace StarFramework.Runtime
{
/// <summary>
/// 界面管理器 用于控制界面启用和关闭
/// </summary>
public class ViewManager : BaseSingleton<ViewManager>
{
private readonly Dictionary<Type, BaseView> viewTypeDic;
private readonly Dictionary<BaseView, GameObject> viewEntityDic;
private readonly Dictionary<BaseView, int> recycleTokenDic = new();
//使用的时候要先给画布赋值 这里我也在考虑是不是要换思路
//脱离mono没办法在构造函数里赋值
public Transform canvas;
private ViewManager()
{
viewTypeDic = new();
viewEntityDic = new();
}
/// <summary>
/// 打开面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
public void OpenView<T>(E_UIVIEWANIM anim_type = E_UIVIEWANIM.Fade) where T : BaseView, new()
{
//以类型为key 这样就不用写字符串了
if (viewTypeDic.ContainsKey(typeof(T)))
{
//曾经打开过但还没有销毁
//抹去清除标记后重新打开
var view = viewTypeDic[typeof(T)];
var obj = viewEntityDic[view];
CancelRecycle(view);
view.ViewOpenAnim(obj.transform, anim_type);
return;
}
else
{
//没这个类型就是没打开过
T view = new();
//添加进一级字典
viewTypeDic.Add(typeof(T), view);
//在这里调用Init方法 此时脚本会给AAkey赋值
view.OnViewInit();
view.AAkey = typeof(T).Name;
//生成UI实体
GameObject obj = null;
AssetsManager.Instance.LoadAsset<GameObject>(view.AAkey, (asset) =>
{
obj = asset;
obj.name = view.AAkey;
obj.transform.SetParent(canvas);
obj.transform.GetComponent<RectTransform>().offsetMax = Vector2.zero;
obj.transform.GetComponent<RectTransform>().offsetMin = Vector2.zero;
//父级画布可能有缩放 要确保自身的scale一定为1
obj.transform.localScale = Vector3.one;
if (obj != null)
{
//这里获取UI组件的实体
view.UIComponentsDic = obj.GetComponent<UITable>().UIComponentsDic;
//做绑定逻辑
view.OnViewBind();
//view与实体关联 二级字典
viewEntityDic.Add(view, obj);
}
//执行回调时调用加载完成
view.OnViewLoaded();
//执行动画
view.ViewOpenAnim(obj.transform, anim_type);
});
}
}
/// <summary>
/// 关闭一个面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
public void CloseView<T>(E_UIVIEWANIM anim_type = E_UIVIEWANIM.Fade) where T : BaseView
{
if (!viewTypeDic.ContainsKey(typeof(T)))
{
return;
}
else
{
var view = viewTypeDic[typeof(T)];
var obj = viewEntityDic[view];
view.OnViewClose();
view.ViewCloseAnim(obj.transform, anim_type);
//注册回收事件
var capturedView = view;
var capturedType = typeof(T);
int tokenSnapShot = NextRecycleToken(view);
StarCore.Instance.RegisterSecondAction(() =>
{
//校验token
if (!recycleTokenDic.ContainsKey(capturedView) || recycleTokenDic[capturedView] != tokenSnapShot)
{
return;
}
UnityEngine.Object.Destroy(viewEntityDic[capturedView]);
viewEntityDic.Remove(capturedView);
viewTypeDic.Remove(capturedType);
}, 5f, false);
}
}
/// <summary>
/// 尝试去刷新一个已经打开的面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
public void TryFlushOpenView<T>() where T : BaseView
{
if (!viewTypeDic.ContainsKey(typeof(T)))
{
return;
}
else
{
viewTypeDic[typeof(T)].FlushView();
}
}
public T TryGetOpenView<T>() where T : BaseView
{
if (!viewTypeDic.ContainsKey(typeof(T)))
{
return null;
}
else
{
if (viewTypeDic[typeof(T)].loadComplete)
{
return (T)viewTypeDic[typeof(T)];
}
else
{
return null;
}
}
}
private void CancelRecycle(BaseView view)
{
//令牌 如果取消了令牌自增
if (recycleTokenDic.ContainsKey(view))
{
recycleTokenDic[view]++;
}
else
{
recycleTokenDic[view] = 0;
}
}
private int NextRecycleToken(BaseView view)
{
if (!recycleTokenDic.ContainsKey(view))
{
recycleTokenDic[view] = 0;
}
recycleTokenDic[view]++;
return recycleTokenDic[view];
}
}
}参考文献
https://www.jianshu.com/p/8aa6f49e7f41