【Unity基础】ugui的基础知识篇

article/2025/10/6 16:27:35

文章目录

  • 前言
  • 一、常用用可视化控件
    • 1、Image
    • 2、RawImage
      • I.和Image的区别
    • 3、Text组件
    • 4、画布是怎么渲染出可视化UI的?
      • II.这里整理一下渲染相关的关系图,如下:
    • 5、关于画布的布局重构
  • 二、Button交互组件
    • 1、Button组件的源码以及使用方式
    • 2、Button组件是如何完成事件的交互的?
    • 3.还有一些交互性组件可以查阅官方文档
  • 三、UGUI合批
    • 1.什么是合批?为什么要合批?
    • 2.DrawCall越低越好吗?
    • 3.UGUI合批的规则
      • I.情况一
        • 解析:

前言

这里主要学习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一样时进行合批。
在这里插入图片描述


http://chatgpt.dhexx.cn/article/0oEEbeFy.shtml

相关文章

Unity之UGUI详解

UGUI 文章目录 UGUI六大基础组件概述Canvas对象上依附的&#xff1a;CanvasCanvas ScalerGraphic RaycasterRectTransform EventSystem对象上依附的&#xff1a;EventSystemStandalone Input Module Canvas画布组件Screen Space overlayScreen Space CameraWorld Space CanvasS…

using namespace std

整体认识 —— 解决命名冲突 一个简单的C程序&#xff1a; #include<iostream>using namespace std;int main(int argc,char **argv) {cout<<"hello world !"<<endl;system("pause"); // 让程序暂停,按任意键继续 注意&#xff0c;…

K8S:Namespace详解

Namespace概念 Kubernetes 支持多个虚拟集群&#xff0c;它们底层依赖于同一个物理集群&#xff0c;这些虚拟集群被称为命名空间。 命名空间 namespace 是 k8s 集群级别的资源&#xff0c;可以给不同的用户、租户、环境或项目创建对应的命名空间。 在创建pod的时候可以指定p…

TypeScript中的命名空间—namespace

TypeScript中的命名空间—namespace 什么是命名空间&#xff1f;在很多语言中都有这个概念。 命名空间是为了解决命名冲突。比如你在代码的不同地方&#xff0c;都定义了同名但是不同含义的函数、变量等&#xff0c;虽然不提倡这么做&#xff0c;但是有时候需要这么做。那怎么…

c++ 中的 namespace 用法

多人代码的整合&#xff0c;namespace 还是非常有用的。可以很轻松的避免变量与函数一样的命名 1. namespace 在 单个头文件 中使用 下面给一个简单示例演示命名空间和自定义头文件的使用&#xff0c;代码如下&#xff1a; compare.h&#xff1a; namespace compare{double…

【C++】命名空间(namespace) 以及理解using namespace std

命名空间 1.命名空间使用的背景1.背景 2.命名空间的定义&#xff08;namespace&#xff09;2.1正常的定义2.2 命名空间可以嵌套定义2.3允许命名空间相同 3.域作用限定符&#xff08;&#xff1a;&#xff1a;&#xff09;和命名空间的使用3.1域作用限定符&#xff08;&#xff…

C++ namespace

C namespace 一、什么是 namespace1.1 C语言标识符作用域的缺陷1.2 namespace作用 二、namespace的定义2.1 命名空间可以嵌套定义小技巧 2.2 可定义重复的命名空间 三、namespace使用3.1 命名空间名称作用域限定符使用3.2 using将命名空间中成员引入3.3 使用using namespace将整…

Linux namespace概述

操作系统通过虚拟内存技术&#xff0c;使得每个用户进程都认为自己拥有所有的物理内存&#xff0c;这是操作系统对内存的虚拟化。操作系统通过分时调度系统&#xff0c;每个进程都能被【公平地】调度执行&#xff0c;即每个进程都能获取到CPU&#xff0c;使得每个进程都认为自己…

C++ | 你真的了解namespace吗?

文章目录 一、前言二、命名冲突三、命名空间1、域作用限定符2、命名空间的概念&#x1f449;示例1&#x1f449;示例2 3、命名空间的定义4、命名空间的使用① 指定命名空间访问【做项目】② 使用using部分展开【做项目】③ 使用using namespace全局展开【日常练习】 5、小结 解…

