BPVM 小食包 #7 - 节点处理器:翻译小队
你的蓝图中的每个节点都需要一个翻译器。认识节点处理器——将你的可视节点转换为可执行代码的无名英雄。
BPVM 小食包 #7 - 节点处理器:翻译小队
本文内容基于Unreal Engine 5.6.0
翻译问题
你拖动一个”Select”节点到你的蓝图中。你连接一些引脚。你点击编译。
但等等——那个可视节点如何成为实际的可执行代码?
进入节点处理器
每个节点类型都有一个专用翻译器叫做节点处理器 (Node Handler):
1
2
3
4
5
// 对于每个 UK2Node 类型...
UK2Node_Select → FKCHandler_Select
UK2Node_CallFunction → FKCHandler_CallFunction
UK2Node_VariableGet → FKCHandler_VariableGet
// ... 还有数百个!
把它们想象成联合国的专业翻译:
- 每个处理器讲一种”节点语言”
- 它们都翻译成相同的”字节码语言”
- 没有它们,你的节点只是漂亮的图片!
处理器模式
每个处理器遵循相同的模式:
1
2
3
4
5
6
7
8
9
class FKCHandler_Select : public FNodeHandlingFunctor
{
public:
// 步骤 1: "我需要什么数据?"
virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node);
// 步骤 2: "我如何翻译这个?"
virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node);
};
两个工作,清晰的关注点分离!
RegisterNets: 设置阶段
在编译之前,处理器需要注册它们的数据需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FKCHandler_Select::RegisterNets(Context, Node)
{
// "我需要这些引脚的存储!"
// 注册索引引脚
FBPTerminal* IndexTerm = Context.CreateLocalTerminal();
Context.NetMap.Add(IndexPin, IndexTerm);
// 注册每个选项引脚
for (UEdGraphPin* Pin : OptionPins) {
FBPTerminal* Term = Context.CreateLocalTerminal();
Context.NetMap.Add(Pin, Term);
}
// 注册输出
FBPTerminal* OutputTerm = Context.CreateLocalTerminal();
Context.NetMap.Add(OutputPin, OutputTerm);
}
这就像在使用之前声明变量——首先预留内存!
Compile: 翻译阶段
现在实际的翻译发生了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FKCHandler_Select::Compile(Context, Node)
{
// 创建字节码语句
FBlueprintCompiledStatement* Statement = new FBlueprintCompiledStatement();
Statement->Type = KCST_SwitchValue; // "这是一个开关操作"
// 获取我们注册的终端
FBPTerminal* IndexTerm = Context.NetMap.FindRef(IndexPin);
FBPTerminal* OutputTerm = Context.NetMap.FindRef(OutputPin);
// 构建开关逻辑
Statement->LHS = OutputTerm; // 存储结果的地方
Statement->RHS.Add(IndexTerm); // 要开关的内容
// 添加每个 case
for (int32 i = 0; i < Options.Num(); i++) {
Statement->RHS.Add(OptionTerms[i]);
}
}
真实例子: Select 节点
让我们看看 Select 节点如何被翻译:
你看到的:
1
2
3
4
5
Index: 2
Option 0: "Hello"
Option 1: "World"
Option 2: "!" <-- 被选中!
Output: "!"
RegisterNets 做的:
1
2
3
4
5
6
// 预留内存槽
Terminal_0 = Index (integer)
Terminal_1 = Option0 (string)
Terminal_2 = Option1 (string)
Terminal_3 = Option2 (string)
Terminal_4 = Output (string)
Compile 创建的:
1
2
3
4
5
6
7
8
Statement: KCST_SwitchValue
LHS: Terminal_4 (输出)
RHS: [
Terminal_0, // 索引
Terminal_1, // Case 0
Terminal_2, // Case 1
Terminal_3 // Case 2
]
处理器注册表
编译器维护一个处理器的巨大映射:
1
2
3
4
5
// 在编译器初始化期间
NodeHandlers.Add(UK2Node_Select::StaticClass(), new FKCHandler_Select());
NodeHandlers.Add(UK2Node_CallFunction::StaticClass(), new FKCHandler_CallFunction());
NodeHandlers.Add(UK2Node_VariableGet::StaticClass(), new FKCHandler_VariableGet());
// ... 还有数百个
编译你的图表时:
1
2
3
4
5
6
7
8
9
for (UEdGraphNode* Node : Graph->Nodes) {
// 找到正确的翻译器
FNodeHandlingFunctor* Handler = NodeHandlers.FindRef(Node->GetClass());
if (Handler) {
Handler->RegisterNets(Context, Node); // 设置
Handler->Compile(Context, Node); // 翻译
}
}
为什么两个阶段?
为什么不直接编译?
编译器需要在生成代码之前了解所有变量:
1
2
3
4
5
6
7
8
9
// 坏的:边走边编译
CompileNode(A); // 创建变量 X
CompileNode(B); // 需要变量 X... 它存在吗?
// 好的:两个阶段
RegisterNets(A); // 声明变量 X
RegisterNets(B); // 声明变量 Y
Compile(A); // 使用变量 X(保证存在)
Compile(B); // 使用变量 X 和 Y(都存在!)
特殊处理器能力
一些处理器有特殊能力:
1
2
3
4
5
6
7
8
9
10
11
12
class FKCHandler_CallFunction : public FNodeHandlingFunctor
{
// 特殊能力:可以优化某些调用!
virtual void Transform(FKismetFunctionContext& Context, UEdGraphNode* Node) {
// 如果可能,将 Print(String) 转换为快速路径
}
// 特殊能力:为签名提前运行!
virtual bool RequiresRegisterNetsBeforeScheduling() {
return true; // 函数入口/出口节点需要这个
}
};
语句输出
处理器产生中间语句(还不是字节码!):
1
2
3
4
5
6
7
8
9
10
11
12
// 处理器产生这个:
Statement {
Type: KCST_CallFunction
Function: "PrintString"
Parameters: ["Hello World"]
}
// 后端稍后转换为字节码:
0x44 (EX_CallFunc)
0x08 (Function ID)
"Hello World"
0x53 (EX_Return)
这是一个两级火箭 - 处理器让你进入轨道,后端让你到达月球!
快速要点
- 每个节点类型都有一个节点处理器(它的个人翻译器)
- RegisterNets: “我需要这些变量”(设置阶段)
- Compile: “这是如何执行我”(翻译阶段)
- 处理器产生语句,不是字节码(那个稍后来)
- 两个阶段确保所有变量在使用前存在
- 这是策略模式在行动——每个节点类型一个处理器!
你的节点活起来了
下次你拖动一个节点到你的蓝图时,记住:
- 那个节点有一个专用的处理器等待翻译它
- RegisterNets 首先运行以设置工作空间
- Compile 其次运行以生成逻辑
- 没有处理器,你的节点只会是漂亮的图片!
想要更多细节?
有关完整的处理器深入:
下一份小食:清理和净化的魔法!
🍿 BPVM 小食包系列
- ← #6: CDO 之谜
- #7: 节点处理器解释 ← 你在这里
- #8: 清理和净化魔法 →
本文由作者按照
CC BY 4.0
进行授权
