Unreal Engine C++: Event, Dispatch, Delegates etc.
An alternative explanation to the obscure unreal doc. (first draft, some parts missing)
Concept
In Unreal Engine there is a fundamental mechanism called 'delegates' or also named 'events'. The concept is very similar to C# delegates or Qt signal & slot system. Here I'll explain what are delegates or event dispatchers and their C++ syntax In Unreal Engine. Let's start with a pseudo code example:
void some_function1(int param){ ... } void some_function2(int param){ ... } DelegateType my_delegate; my_delegate.add(some_function1); my_delegate.add(some_function2); // the call below will call some_function1(40) and some_function2(40) my_delegate.broadcast(40);
This pseudo illustrate the basic usage of a delegate, my_delegate is an object that can store one or several function pointers (or even object methods) and call back those functions with a single call to broadcast(). Parameters provided to broadcast() will be passed along as well.
One concrete use case for Delegates arise when coding UI elements. For instance, you would have a class in charge of drawing buttons or a checkbox etc. and you can use a delegate to notify the user whenever the element / checkbox is triggered. Here is some pseudo code to illustrate this:
// "Service provider": a class in charge of drawing some UI elements: class My_checkbox{ DelegateType my_delegate; public: // draw the UI: void draw() { ... } // function handling mouse events void mouse_event() { if( click toggles our checkbox ) my_delegate.broadcast(); // calls user functions } // "Client"/"user" of this class call this to be notified on click events: void on_checkbox_toggle_do( Function_pointer action) { my_delegate.add(action); } };
If you are familiar with Qt signal & slot system you'll see that a delegate is the equivalent of a signal. When in Qt we 'connect' a signal to a slot in Unreal we 'bind' or 'add' some function (or lambda, class method etc.) Finally broadcasting in Unreal is the equivalent to emitting a signal in Qt.
In Unreal's BluePrint you'll come across things named 'event dispatcher' which is just another name for 'delegate'. As we'll see later you can also call a delegate defined in C++ from the BluePrint.
Concrete C++ example
Dynamic Multicast Delegate
There are many types of delegates in unreal: static, dynamic, multicast, sparse, event... They all have their own drawbacks or advantages in terms of memory, performance or flexibility. I'll explain those differences in the next section. First, let's see how we implement a very common type of delegate in Unreal, the 'dynamic multicast delegate':
// We first declare the signature of the delegate through a macro provided by Unreal. // Our delegate type is named 'FMyDelegate' // we can bind to this delegate a functions with the type following: ' void some_fun(FString m)' // Note: delegate type must always start with the letter 'F' DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMyDelegate, FString, message); UCLASS() class FPSGAME_API AMyActor : public AActor { GENERATED_BODY() public: AMyActor(){ PrimaryActorTick.bCanEverTick = true; } // declaring an instance of our delegate named 'send_message' // 'send_message' is what will be exposed in the BluePrint thanks to 'BlueprintAssignable' // Note: only dynamic multicast delegates can use the 'BlueprintAssignable' property UPROPERTY(BlueprintAssignable) FMyDelegate send_message; UFUNCTION() void mySlot1(FString str){ GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, TEXT("Slot1")); GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, str); } UFUNCTION() void mySlot2(FString str){ GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, TEXT("Slot2")); GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, str); } protected: // Called when the game starts or when spawned virtual void BeginPlay() override{ Super::BeginPlay(); // Remark: you must use UFUNCTIONs with dynamic delegates. send_message.AddDynamic(this, &AMyActor::mySlot1); send_message.AddDynamic(this, &AMyActor::mySlot2); } public: // Called every frame virtual void Tick(float DeltaTime) override{ Super::Tick(DeltaTime); send_message.Broadcast(TEXT("Hello unreal")); } };
As you can see we declare a delegate type with a macro:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_XXX(FMyDelegateType, MyArgType1, MyArgName1);
The first parameter defines the name of the delegate's type then the following parameters in the macro define the signature of the function. If we intend to bind a function with 2 parameters for instance:
void some_fun(int a, FString& m)
then we need to use:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_twoParams(FDelegateTypeName, int, param1, FString&, param2);
Remark that in the case of a dynamic delegate you need to specify the type and names of the parameters each separated by commas. Officially you can declare up to eight parameters ('_eightParams'). Lastly you can use this macro in global space, namespace or even inside a class:
// Global space: DECLARE_DYNAMIC_MULTICAST_DELEGATE_twoParams(FMyDelegate1, ... ) namespace MyProject { // Fine to use in a namespace: DECLARE_DYNAMIC_MULTICAST_DELEGATE_twoParams(FMyDelegate2, ... ) class MyClass { // or even within the class declaration: DECLARE_DYNAMIC_MULTICAST_DELEGATE_twoParams(FMyDelegate3, ... ) }; }
Static Multicast Delegate
Next up is the static version of our multicast delegate,
// Note delegate type must always start with F // Multi-cast delegate signatures are not allowed to use a return value. // For static delegates the name of each parameter is not necessary DECLARE_MULTICAST_DELEGATE_OneParam(FMyDelegate, FString); void static_slot(FString str){ ... } UCLASS() class FPSGAME_API AMyActor : public AActor { GENERATED_BODY() public: // you cannot use UPROPERTY for a static delegate! // Following error is thrown if otherwise: // "unrecognized type must be a UCLASS, USTRUCT, or UENUM" // therefore this attribute won't be serialized (e.g. saved to a file) FMyDelegate send_message; void my_slot1(FString str, bool payload) { ... } protected: // Called when the game starts or when spawned virtual void BeginPlay() override{ Super::BeginPlay(); // Contrary to dynamic delegates you can bind to many things // function, class method etc. send_message.AddStatic(&staticSlot); // payload allowed send_message.AddUObject(this, &AMyActor::my_slot1, /*payload argument:*/true); send_message.AddLambda([](FString str) { ... }); // True after adding functions: bool s = send_message.IsBound(); } public: // Called every frame virtual void Tick(float DeltaTime) override{ Super::Tick(DeltaTime); send_message.Broadcast(TEXT("Hello unreal")); } }
Binding
First contrary to dynamic delegates, static delegates can bind to many things! A series of methods allows you to bind to un-managed function pointers: raw functions pointer, class methods and lambdas.
TODO: rolling menu
void AddStatic(FuncPtr* func, [Payload1 arg1, Payload2 arg2...])
Binds a raw C++ pointer global function delegatevoid AddLambda(FuncPtr* lambdaFunc, [Payload1 arg1, Payload2 arg2...])
Binds a C++ lambda delegate
Technically this works for any functor types, but lambdas are the primary use caseAddRaw(ClassType obj*, FuncPtr* func, [Payload1 arg1, Payload2 arg2...])
Binds a raw C++ pointer delegate
Raw pointer doesn't use any sort of reference,
If you bind a class method and the object's gets deleted when it will be unsafe to call. So be careful when calling Execute()!
For instance this can be potentialy unsafe (see BindRaw() below)
Then you can bind memory managed pointers. These keep a weak reference to your object. You can safely use IsBound() to call them with Broadcast():
TODO: rolling menu?
AddSP(objPtr, func, args...)
AddThreadSafeSP(...)
SP means "Shared Pointer"
Shared pointer-based member function delegate
AddUFunction(uObj*, FName funcName, args...)
UFunction-based member function delegate
here we give the function name as a string (FName("funcName")) instead of a pointer. note that it's preferable to do:
auto funName = GET_FUNCTION_NAME_CHECKED(AMyActor, mySlot2); rod_send_message.AddUFunction(this, funName);
AddUObject(uObj*, func, args...)
UObject-based member function delegateBindWeakLambda(obj*, func, args...)
Just like the non-weak variant
Remove()
RemoveAll()
Payload
up to four arguments?
Summary: Static Vs Dynamic
Static delegates:
- Can bind to many things
- Can use payload variables
- No serialization (No UPROPERTY())
Dynamic delegates:
- Only bind to UFUNCTION
- No payload arguments
- Binding is serialized
- UPROPERTY allowed (only multicast can use BlueprintAssignable)
Single cast delegates
There are only slight differences between multi-cast and single-cast delegates, mainly you can only bind one function at a time.
It follows that when the delegate is invoked the return value of the currently bound function is passed along.
In addition instead of using .Broadcast()
we now rely on .Execute()
or ExecuteIfBound()
.
TODO: rolling menu?
Dynamic single cast:
// Note delegate type must always start with F // single cast delegate can return a value since we can only bind a single function. DECLARE_DYNAMIC_DELEGATE_OneParam(FRodDoSomething, FString, message); class { // '(BlueprintAssignable)' is only allowed // on multicast delegate properties UPROPERTY() FRodDoSomething rod_send_message; UFUNCTION() void mySlot(FString str); UFUNCTION() void mySlot2(FString str); } // Only binds to UFUNCTIONS, can't use payload // Only the second bind will be take into account since it's a single cast: rod_send_message.BindDynamic(this, &AMyActor::mySlot); rod_send_message.BindDynamic(this, &AMyActor::mySlot2); // True after adding a function: s = rod_send_message.IsBound(); // Can possibly return a value if we specifed one: rod_send_message.ExecuteIfBound(TEXT("Hello unreal")); rod_send_message.Execute(TEXT("Hello unreal"));
Static single cast:
TODO: rolling menu?
TODO: code for static single cast delegate
// Note delegate type must always start with F // single cast delegate can return a value since we can only bind a single function. DECLARE_DELEGATE_OneParam(FRodDoSomething, FString, message); etc todo
- BindStatic(func, args...)
- BindLambda(func, args...)
- BindRaw(obj*, func, args...)
- BindRaw(obj*, func, args...) as always Be careful when calling Execute
As before with multi-cast delegates, single-cast static delegates also benefit from many binding types:
Memory managed pointers.
You can safely use ExecuteIfBound() (or IsBound()):
- BindSP(objPtr, func, args...)
- BindThreadSafeSP(...)
- BindUFunction(uObj*, funcName, args...)
- BindUObject(uObj*, func, args...)
- BindWeakLambda(obj*, func, args...)
Use Unbind() to remove a function.
Sparse delegates
TODO: check with mitchelli's slides
Sparse delegates are exactly the same as a dynamic multi-cast delegate except that it optimize for memory at the expense of slow bind times
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_<Num>Params(FDelegateName, args...)
Events
or events:
DECLARE_EVENT_<Num>Params
DECLARE_DERIVED_EVENT_<Num>Params
events are Similar to multicast, but only the class that declares it can call Broadcast() IsBound() and Clear(). This allows you to expose the event/delegate in your public interface for user to add callbacks but keep control of when to Broadcast in your class.Removing callback
TODO: investigate API
(FDelegateHandle )
FDelegateHandle hdle = OnPostResolvedSceneColorHandle = RendererModule->GetResolvedSceneColorCallbacks().AddRaw(this, &FWhiteNoiseCSManager::Execute_RenderThread); RendererModule->GetResolvedSceneColorCallbacks().Remove(OnPostResolvedSceneColorHandle); OnPostResolvedSceneColorHandle.Reset();
Delegate summary
- Single-cast:
- Single cast delegate
- Dynamic Single cast delegate
- Multi Cast:
- Multi-cast delegate
- Dynamic Multi-cast delegate
(is the only one that can useUPROPERTY(BlueprintAssignable)
) - Static
- can bind to many things
- can use payload variables
- No serialization (No
UPROPERTY()
) - Dynamic
- only bind to
UFUNCTION()
- No payload arguments
- Binding is serialized (saved)
UPROPERTY()
allowed
(only multicast can useBlueprintAssignable
)
Emits with Execute()
or ExecuteIfBound()
You can bind only one function, return type allowed.
You can bind several function, no return type:
Emits with .Broadcast()
Events: dynamic multi cast delegate but can't broadcast outside owner class
Sparse delegate: slow bind but memory efficient dyn multi cast delegate
Notes:
You can check if a founction .IsBound()
or remove previously added functions
with RemoveXxxx()
(see doc)
Bonus
Delegate functions support the same
Specifiers
as
UFunctions,
but use the UDELEGATE()
macro instead of UFUNCTION()
.
For example, the following code adds the BlueprintAuthorityOnly
Specifier to the
FInstigatedAnyDamageSignature
delegate
UDELEGATE(BlueprintAuthorityOnly) DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams( FInstigatedAnyDamageSignature, float, Damage, const UDamageType*, DamageType)
Discuss the various bindings
BindRaw to bind a method of an object BindSP is the same but safer since it uses a shared pointer, so your delegate won't crash if the pointer gets deleted.
BindStatic to bind normal functions I guess
https://forums.unrealengine.com/development-discussion/c-gameplay-programming/33283-delegate-createsp-createraw-createuobject-when-to-use-them
Create delegates through helper functions
Instead of declaring a delegate and calling .BindRaw()
,
.BindStatic()
etc. It is possible to construct and bind a delegate in a single line:
FExecuteAction on_clicked = FExecuteAction::CreateSP( this_ptr, &FPersonaMeshDetails::OnChanged, TypeAndText.Key, LODIndex);
FTimerDelegate del = FTimerDelegate::CreateStatic(&UAREffectStatics::SpawnProjectile);
??
FExecuteAction::CreateBind()
FExecuteAction::CreateRaw()
to do: list of helper functions.
References
- Mischitelli Slides
- Official UE Doc
- UE4 and C++, UPROPERTY (BlueprintCallable, BlueprintAssignable)
- UE4 C++ Event Dispatchers syntax
Blue print equivalent
No comments