Unity基础ugui的基础知识篇

Posted 多年了酷拉皮卡依旧还在船上

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity基础ugui的基础知识篇相关的知识,希望对你有一定的参考价值。

文章目录

前言

这里主要学习UGUI的一些基础,先声明,这是个人学习笔记,非教程。有错欢迎各位指出
参考资料:https://docs.unity3d.com/cn/2021.3/Manual/UIVisualComponents.html

一、常用用可视化控件

1、Image

UGUI里面第一个可视化的组件应该就是Image组件了吧。

按照上面的截图来看,可以修改Image的贴图、颜色还有材质球。
按照上面的字段 Source Image 对应的None(Sprite) 来看,Image的贴图只能支持Sprite类型的贴图。
打开unity的Scene面板的wrieframe模式,截图如下

一个Image的渲染仅仅是由四个顶点,两个三角形组成一个面,再由片元着色器阶段的纹理采用对贴图进行采集而显示出来的。

当我们选择好一张Sprite放到Image里面就会出现以下选项:
~Simple - 均匀缩放整个精灵。
~Sliced - 使用 3x3 精灵分区,确保大小调整不会扭曲角点,而是仅拉伸中心部分。
~Tiled - 类似于 Sliced,但平铺(重复)中心部分而不是对其进行拉伸。对于完全没有边框的精灵,整个精灵都是平铺的。
~Filled - 按照与 Simple 相同的方式显示精灵,但不同之处是使用定义的方向、方法和数量从原点开始填充精灵。

2、RawImage

第二个可视化组件,RawImage组件。

还是可以修改贴图,颜色,材质球等,令人瞩目的是还可以修改UV,什么是UV?UV就是纹理贴图坐标,我们的贴图准确的贴到一个面片上面,完全是靠UV的。当UV还有图片自身的Wrap Mode的修改就能实现很多效果了。比如一张图片在一个面片上面晃动之类的。

还是打开wireframe模式,和Image一样的渲染方式,四个顶点,两个三角形,多了一个可以修改UV的操作,不过少了Image的九宫格操作。

I.和Image的区别

I.多了UV操作。
II.支持更多类型的贴图
III.功能没有Image复杂,仅仅展示图片的时候使用RawImage性能会更好。

3、Text组件

这也是会使用的非常多的可视化组件。截一张图,如下,

它提供的属性有文本、字体库、字体风格、字体大小、行间距、颜色等等。
再打开一下scene面板的wireframe模式:

发现,文本的渲染方式是一个字一张贴图。所以是说,Text组件提供上述的属性,然后在把这些字各自都生成一张贴图,那为什么一个text就只有一个drawcall呢?是因为unity在生成这些贴图之后,再把这些贴图放到一个图集里面去了,因为这样可以满足这些对象都使用了同样的纹理,同样的着色器,所有进行了一次合批。

4、画布是怎么渲染出可视化UI的?

按照以上的学习,我们学习了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);
                 
            
     --省略.....
    

II.这里整理一下渲染相关的关系图,如下:


总结: 画布渲染某个继承于Graphic类的组件时,有willRendererCanvasess对该组件通过自身的Rebuild方法进行调用,而Rebuild方法的主要职责是将自己的顶点数据、材质球和贴图放入本物体下携带的CanvasRenderer组件,并由这个组件对GPU发起drawcall。

5、关于画布的布局重构

由上面可以知道,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清除一下,就是把脏标记的元素给清除掉。

二、Button交互组件

这些组件可用于处理交互,例如鼠标或触摸事件以及使用键盘或控制器进行的交互。

1、Button组件的源码以及使用方式


看截图,主要是实现了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("测试一下打印");
    

2、Button组件是如何完成事件的交互的?

从上面可以知道,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方法。

3.还有一些交互性组件可以查阅官方文档

https://docs.unity3d.com/cn/2021.3/Manual/UIVisualComponents.html

三、UGUI合批

1.什么是合批?为什么要合批?

CPU在需要描绘一个物体时,准备好描绘这个物体需要的顶点数据(包含顶点坐标,UV,颜色等),并且通知显卡用这些数据进行描绘,一次这样的过程叫做DrawCall,也可以认为是一次批次。

描绘100个物体,每个物体都让CPU发送一次DrawCall,这无疑是对CPU是一次灾难性的消耗。可以把这100个物体合并到一个批次里面去,再通过一个DrawCall发送给显卡,这样对CPU的开销会小很多。

总的来说DrawCall的主要消耗硬件就是CPU,合批能大大减少CPU的占用率,还能提高GPU的吞吐率(同样的事件内,收到的顶点数据会更多)。

2.DrawCall越低越好吗?

当然不可能,任何东西都会有个度。虽然能drawcall低一些能让cpu的准备工作少一些,但是cpu和gpu之间的通讯是有带宽限制的,当drawcall非常大并且大于cpu和gpu的通讯带宽的时候,无疑是会影响到其他drawcall的。

3.UGUI合批的规则

两个UI元素必须要使用同一个shader和同一张贴图才能进行合批。(隐藏条件是还需要同一个深度才能进行合批)

I.情况一

如图所示,摆上三张图片,其中白色和蓝色之间拜访了一个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

解决方式:如下将b的深度变成0,和w、r一样时进行合批。

Unity基础(23)-UGUI

参考技术A 控件是UGUI内置的,控件上面因因包含不同的组件而不同。

关于按钮的事件统一管理方法

小练习:写个小框架滑动菜单

在实际使用UGUI开发的过程中发现一个UGUI的BUG:当Content下的子物体增加时,ScrollBar下的Handle滑条大小没有实时根据发生Content下的子物体数量发生变化。(在Hierarchy面板中右键创建UI->ScrollView,在子物体中找到Content,需要按行列布置的游戏物体都作为Content的子物体挂在Content下)(以开发垂直的ScrollView为例)在查找问题的过程中发现:我的这个项目里Content的高小于遮罩层Viewport的的高,致使ScrollBar滑条的size一直为1的状态。调整Content的高使高大于遮罩层Viewport的的高后又发现如下问题:在编辑模式下ScrollBar滑条的size只根据Content与遮罩层Viewport的大小比例进行了调整,而不是根据Content的子物体数量进行变换,致使了在Content下添加的子物体的总高大于Content设置的高时下拉滑条并不能全部显示的问题,并且在游戏运行时ScrollBar的Size又重新变回1了,无论怎么调整参数都无济于事。于是自己写了一个脚本,根据Content下的子物体的个数来控制Content的宽高(原理是修改RectTransform的sizedelta)

以上是关于Unity基础ugui的基础知识篇的主要内容,如果未能解决你的问题,请参考以下文章

Unity学习UGUI基础

时光煮雨 Unity3D实现2D人物移动-总结篇

零基础入门 Unity 之 UGUI 详解专栏 | 寻找C站宝藏

零基础入门 Unity 之 UGUI 详解专栏 | 寻找C站宝藏

Unity零基础到进阶 ☀️| UI系统学习UGUI布局元素 Layout Element 介绍 和 简单示例

Unity3D基础让物体动起来①--UGUI鼠标点击移动