C++ K2Node to return a constant

I have issues wrapping my head around the runtime implementation of a K2Node via the ExpandNode function. All examples out there go about spawning intermediate nodes under the hood, but intuitively, I expected a node to have some C++ Execute function that can be overriden to directly implement the runtime behaviour.

Nonetheless, I wrote an example node that does one very simple thing - provides a custom widget that is ultimately responsible for selecting an index. I then want this index to be the output of my node.

UCLASS(MinimalAPI)
class UK2Node_GetIndex
	: public UK2Node
{
public:
	GENERATED_BODY()

	/** This will be set by the node widget, and hopefully saved/serialized with the graph */
	UPROPERTY()
	int32 Idx;

    // Boilerplate...
}
void UK2Node_GetIndex::AllocateDefaultPins()
{
    Super::AllocateDefaultPins();

    const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

    CreatePin(EEdGraphPinDirection::EGPD_Output, UEdGraphSchema_K2::PC_Int, Priv::locIdxPinName);
}

Now, since all I want is to return the value of Idx, what I really wish to express in the ExpandNode function is something like this.

void UK2Node_GetIndex::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
    Super::ExpandNode(CompilerContext, SourceGraph);

    // Validate idx is within range ...

    // Wish to just write a value to a pin, but this DefaultValue is allegedly used if the pin is not connected. Not useful.
    GetIdxPin()->DefaultValue = FString::FromInt(Idx);
}

What kind of intermediate nodes can I spawn to simply RETURN a value?
Consider spawning an intermediate temp variable node for instance. I think this might work if I knew how to “set” the value, which is a reoccurring problem I am hitting when dealing with custom nodes and their pins.

UK2Node_TemporaryVariable* variableNode = CompilerContext.SpawnInternalVariable(this, UEdGraphSchema_K2::PC_Int);

// How do I "set" its value?

CompilerContext.MovePinLinksToIntermediate(*GetIdxPin(), *variableNode->GetVariablePin());

BreakAllNodeLinks();

It should be achievable by spawning an intermediate node equivalent to “make literal integer”.

Something like this

auto CallLiteralNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
CallLiteralNode->FunctionReference.SetExternalMember("MakeLiteralInt", UKismetSystemLibrary::StaticClass());
CallLiteralNode->AllocateDefaultPins();

UEdGraphPin* LiteralValuePin = CallLiteralNode->FindPinChecked("Value");
UEdGraphPin* LiteralReturnPin = CallLiteralNode->FindPinChecked("ReturnValue");

LiteralValuePin->DefaultValue = FString::FromInt(Idx);

CompilerContext.MovePinLinksToIntermediate(*GetIdxPin(), *LiteralReturnPin);

Hey Thanks,

That was exactly what I ended up doing in the meantime and it works. But is there an understanding of why the CallFunction node is special? Surely there must be a node down the line that no longer needs to spawn intermediates? Is that difficult to achieve?

As a follow up example, I am creating a new node that accepts multiple input Exec pins. The idea being, every exec pin should increment a counter for the node. Doesn’t sound obvious how you would implement that in a UFUNCTION – something needs to track a counter variable, presumably the K2Node itself.

It’s not particularly special, it’s just a wrapper for calling an UFunction. The reason it works in this case is because function MakeLiteralInt has an int32 input which is directly transmitted to its int32 output, so you can feed a DefaultValue to the input and retrieve it in the output.

If you had to track a counter variable you wouldn’t do it in the K2Node itself. K2Nodes are not executed at runtime. They are compiled into bytecode, which looks nothing like high level blueprint graphs. The purpose of the CallFunction node is precisely to execute a designated C++ function in place of the node.

I don’t think you’d have to generate bytecode though. Even if you wanted to have multiple input pins and have a counter that tracks the amount of entries, you should be able to get away with ExpandNode, inject a temporary variable for the counter, increment it, generate sub-graphs, connect each input pin to each sub-graph…

I get what you are saying though.
If you want to optimize this at the lowest level, without using intermediate nodes, you’d have to write a FNodeHandlingFunctor to compile your node into bytecode, and return it in this override

FNodeHandlingFunctor* UK2Node_CallFunction::CreateNodeHandler(FKismetCompilerContext& CompilerContext) const
{
	return new FKCHandler_CallFunction(CompilerContext);
}

