Creating K2Node with multiple output exec pins linked to delegates

I have the following bit of code:

DECLARE_DYNAMIC_DELEGATE(FMyDelegate);

UCLASS()
class UMyClass : public UObject
{
  GENERATED_BODY()

public:
  UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = true))
  static void DoStuff(const TArray<FMyDelegate>& delegates);
}

I would like to create a K2Node that wraps around the function DoStuff. The node should look like this:


I’ve already managed to get the output exec pins to generate dynamically based on the default value of the Number Of Delegates pin.
However, I’m having some trouble getting the ExpandNode function to work. In my mind, the expanded node should look like this:

So, in ExpandNode, I’m generating a UK2Node_CallFunction for the DoStuff function, a UK2Node_MakeArray node, and a number of UK2Node_CustomEvent nodes. Here is my current ExpandNode function:

void UK2Node_MyNode::ExpandNode(FKismetCompilerContext& compilerContext, UEdGraph* sourceGraph)
{
  Super::ExpandNode(compilerContext, sourceGraph);
  
  const UEdGraphSchema_K2* schema = compilerContext.GetSchema();
  
  // Generate CallFunction node
  
  UK2Node_CallFunction* functionNode =
    compilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, sourceGraph);
 
  functionNode->FunctionReference.SetExternalMember(
    GET_FUNCTION_NAME_CHECKED(UMyClass, DoStuff), UMyClass::StaticClass());
 
  functionNode->AllocateDefaultPins();
  
  compilerContext.MovePinLinksToIntermediate(*GetExecPin(), *functionNode->GetExecPin());
  compilerContext.MovePinLinksToIntermediate(*GetThenPin(), *functionNode->GetThenPin());
  
  // Generate MakeArray node
  
  UK2Node_MakeArray* makeArrayNode =
      compilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this, sourceGraph);
	  
  makeArrayNode->AllocateDefaultPins();
  
  // Connect MakeArray node to CallFunction node
  
  schema->TryCreateConnection(
    makeArrayNode->GetOutputPin(), functionNode->FindPin(TEXT("delegates")));
	
  // Generate CustomEvent nodes
  
  const int32 numOfEvents = FCString::Atoi(*FindPin(numDelegatesPinName)->DefaultValue);
  for (int i = 0; i < numOfEvents; ++i)
  {
    UK2Node_CustomEvent* eventNode =
      compilerContext.SpawnIntermediateNode<UK2Node_CustomEvent>(this, sourceGraph);
	  
    eventNode->CustomFunctionName = TEXT("MyCustomEvent");
    eventNode->RenameCustomEventCloseToName();
    eventNode->bInternalEvent = true;
    eventNode->AllocateDefaultPins();
	
    // Connect CustomEvent node to MakeArray node
	
    makeArrayNode->AddInputPin();
    schema->TryCreateConnection(
      eventNode->GetDelegatePin(), makeArrayNode->FindPin(makeArrayNode->GetPinName(i)));
	  
    // Connect original node's output exec pin to CustomEvent node's exec pin
	
    compilerContext.MovePinLinksToIntermediate(
      *FindPin(delegatePinNames[i]), *eventNode->GetThenPin());
  }
  
  BreakAllNodeLinks();
}

Note that numDelegatesPinName is the FName of the Number Of Delegates pin, and delegatePinNames is a TArray of FNames that contains the names of the dynamically generated output exec pins.
When compiling the blueprint, I get the following errors on the custom node:


Presumably I have to tell the CustomEvent nodes that their signature is supposed to match the signature of FMyDelegate? But I have no clue how to do this.
Things I have tried, with no success:

  • Using the CustomEvent node’s SetDelegateSignature
UFunction* signatureFunc = FindObject<UFunction>(ANY_PACKAGE, TEXT("MyDelegate__DelegateSignature"));
eventNode->SetDelegateSignature(signatureFunc);
  • Setting the CustomEvent node’s EventReference via SetGlobalField
UPackage* package = FindPackage(nullptr, TEXT("/Script/MyGameProject"));
eventNode->EventReference.SetGlobalField(TEXT("MyDelegate__DelegateSignature"), package);

It’s entirely possible I’m doing something else wrong, like using the wrong types of nodes or not setting up the CallFunction/MakeArray nodes properly.
Any help would be appreciated. Thanks in advance!

If I understand correctly, what you want to do is basically bind an arbitrary number of events to a multicast delegate, or alternatively bind one event that has a sequence node to do multiple things

Honestly I would probably do it the first way, and instead just spawn multiple AddDelegate nodes

You can see how this is done in for example UK2Node_BaseAsyncTask::FBaseAsyncTaskHelper::HandleDelegateImplementation

Otherwise, something that can help is build intermediate products:

Compiling a blueprint with this enabled will show you all the intermediate graphs that are created, and you can see exactly what your node expands to, which helps a lot when debugging custom nodes

Just remember to close any intermediate tabs before disabling this and compiling again or the editor is very likely to crash

any reason you dont actually use delegates? then you can even define and output the signature too

Thank you for the replies!

@zeaf

what you want to do is basically bind an arbitrary number of events to a multicast delegate, or alternatively bind one event that has a sequence node to do multiple things

I don’t think this is exactly right. The DoStuff function is going to asynchronously pick one and only one delegate from the list to execute, so it’s not like I can just use a multicast delegate in place of the list.

something that can help is build intermediate products

I wasn’t aware of this intermediate products build option. Looks very useful, so I’ll give it a try later!

@Auran131

any reason you dont actually use delegates?

Sorry, I’m not sure I understand your question. Do you mean AddDelegate nodes?

i mean what would be an EventDispatcher in BP, rather than making custom events and adding them to an array you just broadcast an ED?

