BPVM 小食包 #10 - 函数工厂:图表变成函数
你的事件图在运行时实际上不是图表。它被转换成一个叫做 Ubergraph 的巨型函数。这就是函数工厂的魔法工作原理。
BPVM 小食包 #10 - 函数工厂:图表变成函数
本文内容基于Unreal Engine 5.6.0
BPVM 小食包 - 蓝图知识快速投喂!是蓝图到字节码系列的一部分。
多图表问题
你为了组织创建了多个事件图页面:
- “玩家输入”页面
- “战斗逻辑”页面
- “UI 更新”页面
干净整洁,对吧?但这里有个秘密:它们都变成一个函数。
认识 Ubergraph
编译器将你所有的事件图合并:
1
2
3
4
5
6
7
8
9
10
11
12
void CreateAndProcessUbergraph()
{
// 创建一个大图表
ConsolidatedEventGraph = NewObject<UEdGraph>("Ubergraph");
// 将所有事件图页面复制进去
for (UEdGraph* EventGraph : Blueprint->EventGraphs) {
MergeIntoUbergraph(EventGraph, ConsolidatedEventGraph);
}
// 这现在是一个巨型函数!
}
想象一下,把多张食谱卡组合成一本烹饪书!
为什么要合并所有内容?
虚拟机不理解”页面” - 它只执行函数:
1
2
3
4
5
6
7
8
9
10
11
// 你在编辑器中看到的:
EventGraph_Page1 → BeginPlay 节点
EventGraph_Page2 → Tick 节点
EventGraph_Page3 → OnDamaged 节点
// 虚拟机看到的:
Ubergraph() {
BeginPlay_Implementation();
Tick_Implementation();
OnDamaged_Implementation();
}
页面是为了人类。机器想要一个函数。
函数创建流水线
工厂处理四种类型的图表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CreateFunctionList()
{
// 1. Ubergraph(所有事件图合并)
if (DoesSupportEventGraphs(Blueprint)) {
CreateAndProcessUbergraph();
}
// 2. 常规函数图
for (UEdGraph* Graph : Blueprint->FunctionGraphs) {
ProcessOneFunctionGraph(Graph);
}
// 3. 生成的函数图(来自宏等)
for (UEdGraph* Graph : GeneratedFunctionGraphs) {
ProcessOneFunctionGraph(Graph);
}
// 4. 接口函数
for (auto& Interface : Blueprint->ImplementedInterfaces) {
for (UEdGraph* Graph : Interface.Graphs) {
ProcessOneFunctionGraph(Graph);
}
}
}
处理每个函数
每个图表都经过相同的工厂流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ProcessOneFunctionGraph(UEdGraph* SourceGraph)
{
// 步骤 1: 克隆到临时图表
UEdGraph* TempGraph = DuplicateGraph(SourceGraph);
// 步骤 2: 展开节点(宏变成真实节点)
ExpandAllMacroNodes(TempGraph);
// 步骤 3: 创建函数上下文
FKismetFunctionContext* Context = CreateFunctionContext();
Context->SourceGraph = TempGraph;
// 步骤 4: 添加到函数列表
FunctionList.Add(Context);
}
事件节点魔法
图表中的每个事件都变成一个函数桩:
1
2
3
4
5
6
7
8
// 你有一个 BeginPlay 事件节点
UK2Node_Event* BeginPlayNode;
// 编译器创建一个函数桩
void ReceiveBeginPlay() {
// 跳转到 Ubergraph 中的正确位置
Ubergraph(ENTRY_BeginPlay);
}
事件只是进入大函数的入口点!
函数上下文:蓝图
每个函数都获得一个 FKismetFunctionContext:
1
2
3
4
5
6
7
8
struct FKismetFunctionContext
{
UEdGraph* SourceGraph; // 可视化图表
TArray<FBPTerminal*> Parameters; // 输入引脚
TArray<FBPTerminal*> Locals; // 局部变量
TArray<UEdGraphNode*> LinearExecutionList; // 节点顺序
TArray<FBlueprintCompiledStatement*> AllGeneratedStatements; // 代码!
};
这个上下文是构建实际函数的蓝图(双关语有意为之)!
宏展开
宏在处理过程中被内联:
1
2
3
4
5
// 展开前
CallMacro("MyUtilityMacro")
// 展开后(节点直接复制)
Node1 → Node2 → Node3 → Node4 // 宏的实际节点
宏消失了 - 它们的节点被直接复制到你的函数中!
Ubergraph 名称
在崩溃日志中见过这个吗?
1
ExecuteUbergraph_BP_MyActor
现在你知道它的含义了 - 这是包含所有事件的大函数!
函数类型解释
常规函数:
1
2
ProcessOneFunctionGraph(MyFunction)
→ 创建: MyFunction()
事件图事件:
1
2
3
CreateAndProcessUbergraph()
→ 创建: ExecuteUbergraph_BP_MyActor()
→ 带有桩: ReceiveBeginPlay()、ReceiveTick() 等
接口函数:
1
2
ProcessOneFunctionGraph(InterfaceFunc)
→ 创建: InterfaceFunc_Implementation()
隐藏的优化
为什么要将所有内容合并到 Ubergraph?
没有 Ubergraph(低效):
1
2
3
4
void BeginPlay() { /* 字节码 */ }
void Tick() { /* 字节码 */ }
void OnDamaged() { /* 字节码 */ }
// 三个独立的函数调用,三个上下文
有 Ubergraph(优化):
1
2
3
4
5
6
7
8
void ExecuteUbergraph(int EntryPoint) {
switch(EntryPoint) {
case 0: /* BeginPlay 字节码 */
case 1: /* Tick 字节码 */
case 2: /* OnDamaged 字节码 */
}
// 一个函数,共享上下文!
}
快速要点
- 所有事件图页面都变成一个函数(Ubergraph)
- 常规函数各自获得自己的函数
- 宏被内联展开(它们消失了)
- 每个函数都获得一个 FKismetFunctionContext(它的蓝图)
- 事件只是进入 Ubergraph 的入口点
- 接口函数获得 _Implementation 后缀
工厂永不停歇
每次你编译时:
- 事件图合并到 Ubergraph
- 函数单独处理
- 宏展开并消失
- 为每个函数创建上下文
- 工厂生产可执行的函数!
想要更多细节?
完整的函数创建过程:
下一篇:所有内容如何链接在一起!
🍿 BPVM 小食包系列
- ← #9: 变量变成属性
- #10: 函数工厂 ← 你在这里
- #11: 链接和绑定 →
本文由作者按照
CC BY 4.0
进行授权