In your simple case, you can probably get away with just creating a BPTerminal (in function RegisterNet) for your output pin and setting its default value. That’s how the K2Node_Literal works, but that node is designed to only work with objects - it’s the node used when referencing actors in the Level Blueprint, all they do is output the referenced object compiled in the node.

At an even lower level, the BPTerminal will generate an FIntProperty with storage on the persistent frame (graph), and the generated bytecode will probably result in one EX_Let or EX_LetValueOnPersistentFrame instruction (which purpose is to fill supplied FProperty), followed by an EX_IntConst instruction to supply the value. For reference, all bytecode instructions are implemented in ScriptCore.cpp.


So, to come back to your original question, no there is not a simple place where you can just override the execution of a node. The best tool for that is CallFunction. In your original case for example, you could also have done it this way :

  • create an UFUNCTION for your node, with Idx as input parameter (it can be a static function in any UClass)
  • use ExpandNode to generate a K2Node_CallFunction to your function
  • Set your node’s Idx value as the default value of the Idx input pin of the generated CallFunction node

This way, the UFUNCTION you created is essentially what you are looking for, ie. the place where you can override the execution of the node. And you have access to Idx even though it’s a custom widget on the node.

I think I get it now, the NodeHandlingFunctor was the final piece to the puzzle, but it’s not documented and will take some time to figure out. Having to create intermediate nodes that just circle around and return the same input made me wonder if I was thinking about it the wrong way.

As for the counter… so basically I need to imagine it like attempting to implement it inside the Blueprint Editor. In this case, I need a TempVariable node. I need a series of decrements or assignment intermediate nodes for each input exec pins (connecting the TempVariable as their input). And finally a branch node to check if the TempVariable reached a desired value.

Ok, so I think I am almost there, just missing one key step.

Let us first agree on what my end result should be:

It is basically an AND gate for execution pins – execution is gated until all incoming exec pins fire.

My idea is to track the state of each pin with some boolean array, where arr[i] == true when the i-th pin executes. I figured a simple way to achieve this is via a SetAndTest UFUNCTION, which set the flag to true, and check whether the condition is met.

// UPARAM(ref) is required otherwise creating the connection will fail -- wrong direction
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly))
static bool SetAndTestBoolArray_AND(int Idx, UPARAM(ref) TArray<bool>& InOutArray);

bool UFunctionLibrary::SetAndTestBoolArray_AND(int Idx, TArray<bool>& InOutArray)
{
    if(!InOutArray.IsValidIndex(Idx))
    {
        ensure(false); // Check your K2Node implementation
        return false;
    }

    InOutArray[Idx] = true;

    for(bool flag : InOutArray)
    {
        if(!flag)
            return false;
    }

    return true;
}

For this to work, this means each input exec pin in my node should:

  • Connect to a CallFunction node that invokes SetAndTestBoolArray_AND

  • Check the output of the function using a Branch node

  • Connect to the output exec pin if true

I need an array of bools as well, so I use SpawnIntermediateVariable, in combination with K2Node_AssignmentStatement and K2Node_MakeArray to set its value. Note, this might be redundant – I should be able to pass the output of MakeArray to all my CallFunction nodes, but let’s stick with this for now.

Resulting ExpandNode function:

