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.
cpp.#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< ERunNodeResult
> & outResult; FLatentActionNode(ALatentExample* endDelegateTarget, const FLatentActionInfo& LatentInfo, TEnumAsByte < ERunNodeResult>
& 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 < ERunNodeResult>
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
< ERunNodeResult>
& 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<
FLatentActionNode
>(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 };
#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
Post a Comment