UGUI最佳实践(1)-UGUI基础

在学习和使用UGUI之前,首先理解UGUI的各个组成部分非常重要,UGUI是由多个类和组件构成的系统。本章主要介绍一些相关术语,这些术语将贯穿整个系统,然后还会涉及到一些UGUI底层的知识。

术语

Canvas - 画布组件

Canvas组件是将其下的各个排序好的图形(Image、Text)提供给渲染系统,然后再由渲染系统将之显示到屏幕或者三维世界中。

Canvas首先对它下面的网格进行批处理(Batch),然后生成相应的渲染指令,最后将指令发送到Unity的图形渲染系统。所有这些工作都是在C++代码中完成的,这个过程称为rebatch或者batch build。当Canvas包含的网格需要重新进行批处理时,那么认为这个Canvas是脏的(dirty)。

嵌套在其他Canvas下的Canvas称为子画布(Sub-Canvas),子画布将它的子物体与父画布隔离开来,一个脏的子物体不会强制父画布进行重构,反之也一样。当然存在一些例外情况,比如:父画布尺寸变化引起子画布尺寸变化,这个时候父画布和子画布都需要进行重构。

Canvas Renderer 组件

上面提到画布对网格进行批处理,Unity利用 Canvas Renderer 组件将网格信息提供给Canvas。

Graphic 组件

Graphic 组件是 UGUI 的 C# 库提供的,不涉及本机代码导入,它是所有 UGUI 可绘制网格的基类,大部分内置UI图形都是通过继承 MaskableGraphic 基类来实现的,该基类允许通过 IMaskable 接口实现遮罩功能,主要的子类包括 Image、Text,两者都有同名的组件。

Layout 组件

Layout 组件控制 RectTransform 的尺寸和位置,通常用于创建需要相对布局的复杂界面,Layout 组件仅依赖和影响 RectTransform 组件,而并不依赖于 Graphic 对象,可以单独使用。

GraphicLayout 组件都依赖于 CanvasUpdateRegistry 类。该类跟踪需要更新的 Layout 组件和 Graphic 组件,当与它们相关的 Canvas 组件调用 willRenderCanvases 事件时,CanvasUpdateRegistry 类会根据需要触发更新。

Layout 和 Graphic 组件的这种更新过程称为重构(rebuild),后面将会详细介绍这一过程。

渲染细节

当搭建UI界面的时候,必须牢记一点,画布下的所有几何图形将在透明渲染队列(Transparent Queue)被绘制,这意味着,所有的图形总是从后向前进行 Alpha 混合。从性能的角度来看,有一点比较重要的需要知道,所有的栅格化之后的像素(图元)都会被采样计算,即使它被不透明的物体完全遮挡。在手机上,很高的 overdraw 将会超过 GPU 的填充率。

什么是overdraw?overdraw也就是过度绘制,是指在每个渲染周期内,屏幕上每个像素最理想只渲染一次,但是由于UI元素的重叠会导致像素会被渲染多次,每次渲染从CPU阶段到GPU阶段会消耗大量资源,如果这种情况比较严重,就会造成卡顿。

什么是填充率(fill rate)?填充率是指 GPU 每秒内能够渲染的像素的个数,单位是MPixel/S、GPixel/S,当每个渲染周期内需要渲染的像素超过GPU处理能力时(可能是场景太复杂、也可能是 overdraw 严重),就会造成丢帧。

批处理过程

批处理是通过 Canvas 组件将所有 UI 元素的网格进行合并,然后生成并发送渲染指令给 Unity图形渲染管线。批处理的结果将被缓存起来并重复利用(顶点缓存对象VBO),直到 Canvas 组件被标记为 dirty,这种情况一般发生在 Canvas 下的UI元素的网格发生了变化。

批处理过程中的网格来自其下的 CanvasRenderer 组件,但是不包括子画布下的物体。

网格合并过程中需要将网格按照深度进行排序、进行交叉测试、共享材质等计算。这个操作是多线程的,所以性能在不同的 CPU 上存在一定差异,这种差异在手机 CPU 和桌面 CPU 上尤其明显。

Rebuild 过程

在介绍Layout组件时,提过LayoutGraphic重新计算时,这个过程称为重构(Rebuild)。这个过程发生在 CanvasUpdateRegistry 类中,这是一个C#类,源码可以在 这里 找到。

在 CanvasUpdateRegistry 类中,比较核心的方法是 PerformUpdate,当 Canvas 组件调用 willRenderCanvases 事件时,PerformUpdate 就会被调用,willRenderCanvases 事件每帧都会调用一次。

PerformUpdate 方法的执行主要包括三个步骤:

  1. 脏的Layout组件必须进行重构(rebuild),通过调用 ICanvasElement.Rebuild 接口完成。
  2. 裁剪组件(如 Masks 组件)进行裁剪操作,通过调用 ClippingRegistry.Cull 方法完成。
  3. 脏的Graphic组件必须进行重构,也是通过 ICanvasElement.Rebuild 接口完成。

对于 Layout 和 Graphic 组件的重构过程,它被处理成多个阶段,Layout rebuild 过程包括 PreLayout、Layout、PostLayout;Graphic 的 rebuild 过程包括 PreRender、LatePreRender

Layout 组件重构

为了重新计算包含一个或者多个Layout组件的位置和尺寸,需要对 Layout 组件进行适当的层次排序,父物体的Layout组件可能会影响子物体的Layout组件的尺寸和位置,所以需要先计算父物体的 Layout 组件,要做到这一点,Unity会将Layout组件按照层次深度进行排序,高层级的(越接近根物体)会被排序到最前面进行优先计算。排序完成的Layout组件将会进行重构,这个过程完成了UI元素位置和大小的控制。如果需要了解Layout组件是如何影响各个UI元素的,可以用户手册的UI自动布局章节。

Graphic 组件重构

Graphic组件重构时,Unity 将处理方法(PreRender)传递给 ICanvasElement.Rebuild 接口,Graphic 实现了ICanvasElement 接口,在重构的 PreRender 阶段执行下面两个步骤:

  1. 如果顶点数据(vertex data)被标记为脏的(例如组件的 RectTransform 尺寸发生变化),重新生成mesh 对象。
  2. 如果材质信息被标记为脏的(例如组件的材质或者贴图发生变化),就会更新 CanvasRenderer 的材质。

与Layout rebuild不同的是,Graphic rebuild 不需要按照特定顺序进行,因此不需要进行任何的排序。