最近在做 Unity 的项目,负责 UI 相关的工作,学习了一下 Unity UGUI 更新的原理,以及优化相关的部分。本文主要参考 UWA 的分享,UWA 专注性能优化,感觉有很多值得学习的文章, UWA - 简单优化、优化简单 ,打好理论基础,少走弯路,后面实际项目中就是尽可能去实现这些细节了。
目录
-
- 元素更新方式
-
- Draw Call 合并规则
-
- 网格更新机制
-
- 降低界面的渲染开销
-
- 降低界面的更新开销
1. 元素更新方式
UGUI
有这样一个 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,导致资源的浪费。
参考文献: