Delegates are an implementation of the Observer pattern in Unreal Engine, a behavioral design pattern that allows an object (the publisher) to inform its subscribed objects (observers) of important changes while maintaining low coupling between objects. This enables changes in the logic of different modules without worrying about unwanted consequences.
It is likely that Unreal Engine developers first encountered delegates when using the UShapeComponent
, where they subscribed to the OnComponentBeginOverlap
or OnComponentEndOverlap
event.
Example with addDynamic subscription to callback
/** Code for when something overlaps this component */
UFUNCTION()
void OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
void UTP_PickUpComponent::BeginPlay()
{
Super::BeginPlay();
// Register our Overlap Event
OnComponentBeginOverlap.AddDynamic(this, &UTP_PickUpComponent::OnSphereBeginOverlap);
}
Even if this happens through the Event Dispatcher, which provides the same functionality as delegates but in the Blueprint editor.
Screenshot of Event Dispatcher from OnComponentBeginOverlap
To start using delegates, you need to:
1 Declare it using a macro, at the end of which you specify the number of parameters that the delegate will pass.
Example of delegate with explained parameters
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPickUp, AObserverExampleCharacter*, PickUpCharacter);
2 Declare the presence of an instance of this delegate. To make the delegate work in Blueprints, use UPROPERTY(BlueprintAssignable)
.
Example of the delegate object
/** Delegate to whom anyone can subscribe to receive this event */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnPickUp OnPickUp;
3 Design and implement the logic where the delegate will notify subscribers about an event and use the Broadcast()
method at this point.
Example with broadcast
OnPickUp.Broadcast(Character);
- Use the
AddDynamic()
method in all subscribers, passing the subscriber object and callback function. The method must accept the same parameters that the delegate will send. It should also be marked withUFUNCTION
.
Example with subscription to the event and callback method declaration
UFUNCTION()
void HandlePickUp(AObserverExampleCharacter* PickUpCharacter);
void UExampleComponent::BeginPlay()
{
Super::BeginPlay();
AActor* Owner = GetOwner();
if (Owner)
{
UTP_PickUpComponent* PickUpComponent = Owner->FindComponentByClass<UTP_PickUpComponent>();
if (PickUpComponent)
{
PickUpComponent->OnPickUp.AddDynamic(this, &UYourComponent::HandlePickUp);
}
}
}
Now let's repeat it with an example
Let's develop a health system and a response to damage received. We'll start with the health component, which will store variables related to health, handle taking damage, and also notify about the damage received using a delegate.
We also want to spawn a camera shake when damage is taken. Let's delegate this to the DamageVisualizationComponent. In the BeginPlay, we'll find the health component on the owner and subscribe to the OnDamageReceived event.
Additionally, let's make the damage apply effects to the character. We'll create an Effects Component, do the same as before—subscribe in BeginPlay and handle the OnDamageReceived event.
For the example, we also need to figure out how to take damage. Let's create a platform, where stepping on it will cause the player to take damage.
And here's the result: the health component notifies all of its subscribers, who can then implement the necessary logic.
In some cases, using delegates may be excessive, but an architecture where responsibility is divided between modules is always convenient.
Types of Delegates
In this article, only the Dynamic_Multicast
delegate was used, but this is not always necessary. Use Dynamic
if the delegate will be used in Blueprints. If there will only be one subscriber, use Single
instead of Multicast
when declaring the delegate.
Conclusion
Why use the Observer pattern:
The Observer pattern is useful in complex applications and games because it enhances modularity and minimizes coupling between components. Instead of interacting directly with each other, components can subscribe to events occurring in other parts of the system. This reduces dependencies between components, making them easier to test and modify since one component can be changed or replaced without rewriting the other parts of the system. In games, this is especially important because the logic often changes based on the game state or player actions, and the Observer pattern allows such changes to be handled easily.
Summary:
Delegates in Unreal Engine are a powerful tool for implementing the Observer pattern, which helps manage notifications between different components of the system with minimal coupling. Although delegates may be unnecessary in some cases, their use promotes a cleaner and more structured architecture, making it easier to maintain and expand the project. Thus, delegates play a key role in simplifying and improving the architecture of gameplay in Unreal Engine, making the system more flexible and easily modifiable.
Top comments (0)