Latent actions

Recently I discovered this extremely useful class called FPendingLatentAction. 

 

It is is basically a helper class to hold execution until a given condition is met. You can think of it as a ticking class that checks whatever condition each frame, once the condition is valid, it executes whatever code you put on it and then gets destroyed.

Looks simple enough, but it can be combined with BPs to hold the execution, which is VERY useful. 

One of the strengths of FPendingLatentAction is that they can be used within a class, and bound with delegates. You can use them to hold execution until you load something, perform any logic or just as a delay.

    *Depending on your needs you may want to use BlueprintAsyncActionBase /     CancellableAsyncAction instead, give it a look at the ref section at the end of the post.

 

To keep things simple I will keep all the code in a single .h and .cpp. So we are going to make an actor able to call the latent action, and the latent action.

The latent action base looks like this (no cpp for now):

#pragma once

#include "LatentActions.h"

 // Latent action meant to wait for Action end and then resume execution depending on the action result
class FLatentActionNode : public FPendingLatentAction
{
public:

    FName ExecutionFunction;
    int32 OutputLink;
    FWeakObjectPtr CallbackTarget;
    int32 UUID;

    //On creation we just assign the parameters
    FLatentActionNode(const FLatentActionInfo& LatentInfo)
        :
        ExecutionFunction(LatentInfo.ExecutionFunction),
        OutputLink(LatentInfo.Linkage),
        CallbackTarget(LatentInfo.CallbackTarget),
        UUID(LatentInfo.UUID)
    {}


    //Latent update, this is the "tick" of the latent function, when Respose.FinishAndTriggerIf first parameter
    //returns true, the action ends
    virtual void UpdateOperation(FLatentResponse& Response) override
    {    
        Response.FinishAndTriggerIf(false, ExecutionFunction, OutputLink, CallbackTarget);
    }


#if WITH_EDITOR
    // Returns a human readable description of the latent operation's current state
    virtual FString GetDescription() const override
    {
        return "Description of the latent action in progress";
    }
#endif
};

The responsible for deciding whether or not the action has finished is Response.FinishAndTriggerIf function. At the moment it wont end. But we can easily change that with a time parameter, like the delay function of the engine:

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once

#include "CoreMinimal.h"
#include "UObject/WeakObjectPtr.h"
#include "Engine/LatentActionManager.h"
#include "LatentActions.h"

// FDelayAction
// A simple delay action; counts down and triggers it's output link when the time remaining falls to zero
class FDelayAction : public FPendingLatentAction
{
public:
	float TimeRemaining;
	FName ExecutionFunction;
	int32 OutputLink;
	FWeakObjectPtr CallbackTarget;

	FDelayAction(float Duration, const FLatentActionInfo& LatentInfo)
		: TimeRemaining(Duration)
		, ExecutionFunction(LatentInfo.ExecutionFunction)
		, OutputLink(LatentInfo.Linkage)
		, CallbackTarget(LatentInfo.CallbackTarget)
	{
	}

	virtual void UpdateOperation(FLatentResponse& Response) override
	{
		TimeRemaining -= Response.ElapsedTime();
		Response.FinishAndTriggerIf(TimeRemaining <= 0.0f, ExecutionFunction, OutputLink, CallbackTarget);
	}

#if WITH_EDITOR
	// Returns a human readable description of the latent operation's current state
	virtual FString GetDescription() const override
	{
		static const FNumberFormattingOptions DelayTimeFormatOptions = FNumberFormattingOptions()
			.SetMinimumFractionalDigits(3)
			.SetMaximumFractionalDigits(3);
		return FText::Format(NSLOCTEXT("DelayAction", "DelayActionTimeFmt", "Delay ({0} seconds left)"), FText::AsNumber(TimeRemaining, &DelayTimeFormatOptions)).ToString();
	}
#endif
};

As you can see its easy to tweak. 

Now, how do you use latent actions in your BPs? We just need to create a function with the latent specifier and create our latent object inside:


h.


//Creates a FLatentActionNode and pauses BPs execution untill it FLatentActionNode ends
	UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"))
		void DoLatentTask( UObject* WorldContextObject, struct FLatentActionInfo LatentInfo);


