Easy C++ Latent Functions in Unreal Engine 5 Blueprints
Probably the easiest way to implement latent Blueprint functions in C++.
UCancellableAsyncAction and ExposedAsyncProxy
Unreal Engine 5 has more than a few ways to implement latent behaviors and latent blueprint functions in C++. However, possibly the easiest way is using UCancellableAsyncAction
.
Thanks to UCancellableAsyncAction
's UCLASS specifier meta=(ExposedAsProxy=AsyncAction)
, so long as derived C++ classes have specific fields (functions and properties), they will receive an automatically generated latent K2 node. And we don't need to deal with memory managing an FPendingLatentAction
, which is the traditional way of creating latent function nodes.
Below is an example implementation of a delay node with this method. Note that it has a factory function (UMyDelayAsyncAction::MyDelayAsyncAction) and two delegates - UMyDelayAsyncAction::OnComplete and UMyDelayAsyncAction::OnFail - which result in the respective output pins.
// .h
#pragma once
#include "CoreMinimal.h"
#include "Engine/CancellableAsyncAction.h"
#include "MyDelayAsyncAction.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMyDelayAsyncActionEvent);
/**
* An example async/latent action.
*/
UCLASS()
class PROJECT_API UMyDelayAsyncAction : public UCancellableAsyncAction
{
GENERATED_BODY()
public:
/**
* Start a delay.
* @param DelayTime How long the delay will last.
* @param WorldContext Object from which the world will be derived.
*/
UFUNCTION(BlueprintCallable, DisplayName="Delay (Custom)", meta=(WorldContext="WorldContext", BlueprintInternalUseOnly="true"))
static UMyDelayAsyncAction* MyDelayAsyncAction(const UObject* WorldContext, float DelayTime);
/** A delegate called when the async action completes. */
UPROPERTY(BlueprintAssignable)
FMyDelayAsyncActionEvent OnComplete;
/** A delegate called when the async action fails. */
UPROPERTY(BlueprintAssignable)
FMyDelayAsyncActionEvent OnFail;
// Start UCancellableAsyncAction Functions
virtual void Activate() override;
virtual void Cancel() override;
// End UCancellableAsyncAction Functions
// Start UObject Functions
virtual UWorld* GetWorld() const override
{
return ContextWorld.IsValid() ? ContextWorld.Get() : nullptr;
}
// End UObject Functions
private:
/** The context world of this action. */
TWeakObjectPtr<UWorld> ContextWorld = nullptr;
/** The time this action will wait before finishing. */
float DelayTime = 0.f;
/** The timer handle. */
FTimerHandle OngoingDelay;
};
// .cpp
#include "MyDelayAsyncAction.h"
#include "Engine.h"
UMyDelayAsyncAction* UMyDelayAsyncAction::MyDelayAsyncAction(const UObject* WorldContext, float DelayTime)
{
// This function is just a factory that creates a UMyDelayAsyncAction instance.
// We must have a valid contextual world for this action, so we don't even make it
// unless we can resolve the UWorld from WorldContext.
UWorld* ContextWorld = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull);
if(!ensureAlwaysMsgf(IsValid(WorldContext), TEXT("World Context was not valid.")))
{
return nullptr;
}
// Create a new UMyDelayAsyncAction, and store function arguments in it.
UMyDelayAsyncAction* NewAction = NewObject<UMyDelayAsyncAction>();
NewAction->ContextWorld = ContextWorld;
NewAction->DelayTime = DelayTime;
NewAction->RegisterWithGameInstance(ContextWorld->GetGameInstance());
return NewAction;
}
void UMyDelayAsyncAction::Activate()
{
// When the async action is ready to activate, set a timer using the world's FTimerManager.
if(const UWorld* World = GetWorld())
{
// The timer manager is a singleton, and GetTimerManger() accessor will always return a valid one.
FTimerManager& TimerManager = World->GetTimerManager();
// We set the timer for DelayTime, and we pass in the callback function as a lambda.
TimerManager.SetTimer(OngoingDelay,
FTimerDelegate::CreateLambda([WeakThis = TWeakObjectPtr<UMyDelayAsyncAction>(this)]()
{
// We're passing "this" as a weak pointer, because there is no guarantee that "this" will
// exist by the time this lambda callback executes.
if(WeakThis.IsValid() && WeakThis->IsActive())
{
// If everything went well, broadcast OnComplete (fire the On Complete pin), and wrap up.
WeakThis->OnComplete.Broadcast();
WeakThis->Cancel();
}
}),
FMath::Max(DelayTime, 0.001f), false);
return;
}
// If something failed, we can broadcast OnFail, and then wrap up.
OnFail.Broadcast();
Cancel();
}
void UMyDelayAsyncAction::Cancel()
{
Super::Cancel();
// Cancel the timer if it's ongoing, so OnComplete never broadcasts.
if(OngoingDelay.IsValid())
{
if(const UWorld* World = GetWorld())
{
FTimerManager& TimerManager = World->GetTimerManager();
TimerManager.ClearTimer(OngoingDelay);
}
}
}
The first declared static UFUNCTION-reflected function that returns an object pointer will be considered the factory function by Unreal Engine when making the custom K2 node. A factory function will be the function called when the K2 node is executed.
All UPROPERTY-reflected and BlueprintAssignable member delegates will appear as output execution pins in the custom K2 node.
The Javadoc-style documentation above the factory function determines what's shown as tooltips when visual programmers hover over our node. If a category is defined for the factory function, the latent function K2 node will appear in that category. Otherwise, the class name will be used as a category.
Why use this approach?
- Compared to the classic
FPendingLatentAction
method, implementing latent blueprint functions this way does not require manual memory management (as you'd need to create an FPendingLatentAction with thenew
keyword). - There isn't a need to keep track of
FLatentActionInfo
data like callback targets, linkage int32, and UUID to fire an output pin when the action is done - simply broadcasting on a delegate is enough. - You can cancel this async action individually by calling
Cancel()
on the returned async action object, which is more straightforward than cancelingFPendingLatentAction
. - Unlike
FPendingLatentAction
, this async action does not need to tick; it can be entirely event-based. - There is much less boilerplate code in the
UCancellableAsyncAction
approach than in theFPendingLatentAction
approach. - Because an async action object is returned when this latent function executes, this object can be used in a future-promise pattern, similar to how the "Run EQSQuery" blueprint function works.
- The code in the implementation of
UCancellableAsyncAction
is similar to gameplay actions (UGameplayTask
) and AI tasks (UAITask
). In fact, both of these classes use themeta=(ExposedAsyncProxy=AsyncTask)
specifier and treat factory functions and blueprint assignable delegates the same way to create a K2 node. So using this method keeps our codebase more consistent. - While initially it seems like using
UCancellableAsyncAction
could make it challenging to ensure only one instance of this action runs at a time per object or node, theFLatentActionInfo
can still be passed into the constructor with the UFUNCTION specifiermeta=(LatentInfo="ArgumentName")
to implement that functionality.
There are not many downsides to using UCancellableAsyncAction
compared to the older FPendingLatentAction
-based implementation of latent blueprint functions in C++. UCancellableAsyncAction
appears as the more modern approach, and I recommend using it.