最近在做 Unity 的项目,负责 UI 相关的工作,学习了一下 Unity UGUI 更新的原理,以及优化相关的部分。本文主要参考 UWA 的分享,UWA 专注性能优化,感觉有很多值得学习的文章, UWA - 简单优化、优化简单 ,打好理论基础,少走弯路,后面实际项目中就是尽可能去实现这些细节了。

目录

    1. 元素更新方式
    1. Draw Call 合并规则
    1. 网格更新机制
    1. 降低界面的渲染开销
    1. 降低界面的更新开销

1. 元素更新方式

UGUI

public class VertexHelper:IDisposable
{
    private List<Vector3> m_Position = ListPool<Vector3>.Get();
    private List<Color32> m_Colors = ListPool<Color32>.Get();
    private List<Vector2> m_Uv0S = ListPool<Vector2>.Get();
    private List<Vector2> m_Uv1S = ListPool<Vector2>.Get();
    private List<Vector3> m_Normals = ListPool<Vector3>.Get();
    private List<Vector4> m_Tangents = ListPool<Vector4>.Get();
    private List<int> m_Indices = ListPool<int>.Get();
}

有这样一个 VertexHelper 类,和 UI 元素有一一对应的关系,包含顶点信息,UV,颜色,等等 当 UI 元素发生变化的时候,就会从位置,长宽等数组填充这些 list。

对制作的影响

当 UI 发生改变的时候,须要对数组的元素进行更新, “动态元素” 少用 Outline,Tiled Sprite 尽量减少 “动态” 长文本

如上图 Tiled 生成了大量网格,在填充的时候耗时更长。 OutLine,是通过把一个四边形重复 5 次,画出的 OutLine 的效果,会使文本的定点数乘以 5,使更新的数组过长。

更新方式

  • UIPanel.LateUpdate

    • 轮询
    • UIPanel.UpdateWidgets
  • Cavans.SendWillRenderCanvas

    • 队列
    • m_LayoutRebuildQueue
    • m_GraphicRebuildQueue

NGUI 每帧更新 UIPanel,轮询,不管发生变化与否,哪怕是静态的,还是会有开销

UGUI 更新包含 2 个队列,渲染之前在 SendWillRenderCanvas 的回掉里面处理 2 个队列的元素,如果大量静态,消耗几乎为 0。

对动态 HUD 缓存机制的影响

  • NGUI

    • 适量元素:Color.a= 0,移出
    • 大量元素:SetActive(false)
    • Time + 二级缓存
  • UGUI

    • Scale = 0, Alpha Group = 0

如血条,伤害数字,经常会出现消失的 UI 元素,如果出现就创建,消失就 destory,开销会非常大。所以通常的做法通过缓存,如果通过 SetActive 有时候会有额外的开销,

UGUI 通常的操作方式可以通过 scale = 0 ,或则 Alpha Group 为 0,可以快速隐藏,不要直接 alpha = 0 ,在 draw call 上是没变化的,实际上还是画了个透明度为 0 的面片。

NGUI 中和 UGUI 相反,如果设置 alpha = 0 ,是会把顶点移除掉,可以减少 setActive 的开销。

2.DrawCall 合并规则

渲染顺序

  • NGUI: Depth

    • 设置 depth 值,以 UIPanel 为单位,按照大小进行排序,相同材质进行合并
  • UGUI:hierarchy

    • 重叠检测
    • 分层合并

存在优势,也有一些问题,UGUI 的合并规则是进行重叠检测,然后分层合并。下面的例子中,不同颜色代表不同图集。

第一个图,4 种颜色,左边和右边数序相同,蓝色是 0 层,白色都是 1 层,这样会分层合批成 4 个 DrawCall。

第二个图,左边的蓝色是 0 层,右边的黑色是 0 层蓝色是 1 层,这种情况下不会合批,所以会是 9 个 drawCall

第三个图,把黑色延长到重叠的地方,黑色同处 0 层, 所以 DrawCall 又降到了 5。

所以在制作 UI 的时候,须要考虑层级关系,结合 UGUI 的合批规则,这样可以达到对 drawCall 的优化,

调试工具

  • NGUI:DrawCall tool
  • UGUI:Frame debugger

NGUI 可以通过 DrawCall tool 看到多少个三角面,多少个 widgets,通过观察 widgets 的关系,对 NGUI 层级直接调整,来进行合批。

NGUI 使用 drawcall tool,通过调整 index,把相同材质的放在同一层。

UGUI 用 frame Debug 看每个 drawcall 绘制了哪些东西,再做调整

对界面的影响

  • UGUI

    • 不规则图标的摆放
    • UI 元素的旋转
    • 动态遮挡
    • 3D UI
  • NGUI

    • 手动排序

UGUI 中,对于不规则图形,视觉上 icon 没有重叠,但是 UI 层是包围盒的形式,Icon 重叠了,UGUI 在判断的时候没办法进行合并。

