建议大家都去看看参考文献的原文,会很有帮助。

对于大部分游戏来说,相比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)。

https://unity.com/cn/how-to/unity-ui-optimization-tips#when-using-a-fullscreen-ui,-hide-everything-else

这里提到了一种方案,就是直接禁用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

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

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

添加新评论