im not sure what exactly you’re trying to do though which is why im asking before i try to help

If you’re talking about creating event dispatchers in the BP, that unfortunately isn’t an option in my case.
The whole reason I’m making this node in the first place is that it’s going to be used by game designers who aren’t very familiar with blueprints, and have no idea how to create variables, functions, event dispatchers or custom events. I need the node on its own to look as simple and intuitive as possible.

Thanks to @zeaf’s advice to look into the blueprint’s intermediate products, I was able to find the issue.
From what I can tell, arrays of delegates simply don’t work properly in blueprints. Attempting to link Create Event nodes to a Make Array node gives a compile error, and even linking the output of a function that returns an array of delegates to another function which takes in an array of delegates seems to sometimes fail when compiling.

I came up with the following solution which involves no blueprint-exposed delegate arrays:

  1. Create a class whose entire purpose is to act as a wrapper for an array of delegates
UCLASS(BlueprintType)
class UDelegateArrayWrapper : public UObject
{
  GENERATED_BODY()
  
public:
  UFUNCTION(BlueprintCallable)
  void AddDelegate(const FMyDelegate& delegate);
  
  UFUNCTION(BlueprintCallable)
  const TArray<FMyDelegate>& GetDelegates() const;
  
private:
  TArray<FMyDelegate> delegates;
};
  1. Declare another version of DoStuff which takes a UDelegateArrayWrapper instead of an array of delegates
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = true))
static void DoStuffWithWrapper(const UDelegateArrayWrapper* delegateArrayWrapper);
  1. In the custom K2Node’s ExpandNode function, generate a CallFunction node, setting it to call UGameplayStatics::SpawnObject and spawn a UDelegateArrayWrapper, and add the custom events to it with the AddDelegate function

Here’s how the ExpandNode function turned out:

void UK2Node_MyNode::ExpandNode(FKismetCompilerContext& compilerContext, UEdGraph* sourceGraph)
{
  Super::ExpandNode(compilerContext, sourceGraph);
 
  const UEdGraphSchema_K2* schema = compilerContext.GetSchema();
 
  // Generate SpawnObject node
  
  UK2Node_CallFunction* spawnObjectNode =
    compilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, sourceGraph);
    
  spawnObjectNode->FunctionReference.SetExternalMember(
    GET_FUNCTION_NAME_CHECKED(UGameplayStatics, SpawnObject), UGameplayStatics::StaticClass());
    
  spawnObjectNode->AllocateDefaultPins();
  spawnObjectNode->FindPin(TEXT("ObjectClass"))->DefaultObject = UDelegateArrayWrapper::StaticClass();
  
  compilerContext.MovePinLinksToIntermediate(*GetExecPin(), *spawnObjectNode->GetExecPin());
  
  bool pinTypeSet = false;
  UEdGraphPin* latestThenPin = spawnObjectNode->GetThenPin();
  
  // For each delegate:
  for (int i = 0; i < delegatePinNames.Num(); ++i)
  {
    // Generate AddDelegate node
    
    UK2Node_CallFunction* addDelegateNode =
      compilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, sourceGraph);
 
    addDelegateNode->FunctionReference.SetExternalMember(
      GET_FUNCTION_NAME_CHECKED(UDelegateArrayWrapper, AddDelegate), UDelegateArrayWrapper::StaticClass());
 
    addDelegateNode->AllocateDefaultPins();

    schema->TryCreateConnection(
      latestThenPin, addDelegateNode->GetExecPin());
      
    if (!pinTypeSet)
    {
      spawnObjectNode->GetReturnValuePin()->PinType = 
        addDelegateNode->FindPin(UEdGraphSchema_K2::PN_Self)->PinType;
      pinTypeSet = true;
    }
    
    schema->TryCreateConnection(
      spawnObjectNode->GetReturnValuePin(), addDelegateNode->FindPin(UEdGraphSchema_K2::PN_Self));
      
    latestThenPin = addDelegateNode->GetThenPin();
    
    // Generate custom event node
    
    UK2Node_CustomEvent* eventNode =
      compilerContext.SpawnIntermediateNode<UK2Node_CustomEvent>(this, sourceGraph);
      
    eventNode->CustomFunctionName = TEXT("MyCustomEvent");
    eventNode->RenameCustomEventCloseToName();
    
    eventNode->AllocateDefaultPins();
    
    schema->TryCreateConnection(
      eventNode->GetDelegatePin(), addDelegateNode->FindPin(TEXT("delegate")));
      
    compilerContext.MovePinLinksToIntermediate(
      *FindPin(delegatePinNames[i]), *eventNode->GetThenPin());
  }
  
  // Generate DoStuffWithWrapper node
  
  UK2Node_CallFunction* doStuffNode =
    compilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, sourceGraph);
 
  doStuffNode->FunctionReference.SetExternalMember(
    GET_FUNCTION_NAME_CHECKED(UMyClass, DoStuffWithWrapper), UMyClass::StaticClass());
 
  doStuffNode->AllocateDefaultPins();

  if (!pinTypeSet)
  {
    spawnObjectNode->GetReturnValuePin()->PinType = 
      doStuffNode->FindPin(TEXT("delegateArrayWrapper"))->PinType;
  }
  
  schema->TryCreateConnection(
    spawnObjectNode->GetReturnValuePin(), doStuffNode->FindPin(TEXT("delegateArrayWrapper")));
    
  // Link remaining exec pins
  
  schema->TryCreateConnection(
    latestThenPin, doStuffNode->GetExecPin());
  compilerContext.MovePinLinksToIntermediate(
    *GetThenPin(), *doStuffNode->GetThenPin());
    
  BreakAllNodeLinks();
}

There might have been an easier solution, but this seems to work for me.