背景
作为一名游戏开发从业者,在很多项目中积累了些技术经验涵盖了从业务到语言到框架到引擎的各个方面,同时在这些经历中我深刻认识到引擎作为游戏研发的基础和依赖的重要性。多年下来对引擎也有了自己的认知,并通过对引擎的剖析和分析深入了解了它的模块结构、流程、技术原理和实现方式。
最近萌生了一个想法,想要对游戏引擎进行彻底的剖析。我想通过这个过程,更深入地理解引擎并分享它们。因此我决定写一系列文章来剖析引擎的模块结构、流程、技术原理和实现方式。同时也分析当下热门的开源引擎和商业引擎中的结构。
由于过去几年中一直致力于性能优化相关工作,包括性能工具研发、性能分析、性能优化和框架结构改造等。在这个过程中深刻认识到理解引擎中的架构和引擎各模块的运作原理对于优化工作的重要性。因此为了更好的理解引擎,在这个系列的文章中将用图的方式来描述原理、流程和结构,以加速自己和读者们的理解。
我将这个系列命名为《图解游戏引擎》,希望通过‘图’形象化解读技术。同时写作过程也是我的学习过程,通过学习和回顾更深入地理解引擎的技术。在这个系列的文章中将从系统性的视角来分析游戏引擎,涵盖包括历史、架构、框架、模块、流程、算法等角度,同时也会通过些自制引擎的案例来完成引擎技术实践。
目录
1、UMG框架结构
1.1 UMG框架
1.2 UMG结构
1.3 UMG动画
2、Slate UI框架结构
2.1 Slate结构
2.2 Slate控件和自定义
3、UI渲染结构和流程
3.1 渲染结构
3.2 渲染流程
3.3 渲染流程细节1
3.4 渲染流程细节2
4、UI合批和图集生成算法
4.1 UI合批算法
4.2 UI图集生成算法
5、UI输入响应机制
摘要:
UMG是UE4内置的UI框架,提供UI设计和交互的工具,包括UI编辑器、预制UI组件、蓝图系统等。它的主要特点包括可视化UI编辑器、蓝图系统集成、预制UI组件库、动画支持、响应式设计、自定义样式、绑定和事件系统。UMG集成了蓝图系统、预制UI组件和可视化布局编辑器等更侧重于面向游戏设计师提供便利的UI设计工具。同时UMG基于Slate构建,Slate是UE4中用于创建UI的底层C++框架游戏引擎教程,提供基本组件和控件。UMG的动画系统是构建在MovieScene过场动画之上的,通过UUserWidget播放UI动画。在Slate中,SWidget是基础元素,控件可以继承自SWidget,重载运算符来组合自定义控件。
关键词:Unreal引擎UI,UMG框架结构,Slate框架,UI空话,UI控件
内容
1.UMG框架结构
Unreal Motion Graphics(以下简称UMG)是 Unreal Engine 4(UE4)内置的 UI(用户界面)框架。
1.1 UMG框架
UMG 为游戏或应用程序的 UI 设计和交互提供强大且灵活的工具,包括一个可视化的 UI 编辑器、预制 UI 组件以及集成的蓝图系统,UMG 的主要特点如下:
1)UMG 编辑器。
UMG 提供了一个所见即所得的 UI 编辑器,它允许开发者在一个视觉交互式的环境中设计和布局 UI 元素,无需手工编写代码。
2)蓝图系统集成。
UMG 与 UE4 的蓝图系统紧密集成,使得开发者在无需编程的情况下定义 UI 逻辑、交互和数据绑定。这样非程序员(如游戏设计师)也能轻松创建 UI 功能。
3)UI 组件库。
UMG 提供了一系列预制 UI 组件(也称为 widget),如按钮、图片、文本框、滚动列表、进度条等。这些组件允许开发者轻松创建复杂且富有吸引力的 UI。此外,还可以通过继承现有组件,创建自定义 UI 控件。
4)动画支持。
UMG 拥有专门的 UI 动画系统,开发者可以使用它为 UI 组件创建动画,如平移、缩放、旋转和淡入淡出等,这能让 UI 更具有吸引力和生动感。
5)响应式设计。
UMG 支持响应式布局,在多种屏幕尺寸和分辨率下保持 UI 元素的布局和可读性。这样可确保 UI 在各类设备(如手机、平板、PC)上均呈现出恰当的界面效果。
6)自定义样式。
UMG 允许开发者为 UI 控件和元素自定义样式,包括颜色、字体、背景图像等,以实现游戏或应用程序的独特视觉效果。
7)绑定和事件系统。
UMG 提供了 UI 与游戏逻辑之间的数据绑定和事件处理机制。例如,将游戏中的分数绑定到 UI 的分数计数器控件,或者为 UI 按钮添加交互事件。
通过 UMG开发者能够快速地为游戏和应用程序创建富有吸引力的用户界面,使得开发过程更加高效,同时提供更好的用户体验。
UMG的背后是Slate,UMG和 Slate 分别代表了两个层次的用户界面(UI)框架,UMG 是面向设计者和程序员的高级框架,而 Slate 是底层的 UI 框架,主要用于程序员编写富有交互性的 UI 元素游戏引擎教程,UMG 在底层实际上是基于 Slate 构建的。
Slate 是 UE4 中用于创建 UI 的底层 C++ 框架。它提供了一套用于创建、布局和处理用户界面的基本组件和控件。通常在游戏引擎的编辑器、工具和原生 UI 组件等方面使用 Slate。在开发低层次的 UI 控件、功能或与引擎紧密集成的自定义 UI 时,程序员会使用 Slate。由于Slate并不可视化,因此需要我们在代码里面手动一行行的敲(链式编程),UI制作效率较低需要UMG做可视化的友好支撑。
UMG 是基于 Slate 的高级 UI 框架,为开发者提供了引擎的友好和可视化的 UI 创建工具。UMG 集成了蓝图系统、预制 UI 组件(如按钮、文本框、图片等)和可视化布局编辑器等。UMG 更侧重于面向游戏设计师提供便利的 UI 设计工具。在游戏界面、HUD 和一般 UI 制作方面,UMG 是更常用的选择。UMG 作为高级框架游戏角色,仅需处理视觉设计、界面逻辑和事件响应,然后在底层通过 Slate 进行实际的 UI 操作和渲染。在使用 UMG 时,该框架会自动处理与 Slate 的底层交互,而且 UMG 在底层会将 UI 控件和布局映射到对应的 Slate 对象。
我们在开发GamePlay的UI时用的最多的就是UMG,在使用过程中不会感觉到Slate的存在,UMG里面都是封装的Slate代码,在UMG编辑器中可以通过修改控件的方式设置属性实际上就是在对Slate进行修改。UMG 为开发者提供高级、直观的 UI 绘制工具,适合处理游戏中的 UI,而 Slate 作为底层框架,用于开发低层次的 UI 功能、自定义控件和引擎工具等。
(图1 - UE的UI架构图)
如上图,UMG主要任务是设计编辑器和蓝图以及一些业务的对接,它被背后是Slate,Slate又分核心和应用(同一个框架下的不同文件夹拆分),Slate Core核心是构成 Slate 框架的基础部分,包含许多核心类、数据类型和辅助功能,Slate 则使用这些基础建设创建实际的 UI 控件、布局机制、渲染流程等。简单来说Slate Core 是 Slate 框架的基石,Slate 构建于此基础之上提供实际的 UI 功能,UMG又在Slate基础上做封装服务于设计师和开发者,通过可视化的编辑器和蓝图提供便利和丰富的UI制作体验。
同时Slate将渲染放在渲染线程中通过SlateRHIRenderer向渲染线程推送绘制指令,渲染线程中通过SlateRHIRenderer的DrawWindow_RenderThread来执行合批计算、绘制指令生成和图形接口调用,最终绘制到屏幕上。
1.2 UMG结构
UMG的整体结构以UWidget为基础类,子类扩展后引用了Slate组件并对Slate做属性设置和同步,同时UUserWidget继承了UWidget的同时对UI组件的蓝图交互、自定义UI组件以及UI间跳转提供了方法,如图2。
(图2 - UMG应用层组件结构图)
如图2所示,展示了UI 类的设计结构遵循的层级和继承关系:
1)UWidget
UWidget 是所有 UI 控件和组件的基类,它是一个表示可视控件的抽象类,所有 UMG 控件和整个 UI 层次结构都继承自此类,UWidget 层级包含布局信息、属性、事件回调等与 UI 控件相关的信息。
2)UUserWidget
UUserWidget 类是 UI 面板和 HUD 的基类,UUserWidget 提供了添加、删除和管理 UI 控件的功能。通常,在 UMG 编辑器中创建 UI 面板时,这些面板(如主菜单面板、设置面板等)都是从 UUserWidget 类衍生出来的。
3)UWidgetBlueprintLibrary
UWidgetBlueprintLibrary 是一个静态帮助类,它包含许多用于操作或查询 UI 控件和属性的静态方法,这些方法在蓝图和 C++ 中非常有用。
4)UPanelWidget
UPanelWidget 类继承自 UWidget,表示容纳和管理子控件的容器,用于实现各种布局策略,如堆叠、网格和自定义布局。常用的子类包括 UCanvasPanel、UHorizontalBox 和 UVerticalBox 等。
5)UButton、UImage、UBackgroundBlur等这些类分别对应于 UMG 中的 UI 控件,如按钮、图片、模糊背景控件等。
这些类共同构成了 UMG 类的设计结构,这种分层结构有助于轻松添加和管理 UI 控件并简化 UI 设计过程。此外,引擎内部许多其他类与这些主要类相关联以完善 UMG 功能,例如 UWidgetAnimation(为 UI 控件提供动画)、UBinding(用于数据绑定)等。
1.3 UMG动画
下面我们来看看UMG中的UI动画时如何实现的。
(UMG中的UI动画结构图)
上图中是UMG的动画结构,其中UUserWidget最常用的面向用户的元素组件,它从SWidget派生并封装了很多功能,是用户界面部分的基类,它充当了创建、编辑和运行时显示用户界面的核心,同时也拥有UI动画的播放接口。
UUserWidget通过UUMGSequencePlayer播放UI动画,UUMGSequencePlayer 包含了 UWidgetAnimation 的引用游戏评测,而 UWidgetAnimation 包含了动画的时间线和所有关键帧信息即要播放的动画序列。同时 UUMGSequencePlayer 维护了动画的播放状态,例如播放速度、当前时间、是否暂停等,这些属性都可以通过蓝图和代码进行控制。
UWidgetAnimation背后有一个庞大的系统即 MovieScene 系统。MovieScene是一个用于创建基于时间轴的动画序列的系统,虽然它主要用于处理电影场景(过场动画),但也作为许多引擎功能的底层,例如UMG的UI动画、角色动画和关卡序列。
尽管MovieScene并不是一个严格意义上的ECS结构,但它的设计理念与ECS很多相似。MovieScene支持动画序列的时间线驱动,通过轨道(类似组件)处理动画关键帧数据,并使用播放器来执行动画计算。这种扩展性和模块化设计使得MovieScene可以适用于各种动画需求,强化了UE中基于时间轴的动画制作。从UE4.26开始MovieScene开始融合传统的基于关键帧的动画序列系统和实体组件系统(ECS)的概念,这种新的结构被称为MovieScene实体系统,新的实体系统能够提供更高效的动画运行时行为及灵活性。其中关键的基础实体为UMovieSceneEntitySystem,每个派生自 UMovieSceneEntitySystem 的实体负责特定类型的实体集合并处理相应的逻辑。
总的来说UMG的动画系统是构建在MovieScene过场动画之上的,应用层上UUserWidget封装了接口提供给用户调用并引用了UI动画封装类UUMGSequencePlayer,UUMGSequencePlayer提供了动画接口给用户并引用了UWidgetAnimation,而UWidgetAnimation的背后则是由MovieScene过场动画系统支撑,MovieScene这是个庞大而又高效的系统,它支持并行同时易于扩展。
2、Slate UI框架结构
Slate分核心和应用,Slate Core核心是构成 Slate 框架的基础部分,包含许多核心类、数据类型和辅助功能,Slate 则使用这些基础设施创建实际的 UI 控件、布局机制、渲染流程等。
2.1 Slate结构
Slate结构主要围绕控件设计,以下是 Slate 中重要类和组件:
1)SWidget
Swidget是Slate 中所有组件和 UI 的基类,几乎所有别的控件类都继承自这个类。
2)SCompoundWidget
一个复合组件,由多个子 UI 元素组成,开发者通常从这个类继承以创建自定义 UI。
3)SLeafWidget
一个不包含子控件的 UI 元素。例如,按钮或填充区域。
4)SlateWidgetStyle
SlateWidgetStyle定义了样式信息的类,用于管理控件的样式和主题。
5)FSlateApplication
FSlateApplication是 Slate 的核心应用类,负责事件处理、输入、渲染以及窗口管理。
6)FSlotBase
FSlotBase用于为容器类添加和管理子控件的占位符基类,例如SHorizontalBox和SVerticalBox的插槽。
(图3 - Slate的控件结构图)
如图3中所示,在Slate中Widget是基础元素,它可以是容器也可以是组件还可以是窗体。
SWidget 为所有的 UI 元素(称为“控件”或“部件”)提供了基本功能与接口。在引擎中大量控件,例如 SButton、SImage、STextBlock 等都是直接或间接继承自 SWidget 的,SWidget 类可以被视为所有控件的共同祖先。
SWidget 类具有许多基本属性和方法,包括:
1)属性 Visibility:设置部件的可见性,例如:可见、隐藏或自适应。这使得开发者可以根据需要控制控件的显示和隐藏。
2)属性 RenderTransform 和 RenderTransformPivot:用于控制部件的2D变换,如:缩放、旋转和平移。这使得开发者可以轻松地对控件进行变换,以满足特定的设计需求。
3)同时 SWidget 包含许多虚函数,从此类派生后可覆盖虚函数以实现自定义功能和行为。例如,OnMouseButtonDown、OnMouseButtonUp、OnMouseMove 和 OnMouseWheel 等函数可用于处理鼠标事件,而 OnPaint 函数则可用于处理部件的绘制功能。
4)还有其他方法,例如 SetVisibility:用于动态更改部件的可见性状态。Construct:用于创建自定义的 SWidget 对象。AsShared 和 AsSharedRef:将 SWidget 转换为 TSharedRef 或 TSharedPtr 类型的智能指针。
开发者通常会使用现有的基于 SWidget 的控件(如 SButton、SImage 等)派生实现自定义控件,即从 SWidget 或已有子类派生新的自定义控件来实现所需的 UI 逻辑,这样实现的速度会更快。同时在创建 UI 时控件也可以组合在一起,例如将一个 STextBlock 放入 SButton 中以生成一个带有文字的按钮,这种组合方式可以实现复杂的 UI 布局和交互逻辑。
2.2 Slate控件和自定义
(自定义控件结构图)
SWidget 的 OnPaint() 函数负责处理部件的绘制过程,该函数在每帧渲染时都会被调用以确保 UI 元素保持实时更新。
当我们需要实现自定义绘制逻辑时可以重写 OnPaint() 函数,OnPaint() 函数的参数包括:FPaintArgs 绘制所需的信息,例如绘制中的局部几何变换和时间戳等;FGeometry 描述部件的布局信息,包括局部和绝对坐标系下部件的大小、位置及缩放等;FSlateWindowElementList 绘制元素列表,用于存储和绘制 Slate 元素。通过向这个列表添加自定义的渲染元素,实现绘制。此外,参数中还包含 LayerId,表示当前正在绘制的 UI 层级。要为自定义部件实现特定的绘制逻辑,需要重写 OnPaint() 函数并执行以下操作:使用 AllottedGeometry 计算部件的位置和大小。接着,根据需要创建 Slate 元素,如 FSlateBoxBrush 或 FSlateColorBrush。最后,使用 OutDrawElements.Add() 将创建的 Slate 元素添加到 OutDrawElements,指定 LayerId 控制绘制顺序。最后,调用超类的 OnPaint() 函数(例如 Super::OnPaint(Args, AllottedGeometry, OutDrawElements, LayerId, InOpacity, bParentEnabled)),确保基类和子部件的绘制被正确处理。需要注意的是,在实际开发中除非确实需要自定义绘制逻辑,否则通常不需要重写 OnPaint()。许多现有的 SWidget 子类(如 SButton、SImage、STextBlock 等)已实现了所需的绘制功能。此外,在处理与 UI 交互或状态变化相关的功能时,例如响应按键或悬停事件,需要考虑在其他虚函数(如 OnMouseButtonDown())中实现这些功能,而不是在 OnPaint() 函数中。最后,需要注意性能优化,由于 OnPaint() 函数每帧调用一次,需要确保执行的操作不会对性能产生过大的影响。
在Slate中我们可以自定义控件,SNew宏定义返回的是输入的Widget类型实例,我们可以通过Widget派生类的重载[]和+运算符来组合自定义的控件,如下代码。
return SNew(SVerticalBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromName(GetFName()))
]
(清单1 - SBox内含有Text文本控件)
return SNew(SVerticalBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
+ SVerticalBox::Slot()
[
SNew(STextBlock)
.Text(FText::FromName(GetFName()))
]
(清单2 - 用重载加号的方法将SVerticalBox中加入Text文本控件)
以上两段C++代码结果相同,都是在SVerticalBox容器中加入了Text文本控件,并对SVerticalBox和Text控件填充控件参数。不熟悉的人第一眼看到以上代码会认为是一个配置文件,看起来也确实很像,这是因为Slate组件对运算符做了重载。
// 重载‘+’运算符不带参
TArray< SlotType* > Slots; \
WidgetArgsType& operator + (SlotType& SlotToAdd) \
{ \
Slots.Add( &SlotToAdd ); \
return *this; \
}
// 重载'[]'
SLATE_NAMED_SLOT(DeclarationType, SlotName) ; \
DeclarationType & operator[]( const TSharedRef
InChild ) \ { \
_
return *this; \
}
// code in DeclarativeSyntaxSupport.h
(清单3 - Slate组件重载运算符)
清单3中,罗列了Slate对运算符做重载的代码,可以看到组件对‘[]’和‘+’做了重载,使得组件的组合实例化更加简单易用。有了Slate框架有了重载运算符,我们可以根据SlateCore来制作自定义组件,如下:
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
SNew(STextBlock)
.AutoWrapText("name")
]
+ SVerticalBox::Slot()
[
SNew(SButton)
[
SNew(STextBlock)
.AutoWrapText("start")
]
]
+ SVerticalBox::Slot()
[
SNew(SButton)
[
SNew(STextBlock)
.AutoWrapText("end")
]
];
(清单4 - 自定组件代码)
如上所示,从自定义组件到控件组合到控件层级到控件表现行云流水一气呵成,先定义容器再通过+重载增加两个Slot槽,在槽中分别加入SButton按钮接着再在按钮中加入Text文本控件,整个控件的组合就像配置文件。
(组件图与渲染图对应关系)
同时在UE中可以通过调试工具查看当前UI渲染的层级和控件的状态。调试工具在UE编辑器的Developer Tools中的Widget Reflector上,可以通过Pick Hit-Testable Widgets的方式获得选中的UI控件信息。