void UK2Node_MultiAND::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
	Super::ExpandNode(CompilerContext, SourceGraph);

	UEdGraphSchema const* schema = SourceGraph->GetSchema();

	// Create a boolean array, where arr[i] = true if the i-th exec pin was fired.
	// Assign its value from the MakeArray node
	UK2Node_TemporaryVariable* execFlagArr = CompilerContext.SpawnInternalVariable(this, UEdGraphSchema_K2::PC_Boolean, NAME_None, nullptr, EPinContainerType::Array);
	UK2Node_AssignmentStatement* assignArrNode = CompilerContext.SpawnIntermediateNode<UK2Node_AssignmentStatement>(this);
	UK2Node_MakeArray* makeArrNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this);

	assignArrNode->AllocateDefaultPins();
	makeArrNode->NumInputs = 0;
	makeArrNode->AllocateDefaultPins();
	
    // Boolean flags matching the number of exec inputs
	for(int i = 0; i < NumInputs; ++i)
	{
		makeArrNode->AddInputPin();
		makeArrNode->GetPinWithDirectionAt(i, EEdGraphPinDirection::EGPD_Input)->DefaultValue = "false";
	}
	
	bool success = true;

	// Perform the "assignment" of the variable.
	success &= schema->TryCreateConnection(makeArrNode->GetOutputPin(), assignArrNode->GetValuePin());
	success &= schema->TryCreateConnection(execFlagArr->GetVariablePin(), assignArrNode->GetVariablePin());

	// Now create a node for our function that sets an idx into the bool arr, and return true if all flags are set
	// We need a separate function per index!
	for(int i = 0; i < NumInputs; ++i)
	{
		// Each exec pin will set its respective index in the boolean array, then check if the condition is met to proceed output execution
		UK2Node_CallFunction* setterFunctionNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this);
		setterFunctionNode->SetFromFunction(UXStudioStatics::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UXStudioStatics, SetAndTestBoolArray_AND)));
		setterFunctionNode->AllocateDefaultPins();
		setterFunctionNode->FindPinChecked(Priv::locSetterFuncIndexPinName, EEdGraphPinDirection::EGPD_Input)->DefaultValue = FString::FromInt(i);
		
		// Pass the array by ref to the function arg
		success &= schema->TryCreateConnection(execFlagArr->GetVariablePin(), setterFunctionNode->FindPinChecked(Priv::locSetterFuncArrayPinName));

		// Check the return of the function - if true, then all exec pins have fired.
		UK2Node_IfThenElse* branchNode = CompilerContext.SpawnIntermediateNode<UK2Node_IfThenElse>(this);
		branchNode->AllocateDefaultPins();
		success &= schema->TryCreateConnection(setterFunctionNode->GetReturnValuePin(), branchNode->GetConditionPin());


		// Connect the i-th Exec pin to the setter function. The resulting Then pin goes to the branch logic.
		success &= CompilerContext.MovePinLinksToIntermediate(*GetExecPin(i), *setterFunctionNode->GetExecPin()).CanSafeConnect();
		success &= CompilerContext.MovePinLinksToIntermediate(*branchNode->GetThenPin(), *GetThenPin()).CanSafeConnect();
	}

	if(!success)
	{
		CompilerContext.MessageLog.Error(TEXT("Node @@ compilation failed"), this);
	}

    // Is this correct?
	BreakAllNodeLinks();
}

Everything compiles, and the function gets called as expected. In the first invocation, one flag is set to true. Then when the second pin fires, and the function is invoked a second time, the boolean array is reset, with all values being false. If I can understand why the array does not persist, this problem would be solved. Maybe it’s the difference between using the Assignment node vs th Set Variable node?

Update: Figured out I didn’t connect the exec pin of the Assignment Node, so I wasn’t properly setting the variable. I needed a dedicated Init pin to address that. Boolean array persists fine. Now the problem is that my output Exec pin isn’t continuing despite passing the condition. Makes me think I missed a simple connection between my CallFunction and Branch nodes.

Hopefully I get a better perspective on it next morning, but here is the updated code if someone beats me to it.

Init Pin to Create and Set the Variable

