BPVM Snack Pack #7 - Node Handlers: The Translation Squad | ebp BPVM Snack Pack #7 - Node Handlers: The Translation Squad | ebp BPVM Snack Pack #7 - Node Handlers: The Translation Squad | ebp
Post

BPVM Snack Pack #7 - Node Handlers: The Translation Squad

Every node in your Blueprint needs a translator. Meet the Node Handlers - the unsung heroes that turn your visual nodes into executable code.

BPVM Snack Pack #7 - Node Handlers: The Translation Squad

The content in this post is based on Unreal Engine 5.6.0

BPVM Snack Pack - Quick Blueprint knowledge drops! Part of the Blueprint to Bytecode series.

The Translation Problem

You drag a “Select” node into your Blueprint. You connect some pins. You hit compile.

But wait - how does that visual node become actual executable code?

Select node in Blueprint editor

Enter the Node Handlers

Every node type has a dedicated translator called a Node Handler:

1
2
3
4
5
// For every UK2Node type...
UK2Node_Select    FKCHandler_Select
UK2Node_CallFunction    FKCHandler_CallFunction
UK2Node_VariableGet    FKCHandler_VariableGet
// ... hundreds more!

Think of them as specialized translators at the UN:

  • Each handler speaks one “node language”
  • They all translate to the same “bytecode language”
  • Without them, your nodes are just pretty pictures!

The Handler Pattern

Every handler follows the same pattern:

1
2
3
4
5
6
7
8
9
class FKCHandler_Select : public FNodeHandlingFunctor
{
public:
    // Step 1: "What data do I need?"
    virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node);

    // Step 2: "How do I translate this?"
    virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node);
};

Two jobs, crystal clear separation of concerns!

RegisterNets: The Setup Phase

Before compiling, handlers need to register their data needs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FKCHandler_Select::RegisterNets(Context, Node)
{
    // "I need storage for these pins!"

    // Register the index pin
    FBPTerminal* IndexTerm = Context.CreateLocalTerminal();
    Context.NetMap.Add(IndexPin, IndexTerm);

    // Register each option pin
    for (UEdGraphPin* Pin : OptionPins) {
        FBPTerminal* Term = Context.CreateLocalTerminal();
        Context.NetMap.Add(Pin, Term);
    }

    // Register output
    FBPTerminal* OutputTerm = Context.CreateLocalTerminal();
    Context.NetMap.Add(OutputPin, OutputTerm);
}

It’s like declaring variables before using them - reserve the memory first!

Compile: The Translation Phase

Now the actual translation happens:

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)
{
    // Create the bytecode statement
    FBlueprintCompiledStatement* Statement = new FBlueprintCompiledStatement();
    Statement->Type = KCST_SwitchValue;  // "This is a switch operation"

    // Get our registered terminals
    FBPTerminal* IndexTerm = Context.NetMap.FindRef(IndexPin);
    FBPTerminal* OutputTerm = Context.NetMap.FindRef(OutputPin);

    // Build the switch logic
    Statement->LHS = OutputTerm;  // Where to store result
    Statement->RHS.Add(IndexTerm);  // What to switch on

    // Add each case
    for (int32 i = 0; i < Options.Num(); i++) {
        Statement->RHS.Add(OptionTerms[i]);
    }
}

Real Example: The Select Node

Let’s see how a Select node gets translated:

What You See:

1
2
3
4
5
Index: 2
Option 0: "Hello"
Option 1: "World"
Option 2: "!"      <-- Selected!
Output: "!"

What RegisterNets Does:

1
2
3
4
5
6
// Reserve memory slots
Terminal_0 = Index (integer)
Terminal_1 = Option0 (string)
Terminal_2 = Option1 (string)
Terminal_3 = Option2 (string)
Terminal_4 = Output (string)

What Compile Creates:

1
2
3
4
5
6
7
8
Statement: KCST_SwitchValue
LHS: Terminal_4 (output)
RHS: [
    Terminal_0,  // Index
    Terminal_1,  // Case 0
    Terminal_2,  // Case 1
    Terminal_3   // Case 2
]

The Handler Registry

The compiler maintains a giant map of handlers:

1
2
3
4
5
// During compiler initialization
NodeHandlers.Add(UK2Node_Select::StaticClass(), new FKCHandler_Select());
NodeHandlers.Add(UK2Node_CallFunction::StaticClass(), new FKCHandler_CallFunction());
NodeHandlers.Add(UK2Node_VariableGet::StaticClass(), new FKCHandler_VariableGet());
// ... hundreds more

When compiling your graph:

1
2
3
4
5
6
7
8
9
for (UEdGraphNode* Node : Graph->Nodes) {
    // Find the right translator
    FNodeHandlingFunctor* Handler = NodeHandlers.FindRef(Node->GetClass());

    if (Handler) {
        Handler->RegisterNets(Context, Node);  // Setup
        Handler->Compile(Context, Node);        // Translate
    }
}

Why Two Phases?

Why not just compile directly?

The compiler needs to know about ALL variables before generating code:

1
2
3
4
5
6
7
8
9
// Bad: Compile as we go
CompileNode(A);  // Creates var X
CompileNode(B);  // Needs var X... does it exist?

// Good: Two phases
RegisterNets(A);  // Declare var X
RegisterNets(B);  // Declare var Y
Compile(A);       // Use var X (guaranteed to exist)
Compile(B);       // Use var X and Y (both exist!)

Special Handler Powers

Some handlers have special abilities:

1
2
3
4
5
6
7
8
9
10
11
12
class FKCHandler_CallFunction : public FNodeHandlingFunctor
{
    // Special power: Can optimize certain calls!
    virtual void Transform(FKismetFunctionContext& Context, UEdGraphNode* Node) {
        // Convert Print(String) to fastpath if possible
    }

    // Special power: Runs early for signatures!
    virtual bool RequiresRegisterNetsBeforeScheduling() {
        return true;  // Function entry/exit nodes need this
    }
};

The Statement Output

Handlers produce intermediate statements (not bytecode yet!):

1
2
3
4
5
6
7
8
9
10
11
12
// Handler produces this:
Statement {
    Type: KCST_CallFunction
    Function: "PrintString"
    Parameters: ["Hello World"]
}

// Backend converts to bytecode later:
0x44 (EX_CallFunc)
0x08 (Function ID)
"Hello World"
0x53 (EX_Return)

It’s a two-stage rocket - handlers get you to orbit, backend gets you to the moon!

Quick Takeaway

  • Every node type has a Node Handler (its personal translator)
  • RegisterNets: “I need these variables” (setup phase)
  • Compile: “Here’s how to execute me” (translation phase)
  • Handlers produce statements, not bytecode (that comes later)
  • Two phases ensure all variables exist before use
  • It’s the Strategy Pattern in action - one handler per node type!

Your Nodes Come Alive

Next time you drag a node into your Blueprint, remember:

  • That node has a dedicated handler waiting to translate it
  • RegisterNets runs first to set up the workspace
  • Compile runs second to generate the logic
  • Without handlers, your nodes would just be pretty pictures!

Want More Details?

For the complete handler deep-dive:

Next snack: The magic of Clean and Sanitize!


🍿 BPVM Snack Pack Series

This post is licensed under CC BY 4.0 by the author.