.cpp

void ALatentExample::DoLatentTask(UObject* WorldContextObject, FLatentActionInfo LatentInfo)
{
	//Create and bind latent action
	if (UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject))
	{
		if (World )
		{
			//Check if the action exist, if so, end it and create a new one.
			FLatentActionNode* actionNode;
			FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
			actionNode = LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, LatentInfo.UUID);

			//If not running
			if (actionNode == nullptr)
			{
				actionNode = new FLatentActionNode( LatentInfo);
				LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, actionNode);
			}

		}
	}
}

 

 


 

 

 

 

We can make our LatentAction finish with an event trigger as well, we only need to bind it on creation(Dynamic delegates wont work, PendingLatentActions are not UObjects!):


.h



#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"

#include "LatentActions.h"

#include "LatentExample.generated.h"

//Delegate declaration
DECLARE_MULTICAST_DELEGATE(FOnEndActionDelegate);

UCLASS()
class CUSTOMSHADER_API ALatentExample : public AActor
{
	GENERATED_BODY()
	

public:	
	// Sets default values for this actor's properties
	ALatentExample();

	//Delegate property
	FOnEndActionDelegate OnEndAction;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UFUNCTION(BlueprintCallable)
	    void CallEndAction()
	{
		OnEndAction.Broadcast();
	}

//Creates a FLatentActionNode and pauses BPs execution untill it FLatentActionNode ends
UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"))
void DoLatentTask(ALatentExample* delegateTarget, UObject* WorldContextObject, struct FLatentActionInfo LatentInfo);
}; // Latent action meant to wait for Action end and then resume execution depending on the action result class FLatentActionNode : public FPendingLatentAction { public: FName ExecutionFunction; int32 OutputLink; FWeakObjectPtr CallbackTarget; int32 UUID; //EventProperty FDelegateHandle EndHandler; //Bool to switch when the delegate fires bool bEnd = false; FLatentActionNode(ALatentExample* endDelegateTarget, const FLatentActionInfo& LatentInfo) : ExecutionFunction(LatentInfo.ExecutionFunction), OutputLink(LatentInfo.Linkage), CallbackTarget(LatentInfo.CallbackTarget), UUID(LatentInfo.UUID) { EndHandler = endDelegateTarget->OnEndAction.AddRaw(this, &FLatentActionNode::EndAction); } //Latent update virtual void UpdateOperation(FLatentResponse& Response) override { Response.FinishAndTriggerIf(bEnd, ExecutionFunction, OutputLink, CallbackTarget); } //Bindeed to endDelegateTarget void EndAction() { bEnd = true; } #if WITH_EDITOR // Returns a human readable description of the latent operation's current state virtual FString GetDescription() const override { return "Description of the latent action in progress"; } #endif };
.cpp

#include "LatentExample.h"

// Sets default values
ALatentExample::ALatentExample()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ALatentExample::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ALatentExample::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ALatentExample::DoLatentTask(ALatentExample* delegateTarget, UObject* WorldContextObject, FLatentActionInfo LatentInfo)
{
//Create and bind latent action
if (UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject))
{
if (World )
{

//Check if the action exist, if so, end it and create a new one.
FLatentActionNode* actionNode;
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
actionNode = LatentActionManager.FindExistingAction<FLatentActionNode>(LatentInfo.CallbackTarget, LatentInfo.UUID);

//If not running
if (actionNode == nullptr)
{
actionNode = new FLatentActionNode(delegateTarget, LatentInfo);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, actionNode);

}

}
}
}
Print string is called after 5 seconds

 

Now our PB node will continue when the given actor calls CallEndAction function. It is useful if you want your player to select a dialogue option, or pause the flow until an actor finishes an action/ task for example.




We can even branch the flow based on our delegate target or an input parameter (more on branching in this post). That's pretty much it! I have added a static function so its easier to implement:


h.

 
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"

#include "LatentActions.h"

#include "LatentExample.generated.h"

//OutActionOptions
UENUM(BlueprintType)
enum ERunNodeResult
{
	Successful,
	Fail,
	None,
	NoEffect
};


