前言

探索从UMG到Slate后加载的全过程

简述

UMG流程:

CreateWidget

生成 UWidget / WidgetTree

AddToViewport / AddChild

TakeWidget

RebuildWidget

生成真实 SWidget

触发首次创建后的逻辑,也就是 NewlyCreated 相关流程

GPT回答

从源码角度看,UE 里 UMG 从“蓝图/类”生成到屏幕上的过程,大致是这条链路:

UWidgetBlueprint → 编译生成 UWidgetBlueprintGeneratedClass → 运行时 CreateWidget 创建 UUserWidgetInitialize / RebuildWidget 构建 Slate 树 → AddToViewport 加入 viewport → Slate 每帧布局、绘制、输入分发。

1. 编辑器里创建 UMG 蓝图
UMG 蓝图资源类型通常是 UWidgetBlueprint

源码里相关模块大概在:

1
2
Engine/Source/Editor/UMGEditor
Engine/Source/Runtime/UMG

一个 Widget Blueprint 里主要包含:

1
UWidgetTree* WidgetTree;

WidgetTree 保存你在设计器里拖出来的控件层级,比如:

1
2
3
4
CanvasPanel
├─ Button
│ └─ TextBlock
└─ Image

这些不是 Slate 控件,而是 UObject 体系里的 UMG 控件对象,例如:

1
2
3
4
UCanvasPanel
UButton
UTextBlock
UImage

它们都继承自 UWidget

2. 编译 Widget Blueprint
UMG 蓝图编译后会生成:

1
UWidgetBlueprintGeneratedClass

它继承自:

1
UBlueprintGeneratedClass

比较关键的是它会保存编译后的 WidgetTree 模板、绑定信息、动画信息等。

运行时创建 Widget 时,不是直接使用编辑器里的 UWidgetBlueprint,而是使用它编译出来的 generated class。

概念上类似:

1
2
3
4
5
6
7
8
9
UWidgetBlueprint
|
| Compile
v
UWidgetBlueprintGeneratedClass
|
| Runtime instance
v
UUserWidget

3. 运行时 CreateWidget
常见代码是:

1
UUserWidget* Widget = CreateWidget<UUserWidget>(PlayerController, WidgetClass);

源码入口通常会走到 UMG 的创建逻辑,核心目标是创建一个 UUserWidget 实例。

流程可以理解为:

1
2
3
4
CreateWidget
-> CreateWidgetInstance
-> NewObject<UUserWidget>
-> UUserWidget::Initialize()

Initialize() 是很关键的一步。

它会根据 UWidgetBlueprintGeneratedClass 中保存的模板数据,初始化当前实例的 WidgetTree,并处理:

1
2
3
4
5
控件实例化
变量绑定
NamedSlot
动画
Blueprint 初始化事件

如果蓝图里某个控件勾选了 Is Variable,生成类中会让对应的 C++/蓝图成员变量指向运行时创建出来的那个控件实例。

例如蓝图里有一个按钮 StartButton,运行时就会绑定到:

1
UButton* StartButton;

4. UWidgetTree 生成 UObject 控件树
此时得到的是 UMG 层的控件树,也就是 UObject 控件树。

例如:

1
2
3
4
5
6
UUserWidget
└─ WidgetTree
└─ UCanvasPanel
├─ UButton
│ └─ UTextBlock
└─ UImage

这些对象本身还不是最终渲染用的 Slate widget。

UMG 的核心思想是:

1
2
UMG = UObject 封装层
Slate = 真正的底层 UI 系统

也就是说,UButtonUTextBlock 等 UMG 控件最终会构建出对应的 Slate 控件:

1
2
3
UButton    -> SButton
UTextBlock -> STextBlock
UImage -> SImage

5. RebuildWidget 构建 Slate 树
当 Widget 需要真正显示时,会调用:

1
UUserWidget::RebuildWidget()

以及各个子控件自己的:

1
UWidget::RebuildWidget()

比如:

1
2
3
4
5
UButton::RebuildWidget()
{
MyButton = SNew(SButton);
return MyButton.ToSharedRef();
}

UTextBlock 会创建 STextBlockUImage 会创建 SImage

所以 UMG 树会被转换成 Slate 树:

1
2
3
4
UCanvasPanel       -> SConstraintCanvas
UButton -> SButton
UTextBlock -> STextBlock
UImage -> SImage

最终类似:

1
2
3
4
5
SObjectWidget
└─ SConstraintCanvas
├─ SButton
│ └─ STextBlock
└─ SImage

SObjectWidget 是 UMG 和 Slate 之间的重要桥接层,它把 UUserWidget 包成 Slate 可以管理的 SWidget

6. AddToViewport
当你调用:

1
Widget->AddToViewport();

通常会进入:

1
2
UUserWidget::AddToViewport()
UUserWidget::AddToScreen()

最后通过 UGameViewportClient 把它加到 viewport 的 widget 层中。

概念上是:

1
2
3
4
UUserWidget::AddToViewport()
-> AddToScreen()
-> ULocalPlayer / UGameViewportClient
-> AddViewportWidgetContent()

Slate 层最终会把这个 Widget 加到游戏 viewport 对应的 Slate 容器中。

7. Slate 负责布局、绘制和输入
一旦进入 Slate 树,后续每帧主要由 Slate 系统处理:

1
2
3
4
5
Prepass
Arrange
Paint
Hit Test
Input Routing

也就是:

1
2
3
4
计算 DesiredSize
布局子控件位置
绘制元素
处理鼠标/键盘/手柄输入

UMG 的属性变更,例如修改 TextBlock 的文字:

1
TextBlock->SetText(FText::FromString("Hello"));

最终会同步到对应的 Slate 控件:

1
STextBlock::SetText(...)

整体流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
编辑器设计 UMG
|
v
UWidgetBlueprint
|
| 编译
v
UWidgetBlueprintGeneratedClass
|
| CreateWidget
v
UUserWidget 实例
|
| Initialize
v
运行时 UWidgetTree
|
| RebuildWidget
v
Slate 控件树 SWidget
|
| AddToViewport
v
加入 GameViewport
|
v
Slate Tick / Layout / Paint / Input

一句话总结:

UMG 蓝图本质上先被编译成 UWidgetBlueprintGeneratedClass,运行时 CreateWidget 创建 UUserWidget 和它的 UWidgetTree,然后每个 UWidget 通过 RebuildWidget() 生成对应的 Slate SWidget,最后 AddToViewport() 把 Slate 树挂到游戏 viewport 上,由 Slate 系统完成布局、绘制和输入。

分析图

img

参考

(99+ 封私信 / 12 条消息) 【UE】通过源码了解Slate Widget和UMG Widget - 知乎