What's the best way to allow binding via both Blueprint and C++?

After following the Event Dispatchers and Delegates quickstart for both Blueprints and C++, I wanted to extend the C++ version to also trigger explosions, which the Blueprint quickstart does but the C++ quickstart omits. Additionally, I wanted the communication for the explosion trigger to be effected through the Event Graph rather than having to build an Explosion C++ class.

In short, an acceptable solution must:

  1. Allow multiple C++ classes to execute custom code when the boss dies; AND
  2. Allow multiple Blueprint classes to execute custom Event Graph logic when the boss dies; AND
  3. Allow the Unreal Editor user to select a (possibly different) boss for each instance from within the applicable Details pane (i.e. selecting the Boss Died Reference should work like it does in the quickstart); AND
  4. Be closer to optimal than my solution in terms of memory footprint, speed, quality, etc.

What I had hoped to do was simply expose the OnBossDied delegate via a UFUNCTION or UPROPERTY and have it work for both Blueprint and C++ bindings, but I couldn’t make that approach work without compilation errors. So, I implemented the following working solution instead.

Changes to BossActor.h:

// Declare two different delegates outside of the class
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnBossDiedBPDelegate);
DECLARE_MULTICAST_DELEGATE(FOnBossDiedCppDelegate);
// ...

protected:
// Add BlueprintCallable to the UFUNCTION for the code-based event handler
	UFUNCTION(BlueprintCallable, Category="Boss Died Notifications")
	void HandleBossDiedEvent();

public:
// Add a UPROPERTY to the Blueprint delegate
	UPROPERTY(BlueprintAssignable, Category = "Boss Died Notifications")
	FOnBossDiedBPDelegate OnBossDiedBP;
	FOnBossDiedCppDelegate OnBossDiedCpp;

Changes to BossActor.cpp:

// Switch to Broadcast() because we are using multicast, and
// call it on both delegate types
void ABossActor::HandleBossDiedEvent()
{
	OnBossDiedBP.Broadcast();
	OnBossDiedCpp.Broadcast();
}

Changes to DoorActor.cpp:

// Rename OnBossDied to OnBossDiedCpp, and
// call AddUObject() instead of BindUObject()
BossActorReference->OnBossDiedCpp.AddUObject(this, &ADoorActor::BossDiedEventFunction);

Save and compile the above changes, and then follow the steps from the Blueprint version of the quickstart for the Blueprint_Effect_Explosion asset. As stated at the beginning, my solution works fine on my machine. When we simulate the boss dying by moving the player character into the BP_BossActor collision box, the door opens, and the explosions fire. Nonetheless, this two-delegate solution feels awkward and silly. There must be a way to use a single delegate to accomplish the same goal, right?

Why wouldn’t you just use this delegate also from C++?
You don’t need the FOnBossDiedCppDelegate separately.
The non-DYNAMIC delegates are there for cases where efficiency matters and you know you don’t need the dynamic behavior, but otherwise, you can just use the DYNAMIC for everything.

Here’s what I figured out, thanks in part to jwatte’s hint.


SOLUTION

BossActor.h

// Outside class declaration
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnBossDiedBPDelegate);

// Inside class declaration
public:
	UPROPERTY(BlueprintAssignable)
	FOnBossDiedBPDelegate OnBossDiedBP;

  • HandleBossDiedEvent() does not need to be exposed via UFUNCTION because we don’t intend to call or extend it from Blueprint Assets. In this example, HandleBossDiedEvent() gets called by ABossActor::NotifyActorBeginOverlap() only.

  • OnBossDiedBP does require exposure via UPROPERTY(BlueprintAssignable) if we want to be able to assign/bind an event to it within a Blueprint Event Graph.

BossActor.cpp

  • No changes except for removing the OnBossDiedCpp code.

DoorActor.h

// Add the UFUNCTION here!
UFUNCTION()
void BossDiedEventFunction();

DoorActor.cpp

// Use AddDynamic()! 
BossActorReference->OnBossDiedBP.AddDynamic(this, &ADoorActor::BossDiedEventFunction);

Basically, I put the UFUNCTION on the wrong function on my first try. It goes on BossDiedEventFunction() in DoorActor.h, not HandleBossDiedEvent() in BossActor.h. As well, AddDynamic() was the other big missing piece that led me to my first solution rather than this one.


NOTES
I’m adding this for anyone else who’s trying to learn.

  • BossDiedEventFunction() is required to be declared a UFUNCTION because we bind it to OnBossDiedBP. I’m not entirely certain why, but maybe the BP_BossActor can’t be bound to a function unless the function is recognized by the Unreal Engine reflection system. It would be helpful to have a clear visual of how UFUNCTION, UPROPERTY, and UCLASS map various parts of code to their respective changes/representations in the Unreal Editor. Please comment if you know of such a resource.

  • Other than the door not opening, UE gave no indication that BossDiedEventFunction() needed to be specified as a UFUNCTION. UE could’ve saved me a ton of debugging time if AddDynamic() returned a boolean to indicate whether the binding was successful, if it failed compilation, or if it threw a Runtime Error when the bind failed. Instead, it fails silently and apparently provides no programmatic means of failure detection and mitigation. If someone knows a better way to approach this, or if I’m mistaken, please share.

  • I didn’t realize that AddDynamic() exists because I was looking at the Multicast Delegates documentation rather than the Dynamic Delegates documentation. The existence of AddDynamic() was further obscured because it doesn’t appear in the code completion menu in VS 2022. This mistake is on me, but hopefully I can spare other folks some time and frustration with this note.

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