using namespace std 介绍

using namespace std&#xff1b; 首先我们要知道&#xff0c;这句代码的意思是&#xff1a;打开标准命名空间&#xff0c;即告诉编辑器我们将要使用名字空间std中的函数或者对象。 using 意思就是正在使用的意思。namespace 的引用是为了解决不同space中命名相同导致命名冲突…

Namespace基本知识

published: true tags: C author: persuez Namespace C中namespace简单来说就是用来控制标志符&#xff08;如变量&#xff0c;函数&#xff0c;类等&#xff09;的名字冲突的。 简单术语 declarative region: 指标志符声明的区域。具体见图一。 potential scope: 指从该标…

【C++】命名空间namespace详解

一、命名空间的引入 C中&#xff0c;名称(name)可以是符号常量、变量、宏、函数、结构体、枚举、类和对象等等。而在大型工程中&#xff0c;难免会有重名的现象&#xff0c;命名空间namespace&#xff0c;就是C引入的一种解决名称冲突的机制 1.1 如何解决命名冲突 C语言中 …

namespace介绍

命名空间 命名空间 namespace 1、::作用域运算符&#xff08;表明 数据、方法 的归属性问题&#xff09; 2、命名空间 namespace 解决命名冲突 2.1&#xff1a;namespace命名空间的定义 2.2:命名空间只能全局范围内定义&#xff08;以下错误写法&#xff09; 2.3:命名空间可嵌套…

【C++】命名空间(namespace)详解

一、为什么使用命名空间 考虑一种情况&#xff0c;当我们有两个同名的人&#xff0c;Zara&#xff0c;在同一个班里。当我们需要对它们进行区分我们必须使用一些额外的信息和它们的名字&#xff0c;比如这个区域&#xff0c;如果它们生活在不同的区域或者它们的母亲或父亲的名…

Python中,线程threading详解

Python中最常用的同步有&#xff1a;锁/互斥&#xff0c;以及信号量。其中锁是最简单最低级的机制&#xff0c;信号量用于多线程竞争有限资源的情况。但是锁被释放&#xff0c;线程不一定被释放。 threading.Lock同步锁&#xff08;原语锁&#xff09; 通常使用获得锁&#xff…

【Python】多线程及threading模块介绍

​目录 1. 多线程简单介绍 2. threading模块介绍 2.1 threading模块常用方法 2.2 Thread类使用 2.2.1 使用构造函数传递可调用对象的方法创建线程 2.2.2 继承threading.Thread类 3. 多线程程序中使用(共享)全局变量 4. 多线程共享全局变量遇到的问题 5. 线程同步 5.1 …

threading库:Python线程的基础知识

目录 前言Thread对象区分线程守护线程自定义线程定时器线程线程间传送信号 前言 前面的subprocess库主要讲解的是进程知识与进程间的交互。而进程有可以拥有多个线程&#xff0c;所以threading库提供了管理多个线程执行的API&#xff0c;允许程序在同一个进程空间并发地运行多…

Python Threading 线程模块用法

一、什么是 Threading Threading用于提供线程相关的操作&#xff0c;线程是应用程序中工作的最小单元。python当前版本的多线程库没有实现优先级、线程组&#xff0c;线程也不能被停止、暂停、恢复、中断。 1.1、线程池图解 二、创建线程 导入模块threading&#xff0c;通过…

python中threading模块_python中threading模块详解

python中threading模块详解,threading提供了一个比thread模块更高层的API来提供线程的并发性。这些线程并发运行并共享内存。 下面来看threading模块的具体用法: 一、Thread的使用 目标函数可以实例化一个Thread对象,每个Thread对象代表着一个线程,可以通过start()方法,开…

python中的threading_python中threading的用法

threading提供了一个比thread模块更高层的API来提供线程的并发性。这些线程并发运行并共享内存。 下面来看threading模块的具体用法&#xff1a; 一、Thread的使用 目标函数可以实例化一个Thread对象&#xff0c;每个Thread对象代表着一个线程&#xff0c;可以通过start()方法…