// This does the one-time initialization to create the counter/flags variable needed for this mechanism. Problem is unlikely to be here, as this pin should NOT be connected to the output exec pin.
void UK2Node_MultiAND::ExpandInitPin(UEdGraphPin* InitPin, UK2Node_TemporaryVariable* TempVariable, FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
	UEdGraphSchema const* schema = SourceGraph->GetSchema();

	UK2Node_AssignmentStatement* assignArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_AssignmentStatement>(this);
	UK2Node_MakeArray* makeArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this);

	makeArrayNode->NumInputs = 0;
	makeArrayNode->AllocateDefaultPins();
	assignArrayNode->AllocateDefaultPins();

	// Initialize MakeArray with default bool values
	for(int i = 0; i < NumInputs; ++i)
	{
		makeArrayNode->AddInputPin();
		makeArrayNode->GetPinWithDirectionAt(i, EEdGraphPinDirection::EGPD_Input)->DefaultValue = "false";
	}

	// Execute the assignment node to set the variable.
	CompilerContext.MovePinLinksToIntermediate(*InitPin, *assignArrayNode->GetExecPin());

	//  Connect inputs of assignment node
	if(!schema->TryCreateConnection(TempVariable->GetVariablePin(), assignArrayNode->GetVariablePin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Failed to connect array variable to assignment node"), this);
	}

	if(!schema->TryCreateConnection(makeArrayNode->GetOutputPin(), assignArrayNode->GetValuePin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Failed to MakeArray output to assignment node"), this);
	}
}

.

Input Exec Pins - this is likely where the issue is.
Each input exec pin effectively spawns a “sub-graph” that calls the function and checks the condition

void UK2Node_MultiAND::ExpandExecPin(UEdGraphPin* ExecPin, UK2Node_TemporaryVariable* ExecFlagsVariable, int ExecPinIndex, FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
	UEdGraphSchema const* schema = SourceGraph->GetSchema();

	// Each exec pin will set its respective index in the boolean array, then check if the condition is met to proceed output execution
	UK2Node_CallFunction* setterFunctionNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this);
	setterFunctionNode->SetFromFunction(UXStudioStatics::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UXStudioStatics, SetAndTestBoolArray_AND)));
	setterFunctionNode->AllocateDefaultPins();
	setterFunctionNode->FindPinChecked(Priv::locSetterFuncIndexPinName, EEdGraphPinDirection::EGPD_Input)->DefaultValue = FString::FromInt(ExecPinIndex);

	// Pass the array by ref to the function arg
	if(!schema->TryCreateConnection(ExecFlagsVariable->GetVariablePin(), setterFunctionNode->FindPinChecked(Priv::locSetterFuncArrayPinName)))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect flags array variable to function input"), this);
	}

	// Check the return of the function - if true, then all exec pins have fired.
	UK2Node_IfThenElse* branchNode = CompilerContext.SpawnIntermediateNode<UK2Node_IfThenElse>(this);
	branchNode->AllocateDefaultPins();
	if(!schema->TryCreateConnection(setterFunctionNode->GetReturnValuePin(), branchNode->GetConditionPin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect output of function branch condition"), this);
	}

	// Connect the function and branch nodes
	if(!schema->TryCreateConnection(setterFunctionNode->GetThenPin(), branchNode->GetExecPin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect execution flow between function and branch nodes"), this);
	}

	// Connect the i-th Exec pin to the setter function.
	if(!CompilerContext.MovePinLinksToIntermediate(*ExecPin, *setterFunctionNode->GetExecPin()).CanSafeConnect())
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not link input exec pin to function"), this);
	}

	// If the branch succeeds, have its Then pin connected to the final output of this node - execution resumes
	//if(!CompilerContext.MovePinLinksToIntermediate(*branchNode->GetThenPin(), *GetThenPin()).CanSafeConnect())
	if(!CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *branchNode->GetThenPin()).CanSafeConnect())
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not link Then pin of branch to exec output of node"));
	}

	// End up with
	// Exec[i] --> CallFunction --> Branch --> true? --> Exec Out
}

.

Finally, the ExpandNode function

void UK2Node_MultiAND::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
	Super::ExpandNode(CompilerContext, SourceGraph);

	UEdGraphSchema const* schema = SourceGraph->GetSchema();

	// Create a boolean array, where arr[i] = true if the i-th exec pin was fired.
	// Assign its value from the MakeArray node
	UK2Node_TemporaryVariable* execFlagsVariable = CompilerContext.SpawnInternalVariable(this, UEdGraphSchema_K2::PC_Boolean, NAME_None, nullptr, EPinContainerType::Array);

	ExpandInitPin(GetInitPin(), execFlagsVariable, CompilerContext, SourceGraph);
	for(int i = 0; i < NumInputs; ++i)
	{
		UEdGraphPin* execPin = GetExecPin(i);
		check(execPin);

		ExpandExecPin(execPin, execFlagsVariable, i, CompilerContext, SourceGraph);
	}

	BreakAllNodeLinks();
}

You are moving the original Then pin connections to the Then pin of each Branch, one by one. So at the end, the original Then pin is only connected to the last Branch.

You can fix it by using CopyLinks instead of MoveLinks when connecting GetThenPin() to branchNode->GetThenPin(), then your final BreakAllNodeLinks() call should take care of cleaning the original links.

Alternatively, you can simplify the graph a bit by adding an intermediate variable for Idx, set its value, and feed it to a single CallFunction node (followed by a single Branch), something like this

You’ve got eyes of a hawk. That fixed it. It gets a bit difficult to visualize the final graph with how verbose spawning and connecting the nodes is. Also incorrectly assumed moving links will just create a duplicate connection.

As an exercise, I will attempt your proposed solution and share a follow up code for future reference.

Thanks!

Alternate solution mimicking the aforementioned graph for the curious:

ExpandNode

