NGUI内部架构
NGUI的核心架构,其实就在于3块:UIPanel、UIWidget、UIDrawcall。其中Panel和Widget是会显示在Hierarchy层级中的,而Drawcall则不会。
所以我们先来说前面2个。
Widget是NGUI中负责界面显示的基础单位。
所有需要在屏幕上显示出来的2D UI本质上都是一个Widget——包括Label、Sprite、Texture。
Widget作为一个整体,内部包含了渲染所需要的所有原材料:mesh面片,贴图,UV,材质,位置信息等等。
而Panel,就是一个用来管理Widget的控件。
每一个Widget都必定从属于一个Panel,不可例外——NGUI的根节点Root本身也包含一个Panel,而你不可能在Root之外创建Widget。
需要注意的是,Hierarchy中的UI层级关系,与NGUI自己内部的层级关系,并不一致。
比如这样一个结构。
Panel_D是Widget_2的儿子,是Panel_A的孙子。
但是对于NGUI内部来说:
Panel_A和Panel_D是平级的,Widget_2和Panel_D并没有父子关系。
在NGUI的架构中,首先存在一个静态的static List<UIPanel>,这个List包含所有的Panel
而每个Panel有两个属于自己的List:一个List<UIWidget>,一个List< UIDrawcall >
每个Panel都会在自己的子节点中向下寻找,把找到的Widget丢进自己的List中。
这个行为在每一次走到叶节点,或者遇到Panel的时候就会中断当前分支,跳到下一个分支。
比如说上图中Panel_A在找孩子的时候,走到Panel_D的时候就会中断当前分支,然后继续到Widget_3中去寻找。如此循环直到所有可获取的Widget都被装进List。
而对于Panel_D,也是如此。
其大致的结构图如下:
static List<UIPanel>内部包含一个Sort排序方法,会基于Panel的Depth进行一次排序。
而Panel内部也包含一个Sort排序方法,会基于Widget的Depth进行一次排序。
所以,虽然Panel和Widget都有Depth这个参数,但是这两个参数的地位是不一样的。
只要一个Panel的depth比另一个Panel小一丢丢,这个Panel内部的所有Widget都会被先处理。
好了,现在我们来聊聊Drawcall
一次Drawcall,就是CPU调用GPU干一次活。
如果要说得更具体一点的话,我们可以把GPU想象为一个庞大系统中高度独立,功能单一,但是非常重要的模块——比如说黑帮中专门负责砍人的部门。这个部门其他什么都不管,就负责接到指令,然后出去砍人。
而CPU,就是下达指令的单位。砍谁,去哪儿砍,需要什么物资,事前需要处理什么,事后需要处理什么,这些都需要CPU来安排。
CPU给GPU一次指令,GPU办一次事情,就是一次Drawcall。
Drawcall是衡量渲染负担的一个重要指标。而且通常情况下,主要就是衡量CPU的负担。
因为GPU是针对砍人这件事情高度定制优化的。一辈子就做这么一件事情。因为专注,所以效率。
而CPU就不一样了,要负担的责任太多,不可能针对某个单一需求高度定制架构。对于CPU来说,发10条指令,每条指令砍1个人,其负担要远远大于发一条指令一次砍10个人。
所以,优化Drawcall主要就是尽可能的合并指令,让一个指令包含尽可能多的内容,以此降低CPU的负担。
当然,有时候我们也会先让目标合体,比如说砍一个合体葫芦娃,比起砍7个小葫芦娃,CPU和GPU双方的工作量都可以减少。
好了,闲话扯完,最后我们来一句正经的总结:
所谓Drawcall,就是CPU加载并整理好美术资源(包括模型、贴图、材质等)及相关指令后,调用GPU进行工作的过程。
NGUI的Drawcall处理
当一个Panel的List<Widget>排序完成后,Panel就会根据List<Widget>来生成List<Drawcall>
List<Widget>中的第一个Widget必定会创建一个新的Drawcall
之后的每一个Widget都会拿出来和前一个进行对比
如果两者的material、texture、shader(下简称M/T/S)一致,则把后面这个Widget也丢给同一个Drawcall处理
如果两者的M/T/S有一丢丢不一样的地方,就创建一个新的Drawcall
也就是说,相同M/T/S且下标连续的Widget会共用一个Drawcall
而如果相同M/T/S的Widget中间隔着一个或多个不同M/T/S的Widget,就会拆分出许多额外的Drawcall。
如图,在Widget相同但深度排序不同的情况下,各自所产生的List<Drawcall>
注:各个Panel的List<Drawcall>最终会合并汇总一个静态的ActiveDrawcallList,最终供渲染时调用,不过这个不重要,反正也没有人会去动这里。
通常情况下,做好depth深度管理,尽可能的减少Drawcall就可以有效提高渲染效率。
但有一点需要特别注意的就是。对于CPU来说,调用Drawcall需要耗费时间,而构建Drawcall同样需要耗费时间。
NGUI在运行的过程中,如果某一个Panel下面有任意一个Widget进行了一点非常微小的变动:比如移动了一点点距离。那么这个Panel就会清空自己的List<Drawcall>,从头再遍历所有Widget,重新构建所有Drawcall。而这个过程显然是非常耗费性能的。
所以,有时候可以根据Widget的用途,将动态Widget和静态Widget拆分到不同Panel,如此一来虽然增加了Drawcall的数量,但最终结果却反而能提升渲染效率。
渲染序列
NGUI里的每一个Panel都有一个Render Q的设置选项。
RenderQ有两种主要模式:
Automatic —— 无参数
StartAt —— 需要设置一个整数参数
大多数时候我们不需要动这个东西。如果全部都默认的Automatic的话,NGUI会自己帮我们按照Panel的depth为第一优先级,Widget的depth为第二优先级来处理好渲染序列。
不过有时候我们难免会遇到想人为调整RenderQ的情况——比如说想在两个Panel之间插入特效之类的。这个时候可能就需要去设置RenderQ的type和数值。
对于StartAt模式的Panel,会以用户配置的参数作为第一个Drawcall的渲染序列,之后每个Drawcall递增1。这个很明确,不会有什么幺蛾子。
而对于Automatic的Panel,会以Max ( 3000 , ActiveList中的最大RenderQ+1)作为第一个Drawcall的渲染序列,之后每个Drawcall递增1。
比如说,如果当前最大序列是2500,那么处理到当前Panel的时候,渲染序列还是会从3000开始,每个Drawcall递增1;但是如果当前最大序列是3500,那么处理到当前Panel的时候,就会从3501开始,每个Drawcall递增1。
而如果用户想要在两个Panel之间插入想要渲染的东西,只需要把要插入的东西的渲染序列设置为渲染序列较高的那个Panel的StartAt值-1就可以了。
补充阅读——Hierarchy中的父子关系
前面我们说到,NGUI内部的逻辑和Hierarchy中的层级关系并不一致,就算把一个Panel挂在一个Widget下面,Panel也不会认Widget当爹。
不过在处理起来的时候,这个Panel还是会从自己的父对象那里去获取一些参数(U3D通用规则)。
比如说Panel_D,他还是需要从Widget_2那里去获取位置、缩放、旋转、Alpha,gameobjcet. Activeself等信息,以便处理自己的状态。
此外,值得注意的是,在当前最高版本的NGUI里,Panel还会一级一级向上的不断去尝试找另一个Panel,如果能找到,就会进行一次裁剪区域的交集处理。
比如Panel_D,他最终会找到Panel_A,然后用Panel_A的clip剪裁区域和自己的clip剪裁区域求交集,作为最终实际生效的裁剪区域。