Hi there, the issue I’m encountering seems pretty simple to solve but I don’t have the necessary knowledge in K2Nodes to find a proper solution.
I’m simply trying to connect a delegate (passing an int) to an output pin in a K2Node.
1. Where I am right now
-
I’m creating a custom blueprint callable node that replicates a simple for loop. So far, this works fine and looks like that in Unreal:
-
As the node look a bit ugly, I want to map it to a custom K2Node. I’m in this process right now, and it’s where things get messed up for me.
- I successfully completed the visual and the
AllocateDefaultPins()
part. So the node now looks a bit more like a conventional for loop:
- I successfully completed the visual and the
2. The doubts I have
-
First off, I’m not so sure if the way I do things when creating my for loop is the right one -with the delegates-. Friendly reminder, I’m still fairly new to c++ so be kind please.
- If there is a better way to tackle that I’d love to now!
-
While working on the
ExpandNode()
function in the K2Node, I can’t quite figure out how to properly connect the delegates with their parameters to the output pins andIndex
integer.
3. Some of my code
Here’s how I declare my dynamic delegates with parameters:
/** Delegate for the loop body, passing the current index */
DECLARE_DYNAMIC_DELEGATE_OneParam(FLoopBodyDelegate, int32, Index);
/** Delegate for loop completion */
DECLARE_DYNAMIC_DELEGATE(FLoopCompletedDelegate);
/** Delegate for loop cancellation */
DECLARE_DYNAMIC_DELEGATE(FLoopCanceledDelegate);
So the two parameters I’m trying to connect are Index
from the first OnLoopBody
delegate, and CanceledAtIndex
from the third delegate OnTaskCanceled
.
So the UFUNCTION
, public and private declarations looks like:
public:
UFUNCTION(BlueprintCallable, Category = "Editor Task|Slow Loops")
static USlowTaskNodes* SlowForLoop(
int32 StartIndex,
int32 EndIndex,
bool bCanCancelTask,
FLoopBodyDelegate LoopBody,
FLoopCompletedDelegate OnCompleted,
FLoopCanceledDelegate OnCanceled);
virtual void Activate() override;
private:
int32 StartIndex;
int32 EndIndex;
bool bCanCancelTask;
FLoopBodyDelegate LoopBodyDelegate;
FLoopCompletedDelegate OnCompletedDelegate;
FLoopCanceledDelegate OnCanceledDelegate;
The loop logic seems to be working fine as is:
USlowTaskNodes* USlowTaskNodes::SlowForLoop(
int32 InStartIndex,
int32 InEndIndex,
bool bInCanCancelTask,
FLoopBodyDelegate InLoopBody,
FLoopCompletedDelegate InOnCompleted,
FLoopCanceledDelegate InOnCanceled)
{
USlowTaskNodes* Node = NewObject<USlowTaskNodes>();
Node->StartIndex = InStartIndex;
Node->EndIndex = InEndIndex;
Node->bCanCancelTask = bInCanCancelTask;
Node->LoopBodyDelegate = InLoopBody;
Node->OnCompletedDelegate = InOnCompleted;
Node->OnCanceledDelegate = InOnCanceled;
return Node;
}
void USlowTaskNodes::Activate()
{
// Calculate the total number of iterations
const int32 TotalIterations = EndIndex - StartIndex + 1;
// Only create and show the dialog if running in the editor
bool bShowDialog = GIsEditor;
// Create a slow task
FScopedSlowTask SlowTask(TotalIterations, FText::FromString(TEXT("Processing...")), bShowDialog);
if (bShowDialog)
{
SlowTask.MakeDialog(bCanCancelTask); // Show cancel button if allowed
}
// Execute the loop
bool bIsCanceled = false;
for (int32 Index = StartIndex; Index <= EndIndex; ++Index)
{
// Check if the task was canceled (only relevant if bShowDialog is true)
if (bShowDialog && SlowTask.ShouldCancel())
{
bIsCanceled = true;
break;
}
// Update the slow task progress
SlowTask.EnterProgressFrame(1,
FText::Format(LOCTEXT("SlowTask", "Processing... {0} / {1}"), FText::AsNumber(Index - StartIndex + 1),
FText::AsNumber(EndIndex - StartIndex + 1)));
// Call the loop body delegate
if (LoopBodyDelegate.IsBound())
{
LoopBodyDelegate.Execute(Index);
}
}
// Handle task completion or cancellation
if (bIsCanceled)
{
// Trigger the OnCanceled delegate
if (OnCanceledDelegate.IsBound())
{
OnCanceledDelegate.Execute();
}
}
else
{
// Trigger the OnCompleted delegate
if (OnCompletedDelegate.IsBound())
{
OnCompletedDelegate.Execute();
}
}
// Mark as ready for destruction
SetReadyToDestroy();
}
On the K2Node side, the AllocateDefaultPins()
function successfully binds everything:
void USlowForLoop::AllocateDefaultPins()
{
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
// Execution Input Pin
CreatePin(EGPD_Input, K2Schema->PC_Exec, FName("Execute"));
// Input Pins
CreatePin(EGPD_Input, K2Schema->PC_Int, FName("Start Index"));
CreatePin(EGPD_Input, K2Schema->PC_Int, FName("End Index"));
CreatePin(EGPD_Input, K2Schema->PC_Boolean, FName("Can Cancel Task"));
// Output Pins
CreatePin(EGPD_Output, K2Schema->PC_Exec, FName("Loop Body"));
CreatePin(EGPD_Output, K2Schema->PC_Int, FName("Index"));
CreatePin(EGPD_Output, K2Schema->PC_Exec, FName("Completed"));
CreatePin(EGPD_Output, K2Schema->PC_Exec, FName("Canceled"));
}
Now in the ExpandNode()
function, things get dirty. I’ve managed to make everything work except the 3 delegates (one having the index parameter to pass -LoopBody-).
That’s where I’m stuck right now:
void USlowForLoop::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
Super::ExpandNode(CompilerContext, SourceGraph);
const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema();
if (!Schema)
{
CompilerContext.MessageLog.Error(*LOCTEXT("SchemaError", "SlowForLoop: Schema not found. @@").ToString(), this);
return;
}
// Track whether all operations are error-free
bool bIsErrorFree = true;
// Retrieve the SlowForLoop function
UFunction* Function = USlowTaskNodes::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(USlowTaskNodes, SlowForLoop));
if (!Function)
{
CompilerContext.MessageLog.Error(*LOCTEXT("FunctionError", "SlowForLoop: Function 'SlowForLoop' not found in USlowTaskNodes. @@").ToString(), this);
return;
}
// Create CallFunction node
UK2Node_CallFunction* CallSlowForLoop = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
CallSlowForLoop->SetFromFunction(Function);
CallSlowForLoop->AllocateDefaultPins();
// Move input pins
bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(TEXT("Start Index")), *CallSlowForLoop->FindPinChecked(TEXT("StartIndex"))).CanSafeConnect();
bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(TEXT("End Index")), *CallSlowForLoop->FindPinChecked(TEXT("EndIndex"))).CanSafeConnect();
bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(TEXT("Can Cancel Task")), *CallSlowForLoop->FindPinChecked(TEXT("bCanCancelTask"))).CanSafeConnect();
// Move execution input pin
bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(TEXT("Execute")), *CallSlowForLoop->GetExecPin()).CanSafeConnect();
/////////////////////////////////////////////////////////
// TODO: Move delegates (LoopBody, OnCompleted, OnCanceled) and Index output pins
/////////////////////////////////////////////////////////
// If any operation failed, log an error
if (!bIsErrorFree)
{
CompilerContext.MessageLog.Error(*LOCTEXT("InternalConnectionError", "SlowForLoop: Internal connection error occurred while expanding node. @@").ToString(), this);
}
// Cleanup
BreakAllNodeLinks();
}
So if you have ever found a solution for that, or have a logic rework idea, please give your insights, it would be much appreciated!