UGUI 对于发生旋转的 UI,包围盒是会发生重叠,会限制 UGUI 在合并 DrawCall 的操作。

如下图:

NGUI 把不同的元素设在一个图集中,进行同批次绘制。

3. 网格更新的机制

  • UIPanel.LateUpdate 两种更新方式

    • UIPanel.FillDrawCall 更新单个 DrawCall
    • UIPanel.FillAllDrawCall 更新所有 DrawCall
  • Canvas.BuildBatch 更新所有 DrawCall

    • WaitingForJob 子线程网格合并
    • PutGeometryJobFence
    • BatchRendere.Flush UI 如果开多线程渲染,BatChRender.Flush 会增高,主线程在等待子线程的结果时 Flush 会等待。

NGUI 根据不同的 DrawCall 合并不同的网格 UGUI 以 Canvas 为单位,一个 Canvas 下的元素,合并成一个 Mesh,不同的 UI 元素会以 SubMeshes 的形式存在。UGUI 中如果一个 Canvas 中有很复杂的动态元素,尽量将静态元素拆分出来,确保更新的效率。

优化方法:

  • UGUI

    • 拆分 Canvas
  • NGUI

    • 控制 FillAllDrawCalls
    • 拆分 UIPanel

性能比较

  • 功能界面的 DrawCall 控制 NGUI>UGUI (NGUI 通过 DC 树,通过调整 Index 进行调整)
  • 功能界面的网格更新机制 NGUI>UGUI (UGUI 更新任何一个 UI,都会更新整个 Canvas)
  • 动态 HUD 界面的网格更新机制 UGUI>>NGUI (UGUI 在处理动态 UV 的元素,如血条,动态 UI 会更有优势)
  • 堆内存控制 UGUI>>NGUI (NGUI 堆内存占用更高)

参考 https://blog.uwa4d.com/archives/Implosion.html

4. 降低界面的渲染开销

  • Profiling 定位
  • DrawCall 控制
  • Mesh.CreateVBO UI 变化的网格开销
  • Overdraw UI 比较容易产生 Overdraw

Profiling

UGUI 非多线程渲染 Unity5.3 主要集中在 RenderSubBatch,

DrawCall 控制

Z 值!=0

合并时只会合并相邻层级,相同图集的元素

左边的图,4 个血条红色和白色的 z 值相同,共 2 个 drawcall,但是右边的图,红色和白色穿插,变成 8 个 drawcall,在 3D UI 的时候尤其明显,2DUI 不要通过这种方法,改 Z 值,因为 2D 改了之后,

未 “隐藏” 的元素

包含 Null Sprite, Color.a = 0 屏幕外

对于隐藏的元素,NGUI 的 image 组件中,alpha 为空和 sprite 为空,都是占用 drawcall 渲染的,而且会打断前后的 drawcall,穿插在上下 2 个元素中间的时候。

Hierarchy 穿插 + 重叠

如下图红点和 Icon 在不同图集中,如果红点稍微大一点,遮挡了旁边的 Icon,就不能合批,须要调整 Icon 和红点的节点关系,4 个 Icons 放在一个节点下,4 个红点放在一个借点下。在同步位置的时候可能稍微麻烦有点,须要写个脚本同步位置。

图集分离

可能因为压缩方式的不同,导致 UI 的 sprite 在不同图集中,也会影响渲染开销,不同图集中无法进行合批

OverDraw

  • 减少 UI 层叠
  • 遮挡场景时,关闭场景相机
  • 不用 Image 检测事件

参考: https://blog.uwa4d.com/archives/video_UI.html

5. 降低界面的更新开销

  • 动静分离
  • 降低更新频率
  • 避免 “敏感” 操作
  • 优化选项

动静分离

在 UGUI 中细分 Canvas 下图中,血量和经验条会经常更新,如果在一个 canvas 中,PutGeometryJbFence 和 WaitngForJob,buildBatch 出现的时候,表示更新的开销在子线程中,主线程处在一个等待的状态,差不多有 5,6 毫秒的等待。

拆分之后刚才的 WaitingForJob 等都没有了,动态的 canvas 开销就会很小。

降低更新的频率

  • 设定移动阈值
  • 设定更新频率

比如像小地图这样的界面, 可能移动了一小段距离,小地图上更新了也不明显,可以通过设定阈值的方法,降低开销,或者直接设定更新时间。

避免 “敏感” 操作

  • 元素的 Position 赋值 Canvas.BuildBatch

下面的一个例子是在 Canvas 中,所有元素基本是静态的,但是有个元素,在 Update 中,会跟随 target 的 position,每次发送改变的时候,会重建整个 canvas,导致资源的浪费。

参考文献

https://blog.uwa4d.com/

[《聚爆 Implosion》性能精析 UI 部分]

UGUI 研究院之全面理解图集与使用