//Delegate declaration
DECLARE_MULTICAST_DELEGATE_OneParam(FOnEndActionDelegate, TEnumAsByte);

UCLASS()
class CUSTOMSHADER_API ALatentExample : public AActor
{
	GENERATED_BODY()
	



public:	
	// Sets default values for this actor's properties
	ALatentExample();

	//Delegate property
	FOnEndActionDelegate OnEndAction;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;


	UFUNCTION(BlueprintCallable)
	void CallEndTask(TEnumAsByte result)
	{
		OnEndAction.Broadcast(result);
	}

//Creates a FLatentActionNode and pauses BPs execution untill it FLatentActionNode ends
	UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject", ExpandEnumAsExecs = "result"))
		void DoLatentTask(ALatentExample* delegateTarget, UObject* WorldContextObject, struct FLatentActionInfo LatentInfo, TEnumAsByte& result);

};




// Latent action meant to wait for Action end and then resume execution depending on the action result
class FLatentActionNode : public FPendingLatentAction
{
public:

	FName ExecutionFunction;
	int32 OutputLink;
	FWeakObjectPtr CallbackTarget;
	int32 UUID;

	//EventProperty
	FDelegateHandle EndHandler;
	//Bool to switch when the delegate fires
	bool bEnd = false;
	//OutResult
	TEnumAsByte& outResult;


	FLatentActionNode(ALatentExample* endDelegateTarget, const FLatentActionInfo& LatentInfo, TEnumAsByte& result)
		:
		ExecutionFunction(LatentInfo.ExecutionFunction),
		OutputLink(LatentInfo.Linkage),
		CallbackTarget(LatentInfo.CallbackTarget),
		UUID(LatentInfo.UUID),
		outResult(result)
	{
		
		EndHandler = endDelegateTarget->OnEndAction.AddRaw(this, &FLatentActionNode::EndTask);

	}
	

	//Latent update
	virtual void UpdateOperation(FLatentResponse& Response) override
	{	
		Response.FinishAndTriggerIf(bEnd, ExecutionFunction, OutputLink, CallbackTarget);
	}

	//Bindeed to endDelegateTarget
	void EndTask(TEnumAsByte out)
	{
		bEnd = true;
		outResult = out;
		
	}

	//Static latentAction helper so we can call it with just one line anywhere
	static void CreateLatentActionNode(ALatentExample* delegateTarget, UObject* WorldContextObject, FLatentActionInfo LatentInfo, TEnumAsByte& result)
	{
		//Create and bind latent action
		if (UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject))
		{
			if (World)
			{

				//Check if the action exist, if so, end it and create a new one.
				FLatentActionNode* actionNode;
				FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
				actionNode = LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, LatentInfo.UUID);

				//If not running
				if (actionNode == nullptr)
				{
					actionNode = new FLatentActionNode(delegateTarget, LatentInfo, result);
					LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, actionNode);
				}
			}
		}
	}

#if WITH_EDITOR
	// Returns a human readable description of the latent operation's current state
	virtual FString GetDescription() const override
	{
		return "Description of the latent action in progress";
	}
#endif
};
cpp.
 
#include "LatentExample.h"

// Sets default values
ALatentExample::ALatentExample()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ALatentExample::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ALatentExample::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ALatentExample::DoLatentTask(ALatentExample* delegateTarget, UObject* WorldContextObject, FLatentActionInfo LatentInfo, TEnumAsByte& result)
{
	//Simpler implementation
	FLatentActionNode::CreateLatentActionNode(delegateTarget, WorldContextObject, LatentInfo, result);
}






 


Prints NoEffect(or whatever we feed CallEndTask) after 5 seconds

 

 

 

hat's pretty much it, a powerful way to control flow execution. Hope you find it useful!



References :

 

Excelent examples of both latentActions and blueprintAsyncNode:

Creating latent blueprint nodes with multiple execution pins 

LatentAction:

Custom delayed blueprint function in c

Creating latent functions for blueprints in c

BlueprintAsyncNode:

Easy c latent functions in unreal engine blueprints

Creating asynchronous blueprint nodes



Comments