作为一名游戏开发从业者,我在过去的10年里积累了很多项目的经验。 同时,我也想在大公司分享自己。 为此,我在2021年出版了《Unity3D高级编程:主流程笔记》一书,书中记录了我过去做过的所有项目各个模块的技术要点,包括框架、算法、原理、优化方案、等等(博客:)。 同时,通过这些经历,我深刻认识到引擎作为游戏开发的基础设施和底层逻辑的重要性。 多年来,我对发动机有了自己的理解。 近年来,通过对引擎的解剖和分析,我对其模块结构、流程、技术原理和实现方法有了深入的了解。
最近,我产生了一个想法,系统地解剖游戏引擎,所以我开始了这个行动。 我想更深入地了解引擎并通过这个分析过程来分享,所以我决定写一系列的文章来分析引擎的各个模块,包括结构、流程、技术原理和实现方法,以便结合起来使工作取得最大效益。 在解析引擎的同时,我也在自己的项目中使用了这些技术。
由于这几年我一直在努力从事性能优化相关的工作,包括性能工具开发、性能分析、性能优化以及框架结构改造升级等,所以对引擎中的架构也有了深刻的理解以及各发动机模块的工作原理。 优化工作的重要性。 事实上,“发动机技术”不再只是发动机。 为了描述方便,我这里将其统称为“引擎技术”。 同时,为了更好地理解引擎,在本系列文章中,我将用图来描述其原理、流程和结构。 ,加速您自己和读者对知识的理解。
我将这个系列命名为“图解游戏引擎”,希望通过“图表”来直观地诠释抽象技术。 写作的过程对我来说也是一个学习的过程,通过学习和复习对引擎的技术有更深入的了解。 本系列文章将系统剖析游戏引擎技术,涵盖引擎的历史、架构、功能框架、模块结构、执行流程、策略机制、公式算法、硬件结构等。另外,如果时间允许,一些自制引擎也将予以介绍。 案例完成发动机技术的实际实施。
目录
1.UGUI结构概述
2.UGUI图集及组件
2.1 Atlas算法
2.2 组件原理
3.UGUI输入机制
4.UGUI内核
4.1 UGUI核心结构
4.2 批量合并算法及流程
4.2.1 元素排序
4.2.2 元素批量计算
4.2.3 批量网格生成
5.UGUI渲染及材质
5.1 渲染过程
5.2 渲染材质
总结:UGUI是Unity引擎中内置的UI框架,用于创建2D和3D用户界面。 其框架功能包括UI分层、矩形变换、事件系统、内置UI组件、UI图集、材质和效果、UI画布和UI批处理。 UGUI框架由图集工具、业务组件和批量渲染三部分组成,分别负责图集的封装、具体效果的实现和批量渲染。
关键词:Unity引擎、UI底层框架、UGUI分析
内容
1.UGUI结构概述
在Unity引擎中,UGUI(Unity UI,也称为Unity GUI)是一个内置的UI框架,用于创建2D和3D用户界面。 UGUI框架提供了一系列UI组件和工具,使开发人员可以快速、轻松地构建用户界面。
(UGUI的框架特点)
其框架特点如下:
1)UI分层:UGUI采用分层结构来组织UI元素,这使得元素的管理和定位变得更加容易。 场景层次结构中的每个 UI 元素都呈现为附加有特定 UI 组件的 GameObject。
2)矩形变换(RectTransform):UGUI引入矩形变换来替代传统的Transform组件以支持2D布局。 RectTransform 组件定义 UI 元素的大小、位置和锚点,并支持锚点、边距和偏移等常见布局元素。
3)事件系统:UGUI提供了一个事件系统,用于处理和传输用户交互事件,例如点击、拖动、滚动等。该系统允许开发人员轻松编写UI元素的事件响应代码,以与用户交互。
4)UI组件:UGUI有许多内置的UI组件,例如按钮、文本、图像、滑块、滚动视图等,这些组件提供了基本的UI功能,使开发人员可以轻松构建用户界面。
5)UI图集:打包时自动生成UGUI图集,使元素共享相同的纹理和材质,可以提高渲染效率并减少draw调用次数,优化UI元素的批量渲染。
6)材质和效果:UGUI内置了一些材质和效果,开发者可以快速尝试UI效果,比如笔画、文字阴影等。
7)UI画布(Canvas):UGUI使用canvas来渲染整个UI。 它负责确定像素大小并处理视觉层次结构。 开发者可以将画布划分为屏幕空间和世界空间两种模式,以实现3D场景与UI的交互。
8)UI Ratching:UGUI使用一种渲染优化技术UI Batching,将绘制相同材质的UI元素合并到一个绘制调用中,以提高渲染性能。
(UGUI的示例显示)
UGUI框架由三部分组成游戏引擎编辑器,图集工具、业务组件、批量渲染。 这三个部分可以概括为:工具在编译打包时生成图集,业务组件在渲染时构建顶点,Canvas进行批处理并提交渲染。
(UGUI三部分)
如上图,三个部分的具体职责范围:
1.工具部分,负责图集的打包和可视化编辑
2、C#的组件部分负责具体效果的实现,主要是顶点、颜色、UV的逻辑组合。 除了网格之外,它还包括附加效果的实现。
3、C++的Core部分主要是Canvas提供的功能。 它提供批处理算法和渲染管道,因此负责网格渲染、批处理和引擎渲染调用。
2.UGUI图集及组件
2.1 Atlas算法
我们知道UGUI图集是在编辑器启动或打包时创建的。 标签用于识别同一图集中包含哪些图片。 同时,如果当前版本是ETC1,alpha会被拆分成单独的图集。 如果一个图集不足以容纳所有标记的图像,则会添加一个新的图集来容纳剩余的图像。
(示例图片来自画廊)
在上一篇文章中,我们了解到Unreal的图集算法通过划分上、下、左、右空间,以树状方式存储图集中的空闲空间,这样可以更方便地实时生成图集。 不过,由于Unity的UGUI图集的生成是在打包或者启动编辑器时生成的,实际上是一个一次性的生成过程,所以它的图集生成方法可以不那么麻烦,也更简单。 在生成图集之前,所有的图片都会被模拟并放入图集中。 放置时从上到下、从左到右遍历并填充到模拟图集中。 当所有的图片都填充到模拟图集中后,我们就得到了所有的图片。 在图集中的位置,图集的大小以及UV信息,然后图集就真正生成了,将之前的信息填入空白图集中。
UGUI依赖于Unity的自动图集生成工具:Sprite Packer或Sprite Atlas。 在Unity中氛围,这些工具负责将多个小纹理打包成一个大纹理。
Sprite Atlas 使用类似于 Rectangle Packing 算法的方法来生成图集。 贪心最大矩形算法(MaxRects)是该领域众所周知的图集生成算法之一。 该算法广泛应用于在保证打包速度的同时,尽量减小大图片的尺寸,以达到更高的空间利用率。
MaxRects算法的基本步骤如下:
1)首先获取所有填充图元素的尺寸,并按升序或降序对它们进行排序。 这确保算法从大到小或从小到大迭代纹理。
2)初始化一个容器,用于存储当前计算出的图集。 容器有一定的最大宽度和最大高度,可以根据需要设置。
3) 从已排序的纹理元素列表中按顺序插入每个纹理。 插入时,遍历图集像素中可以放置的位置,选择合适的插入位置。 适当的插入位置将优化装箱效果(例如最小浪费、最佳边界对齐等)。
4) 将选中的地图元素插入到找到的位置,并更新图集中的可用空间,循环直至所有地图元素都被填满。
5) 批处理完成后,对图集进行排列,填充所有输入的纹理元素,完成图集的生成。
(图集生成的示例图像)
Unity中的Sprite Packer或Sprite Atlas使用的算法与MaxRects略有不同。 UGUI会先模拟计算填充位置,计算完成后再进行填充处理。 同时,可以在UGUI中的图集查看器上选择不同的打包策略,方便开发者根据开发功能选择图集生成策略。
2.2 组件原理
(组件模块和核心模块概念图)
C#组件部分分为组件、布局、效果三个部分。 组件主要负责运行时的图像网格生成,布局主要负责方便地为UI元素提供自动布局功能,效果主要负责为UI元素提供一些额外的渲染效果。
元件组成:
元素组件以Graphic为基类进行扩展。 Graphic 负责 UI 元素的网格构建。 它构建了一些基本的逻辑处理供组件类使用。 例如,当UI组件的状态发生变化时,Graphic会将其标记为Dirty,以便在Canvas中进行处理。 Rebuild 在计算之前就知道哪些元素需要重建。
Graphic中的Dirty mark实现有三种类型,分别是布局Dirty mark、顶点Dirty mark、材质Dirty mark。 无论布局、顶点、材质或颜色发生变化,都会被标记以识别是否需要重建。
基于Graphic的派生类有很多,都是UGUI内置的元素,比如Image、RawImage、MaskableGraphic、Text等,都是我们开发中常用的元素。
(Element组件脏标流程图)
面膜成分:
Mask 和 RectMask2D mask 组件与其他普通组件不同。 它们独立于图形而存在。 它们本身不绘制任何图形,专门用来裁剪其下方子节点的图形,因此它们的存在相当特殊。
同时,Mask 和 RectMask2D 之间也存在一些差异。 他们的切割方法不同。 RectMask2D采用的是顶点裁剪的方式,即UI元素生成顶点时,对范围之外的顶点进行二次裁剪,而Mask则采用的是材质球Shader的裁剪方式,即在Cut时顶点裁剪是在GPU进行的执行顶点函数。
修饰成分:
效果类称为Modifiers,它们都需要继承IMeshModifier、IVertexModifier或IMaterialModifier来统一调用和修改元素。
Modifier效果包括外边缘、UV动画、UI阴影等。同时客户端程序员可以扩展Modifier组件来增强效果。
布局组件和剔除组件:
Layout 本地组件用于 UI 上元素的自动布局。 不幸的是,它的效率不高,因为它过于关注模块化封装,导致封装逻辑出现更多冗余。
Culling 裁剪组件专门用于 RectMask2D 框外的顶点裁剪。
其他扩展类和工具组件
用于网格数据缓存和填充的 VertexHelper
ObjectPool用于对象池的申请和释放
3.UGUI输入机制
发动机的输入机制都是相似的。 例如,大多数采用轮询(Windows除外,需要特殊处理)来获取输入数据,然后识别并处理不同类型的输入数据,如鼠标、触摸屏、键盘等,作为输入信号进行传输。 给出业务逻辑。
UGUI输入机制也遵循通用引擎的做法。 在引擎轮询中加入输入轮询,检查输入设备的运行情况,然后通过InputModule过滤成输入信号,传递给业务逻辑模块。 Unity和Unreal的区别在于输入后的碰撞测试算法不同。
UGUI的输入机制可以简单概括为:
1)用户与屏幕进行交互(如点击、拖动等)。
2) 输入轮询获取输入设备数据。
2)根据平台和输入设备的不同,InputModule处理这些输入事件并将其转换为相应的UGUI事件。
3)EventSystem对这些输入事件进行碰撞测试,获取正确的UI碰撞元素并发送。
4) 接收事件的UI元素触发回调函数或根据这些事件执行内置行为。
输入机制主要依赖以下几个关键组件:
1)EventSystem:EventSystem组件是UGUI中负责输入处理的主要组件。 它用于将输入事件(如触摸、单击、拖动等)分配给UI元素,因此场景中只需要一个EventSystem实例即可处理所有输入事件。
2)InputModule:InputModule是EventSystem的子组件,负责将特定类型输入设备的输入数据转换成UGUI可以理解的事件。 InputModule 的示例包括:StandaloneInputModule(用于独立平台,例如 PC 和游戏机)、TouchInputModule(用于触摸屏设备)和 CustomInputModule(用于自定义输入处理)。 InputModule 根据特定平台和输入类型处理输入事件。
3)GraphicRaycaster:GraphicRaycaster是负责碰撞测试的组件。 由于UGUI使用碰撞测试来计算响应元素,因此输入点击和释放需要GraphicRaycaster通过射线碰撞和矩形点计算来确定最终的响应UI元素。
4)UI元素(如Button、Slider、Toggle等):这些元素是UGUI系统的基本组件。 它们具有与用户交互的能力,可以通过EventTrigger组件或内置API(如onClick、onValueChanged等)响应输入事件。 当InputModule检测到输入事件时,它会将事件发送到具有IPointerClickHandler和IPointerEnterHandler等接口的UI元素。 这些元素将处理这些事件并调用相关的回调函数。
5)事件触发器(EventTrigger):它允许将开发人员定义的事件监听器附加到特定的游戏对象。 例如,当用户点击某个Button时,EventTrigger会触发指定的回调函数。 这使得开发人员能够处理更复杂的 UI 交互,例如自定义动画、声音或控制器事件。
与Unreal不同的是,Unity的UGUI不使用Grid光栅存储,而是直接使用碰撞测试和图形点计算来计算碰撞响应元素。 步骤是:
1)首先从Canvas中删除所有需要响应点击的UI元素
2)依次对这些元件进行碰撞试验。 碰撞测试有两种类型。 一是矩形内点的计算判断,二是利用Ray射线进行碰撞测试。
3)将所有发生碰撞的UI元素按深度排序,并响应排名第一的UI元素,即传递输入事件信息。
4) 不同类型的UI元素根据自身元素组件的逻辑功能做出自己的响应动作。
(输入模块关系图)
UGUI的点击、拖动、文本输入最终来源于引擎的输入组件Input。 引擎的Input组件本身提供了鼠标点击事件、手指点击事件、点击次数、拖动位移详情等数据。EventSystem是UGUI输入模块的起点,也是管理类。 它负责采集输入信号并将输入信号分配给相应的模块进行处理。 因此,它是所有输入逻辑处理的中转站游戏引擎编辑器,也是输入源的管理者。 UGUI将Input的输入数据和接口封装成标准输入模块和触摸屏输入模块。 EventSystem会在每个更新帧期间更新当前的输入模块,以获取输入信号并做出响应。
获得输入信号后贴图笔刷,会进行点击位置与元素的碰撞测试,这就是GraphicRaycaster组件的工作。 输入点的碰撞测试有两种类型。 一是判断2D点是否在元素的矩形范围内来判断该点与UI元素是否发生碰撞。 另一种是直接使用Ray射线的物理碰撞测试。 非3DUI碰撞测试主要基于第一类,即2D点是否在元素范围内。 同时,每个继承Graphic的可碰撞元素都需要重写Raycast函数。 该函数用于判断输入点是否在当前UI元素的范围内。 里面。