Mikelis' Game Blog

Easy C++ Latent Functions in Unreal Engine 5 Blueprints

Probably the easiest way to implement latent Blueprint functions in C++.

Mikelis' Game Devlog

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.

Mikelis' Game Devlog

Why use this approach?

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.

#Cpp #Engineering #Game Development #Shortform #Unreal Engine