void UK2Node_ExecGate::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
	Super::ExpandNode(CompilerContext, SourceGraph);

	UEdGraphSchema const* schema = SourceGraph->GetSchema();

	// Create a boolean array, where arr[i] = true if the i-th exec pin was fired. Assign its value from the MakeArray node
	// Create an int that will be set by each input exec pin to the value of its idx. The idx is used to set the flag in the boolean array
	UK2Node_TemporaryVariable* execFlagsVariable = CompilerContext.SpawnInternalVariable(this, UEdGraphSchema_K2::PC_Boolean, NAME_None, nullptr, EPinContainerType::Array);
	UK2Node_TemporaryVariable* execIdxVariable = CompilerContext.SpawnInternalVariable(this, UEdGraphSchema_K2::PC_Int);

	// Initialize the bool array with the MakeArray + Assignment
	ExpandInitPin(GetInitPin(), execFlagsVariable, CompilerContext, SourceGraph);

	// Each exec pin will call this function with a different index, therefore setting its respective flag in the bool array
	UK2Node_CallFunction* setterFunctionNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this);
	setterFunctionNode->SetFromFunction(UXStudioStatics::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UXStudioStatics, SetAndTestBoolArray_AND)));
	setterFunctionNode->AllocateDefaultPins();

	// Pass the index variable for the function
	if(!schema->TryCreateConnection(execIdxVariable->GetVariablePin(), setterFunctionNode->FindPinChecked(Priv::locSetterFuncIndexPinName, EEdGraphPinDirection::EGPD_Input)))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect exec index variable to function input"), this);
	}

	// Pass the array by ref to the function arg
	if(!schema->TryCreateConnection(execFlagsVariable->GetVariablePin(), setterFunctionNode->FindPinChecked(Priv::locSetterFuncArrayPinName)))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect flags array variable to function input"), this);
	}

	// Every time the function is called, we want to check its return value and see if the condition is met. Use a branch, and connect it to the output exec
	UK2Node_IfThenElse* branchNode = CompilerContext.SpawnIntermediateNode<UK2Node_IfThenElse>(this);
	branchNode->AllocateDefaultPins();
	if(!schema->TryCreateConnection(setterFunctionNode->GetReturnValuePin(), branchNode->GetConditionPin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect output of function branch condition"), this);
	}

	// Connect the function and branch nodes
	if(!schema->TryCreateConnection(setterFunctionNode->GetThenPin(), branchNode->GetExecPin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect execution flow between function and branch nodes"), this);
	}

	// If the branch succeeds, have its Then pin connected to the final output of this node - execution resumes
	if(!CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *branchNode->GetThenPin()).CanSafeConnect())
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not link Then pin of branch to exec output of node"));
	}

	// Creates the additional nodes and connections for each input exec pin. SetByRef node --> CallFunction node
	for(int i = 0; i < NumInputs; ++i)
	{
		UEdGraphPin* execPin = GetExecPin(i);
		check(execPin);

		ExpandExecPin(execPin, execIdxVariable, setterFunctionNode, i, CompilerContext, SourceGraph);
	}

	BreakAllNodeLinks();
}

ExpandInit is unchanged from the prior solution.

ExpandExecPin

void UK2Node_ExecGate::ExpandExecPin(UEdGraphPin* ExecPin, UK2Node_TemporaryVariable* IdxVariable, UK2Node_CallFunction* SetTestFlagFunction, int ExecIdx, FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
	UEdGraphSchema const* schema = SourceGraph->GetSchema();

	UK2Node_VariableSetRef* setByRefNode = CompilerContext.SpawnIntermediateNode<UK2Node_VariableSetRef>(this);
	setByRefNode->AllocateDefaultPins();

	// Connect which variable we want to set by ref
	if(!schema->TryCreateConnection(IdxVariable->GetVariablePin(), setByRefNode->GetTargetPin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect index variable to SetByRef node"), this);
	}

	// Which value to set it to (the index of the exec pin)
	setByRefNode->GetValuePin()->DefaultValue = FString::FromInt(ExecIdx);

	// Connect to exec input 
	if(!CompilerContext.MovePinLinksToIntermediate(*ExecPin, *setByRefNode->GetExecPin()).CanSafeConnect())
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect SetByRef to input exec"), this);
	}

	// Call the test function with the newly set index
	if(!schema->TryCreateConnection(setByRefNode->FindPinChecked(UEdGraphSchema_K2::PN_Then), SetTestFlagFunction->GetExecPin()))
	{
		CompilerContext.MessageLog.Error(TEXT("@@ Could not connect SetByRef to function node"), this);
	}
}

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.