这里主要学习UGUI的一些基础,先声明,这是个人学习笔记,非教程。有错欢迎各位指出
参考资料:https://docs.unity3d.com/cn/2021.3/Manual/UIVisualComponents.html
UGUI里面第一个可视化的组件应该就是Image组件了吧。
按照上面的截图来看,可以修改Image的贴图、颜色还有材质球。
按照上面的字段 Source Image 对应的None(Sprite) 来看,Image的贴图只能支持Sprite类型的贴图。
打开unity的Scene面板的wrieframe模式,截图如下
一个Image的渲染仅仅是由四个顶点,两个三角形组成一个面,再由片元着色器阶段的纹理采用对贴图进行采集而显示出来的。
当我们选择好一张Sprite放到Image里面就会出现以下选项:
~Simple - 均匀缩放整个精灵。
~Sliced - 使用 3x3 精灵分区,确保大小调整不会扭曲角点,而是仅拉伸中心部分。
~Tiled - 类似于 Sliced,但平铺(重复)中心部分而不是对其进行拉伸。对于完全没有边框的精灵,整个精灵都是平铺的。
~Filled - 按照与 Simple 相同的方式显示精灵,但不同之处是使用定义的方向、方法和数量从原点开始填充精灵。
第二个可视化组件,RawImage组件。
还是可以修改贴图,颜色,材质球等,令人瞩目的是还可以修改UV,什么是UV?UV就是纹理贴图坐标,我们的贴图准确的贴到一个面片上面,完全是靠UV的。当UV还有图片自身的Wrap Mode的修改就能实现很多效果了。比如一张图片在一个面片上面晃动之类的。
还是打开wireframe模式,和Image一样的渲染方式,四个顶点,两个三角形,多了一个可以修改UV的操作,不过少了Image的九宫格操作。
I.多了UV操作。
II.支持更多类型的贴图
III.功能没有Image复杂,仅仅展示图片的时候使用RawImage性能会更好。
这也是会使用的非常多的可视化组件。截一张图,如下,
它提供的属性有文本、字体库、字体风格、字体大小、行间距、颜色等等。
再打开一下scene面板的wireframe模式:
发现,文本的渲染方式是一个字一张贴图。所以是说,Text组件提供上述的属性,然后在把这些字各自都生成一张贴图,那为什么一个text就只有一个drawcall呢?是因为unity在生成这些贴图之后,再把这些贴图放到一个图集里面去了,因为这样可以满足这些对象都使用了同样的纹理,同样的着色器,所有进行了一次合批。
按照以上的学习,我们学习了Image组件,RawImage组件,还有Text组件,都知道所有可视化UI基本上都是通过网格的方式渲染出来的,那么这些UI元素是怎么通过管理然后显示出来的呢?
可以先点开Image类,然后发现Image类是继承于MaskableGraphic类,如下图:
然后再往下扒MaskableGraphic类,如下:
发现MaskableGraphic类由继承于Graphic类,并且在Graphic类的基础上实现了剪切、遮罩等功能。
然后再扒Graphic类,如下图所示:
发现Graphic类又继承于UIBehaviour,而UIBehaviour继承于MonoBehaviour(主要是用来获取Unity的生命周期函数的,这里的重点不在这里)。还有就是Graphic类还实现了ICanvasElement接口,按名字可知,所有的Canvas下面的UI元素最终都会实现这个接口。
再看ICanvasElement里面有一个Rebuild()方法。好,现在再点开Graphic类实现ICanvasElement的Rebuild方法,如下:
到了这里,就明白了。ugui是使用脏标记渲染的,而这个脏标记主要是m_VertsDirty和m_MaterialDiry两个,当m_VertsDirty标记为true时候,Graphic会对顶点数据进行刷新并且重构,当m_MaterialDiry标记为true时候,会把新的材质球和贴图放到自己携带的canvasRenderer组件里面,所有画布要渲染UI元素,就是从canvasRenderer里面获取材质球和贴图的。
那么,Canvas是怎么管理并且进行渲染的呢?来扒以下Canvas类,如下图:
点开后,发现它有一个willRenderCanvases的事件,再看看它被哪里引用过,如下图:
最后找到了,是被CanvasUpdateRegistry的单例进行管理的,再看看PerformUpdate的源码是干什么的?,以下为PerformUpdate函数并且再代码里面写注释:
--发现了PerformUpdate函数主要为以下步骤:
private void PerformUpdate(){
--步骤一,清除掉被销毁或者不存在的Canvas元素(指的是自己类里面的维护的集合的元素)
CleanInvalidItems();
--步骤二,按s_SortLayoutFunction的排序规则对m_LayoutRebuildQueue里面的UI元素进行排序,并且进行布局的重建
--省略......
--布局重构完成
--步骤三、进行渲染的重建
--省略........
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++){
for (var k = 0; k < m_GraphicRebuildQueue.Count; k++){
var element = m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
}
--省略.....
}
总结: 画布渲染某个继承于Graphic类的组件时,有willRendererCanvasess对该组件通过自身的Rebuild方法进行调用,而Rebuild方法的主要职责是将自己的顶点数据、材质球和贴图放入本物体下携带的CanvasRenderer组件,并由这个组件对GPU发起drawcall。
由上面可以知道,Canvas的willRendererCanvasess通过监听CanvasUpdateRegistry的PerformUpdate函数来进行重构的调用,而该函数里面也包含了重布局的代码,所以布局方面的代码也需要通过实现ICanvasElement接口对Rebuild方法进行调用的,再看看除了Graphic类实现了这个接口,还有一个LayoutBuilder类实现了这个接口。
看下LayoutBuilder是如何实现Rebuild()函数的,截一下图:
由上面源码可以得知,当CanvasUpdate执行到Layout的时候,会通过ILayoutElement方法计算自身水平方向布局,再通过ILayoutController对自身水平方向布局进行设置,垂直方向同理。
再看看具体实现了ILayoutElement接口的LayoutElement类,如下图:
然后发现布局和Graphic渲染一样,使用了脏标记渲染,再查看SetDirty是如何进行脏标记的,发现是通过调用LayoutRebuilder的MarkLayoutForRebuild方法进行脏标记的,再点开这个MarkLayoutForRebuild方法,如下图,
然后阅读一下源码,发现它是先找到当前进行脏标记的UI物体的父辈,然后把父辈再传到MarkLayoutRootForReBuild方法里面去,再看一下MarkLayoutRootForReBuild方法的源码,如下图:
由此可知道,布局的脏标记方式是通过把UI元素放到CanvasUpdateRegistry的m_LayoutRebuildQueue集合中,最后再由PerformUpdate函数来统一实现重构逻辑。每次重构完把m_LayoutRebuildQueue清除一下,就是把脏标记的元素给清除掉。
这些组件可用于处理交互,例如鼠标或触摸事件以及使用键盘或控制器进行的交互。
看截图,主要是实现了Selectable类,IPointerClickHandoler,ISubmitHandler等互动接口。
而Selectable主要是实现一些选中该组件时候的表现,比如鼠标放上去这个按钮时或者鼠标点击这个按钮时(这里不是指按钮绑定的事件),颜色加深等。
按钮的ButtonClickedEvent继承于UnityEvent,主要是用来绑定最终的执行函数。
然后再看看这个,如下图:
也就是说,点击一下鼠标左键是由Press函数去执行button绑定的最终的执行函数。
编写一下代码即可让button组件完成监听
private Button button;
private void Awake()
{
button = GameObject.Find("button").GetComponent<Button>();
button.onClick.AddListener(() => { printEvent(); });
}
void printEvent() {
Debug.Log("测试一下打印");
}
从上面可以知道,Button组件是通过实现IPointerClickHnandler接口来进行交互的,那我们可以看一下IPointerClickHnandler接口的OnPlointerClick方法在哪里会被调用呢?
在看看OnPlointerClick方法会被哪里调用
可以发现,ExecuteEvent类里面有一个EventFunction委托会对IPointerClickHandler进行接受,最终由Execute方法执行OnPointerClick()方法。
再看看Execute方法会在哪里被进行调用,可以发现,StandaloneInputModule会其进行调用,如下图所示:
参数PointerEventData主要是用来收集输入数据,数据的来源是来自于Input类。然后再看,这个ReleaseMouse方法是被UpdateModule调用,在看看这个UpdateModule。再看看这个UpdateModule类:
然后发现这个UpdateModule会做一些判断,判断先判断自身的m_InputPointerEvent是否由数据还没执行,如果有再调用ReleaseMouse()函数,再看看这个UpdateModule在哪里被使用,最后发现它是在EventSystem类里面的TickModule方法里面被调用。
然后由EventSystem的Update函数每帧运行一次,可以知道这个EventSystem在这一条调用链的过程中仅仅是一个输入模块管理和分发的角色。
难怪每次创建出UI相关的物体,EventSystem总是会自动创建出来。如果我们把EventSystem删掉,那么所有的UI事件都会无效。
再看看EventSystem的Update函数里面的这里,如下图
发现EventSystem的update函数会调用自己管理的输入模块的Process方法,也就是所谓的事件分发。在看看process方法里面每获取一次EventData都会调用EventSystem的RaycastAll方法,可见UI的数据获取都是通过射线获取UI元素,再触发UI元素身上上实现对应接口监听的方法来实现对应事件的触发的。代码如下,
总结:
1.首先,Button组件会去实现IPointerClickHandler接口,并且把该组件要绑定的函数放入IPointerClickHandler接口的OnPointerClick方法里面去。
2.EventSystem里面的生命周期Update()函数,会在每一帧去调用自己当前是输入模块的Process方法,主要作用是完成这个输入模块的各类事件发送。而这个一过程会调用到EventSystem的RaycastAll方法(所有输入模块的数据更新都是基于射线检查的),下下图。
3.输入模块会在每次Process的执行找到自己要执行哪个物体身上组件的对应的事件接口,比如找到一个IPointerClickHandler的组件那么就执行对应的组件的IPointerClickHandler的OnPointerClick方法。
https://docs.unity3d.com/cn/2021.3/Manual/UIVisualComponents.html
CPU在需要描绘一个物体时,准备好描绘这个物体需要的顶点数据(包含顶点坐标,UV,颜色等),并且通知显卡用这些数据进行描绘,一次这样的过程叫做DrawCall,也可以认为是一次批次。
描绘100个物体,每个物体都让CPU发送一次DrawCall,这无疑是对CPU是一次灾难性的消耗。可以把这100个物体合并到一个批次里面去,再通过一个DrawCall发送给显卡,这样对CPU的开销会小很多。
总的来说DrawCall的主要消耗硬件就是CPU,合批能大大减少CPU的占用率,还能提高GPU的吞吐率(同样的事件内,收到的顶点数据会更多)。
当然不可能,任何东西都会有个度。虽然能drawcall低一些能让cpu的准备工作少一些,但是cpu和gpu之间的通讯是有带宽限制的,当drawcall非常大并且大于cpu和gpu的通讯带宽的时候,无疑是会影响到其他drawcall的。
两个UI元素必须要使用同一个shader和同一张贴图才能进行合批。(隐藏条件是还需要同一个深度才能进行合批)
如图所示,摆上三张图片,其中白色和蓝色之间拜访了一个Text文本。其中白色图名为w,文本为t1,红色图为r,蓝色图为b
它产生的批次如下所示:
1.由于图片r是直接盖在图片w上面的,由因为r和w使用了同一贴图同一shader,所以会被unity合并到Batch0且认为深度为0
2.t1由于和其他元素使用了不同的贴图所以无法与其他元素进行合批,所以产生了一个Batch1的批次,且它会占有深度为1的深度
3.由于b是盖在t1上面的,并且t1和b无法进行合批,所以b的深度为2,因为深度不一样,无法与w,r进行合批,并且产生批次Batch2