While our game may be running without any issues in the editor or even in…
Creating Latent Blueprint Nodes with Multiple Execution Pins
In this post I’m going to show you how to create latent Blueprint nodes that have multiple execution pins inside the Editor! The code in this post is written in version 4.26 so depending on your version you may need to perform a few changes.
Creating a latent node with multiple output pins
In this section, we’re going to create a latent node which will tick a few times depending on our input. For demonstration purposes, we’re going to create a node that will calculate a few fibonacci numbers in each tick.
Fortunately, it’s quite easy to create nodes with multiple output pins. To do that, create a new C++ class that inherits the BlueprintAsyncActionBase and add the following code to its header file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
/** * Signatures of execution pins in the editor */ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FBPNodeOutputPin,int32,FibonacciNumber); /** * */ UCLASS() class LATENTNODES_API UBPAsyncActionBase : public UBlueprintAsyncActionBase { GENERATED_BODY() private: /** * Keeps track of the last sequence number * Used in order to stop the execution at some point */ int32 FiboNum; /** * Contains the latest result from FibonacciNum */ int32 LastResult; /** * Recursive way of computing a fibonacci number * @param N the number of the sequence https://en.wikipedia.org/wiki/Fibonacci_number */ int32 FibonacciNum(int32 N); /** * Internal tick. Will also broadcast the Tick Execution pin in the editor */ UFUNCTION() void InternalTick(); /** * Internal completed. Clears timers and flags and broadcasts Completed Execution pin in the editor */ UFUNCTION() void InternalCompleted(); /** * Static property to prevent restarting the async node multiple times before execution has finished */ static bool bActive; /** * Numbers of fibonacci numbers to calculate */ int32 NumsToCalculate; /** * World context object to grab a reference of the world */ const UObject* WorldContext; /** * Timer handle of internal tick */ FTimerHandle TimerHandle; public: UPROPERTY(BlueprintAssignable) FBPNodeOutputPin Tick; UPROPERTY(BlueprintAssignable) FBPNodeOutputPin Completed; /** * InternalUseOnly to hide sync version in BPs */ UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category = "AsyncNode") static UBPAsyncActionBase* BPAsyncNode(const UObject* WorldContextObj, int32 Num); //Overriding BP async action base virtual void Activate() override; }; |
Then, in the source file, add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
bool UBPAsyncActionBase::bActive=false; //Init bactive for all instances int32 UBPAsyncActionBase::FibonacciNum(int32 N) { if (N == 0) { return 0; } else if (N == 1) { return 1; } else { return FibonacciNum(N-1) + FibonacciNum(N-2); } } void UBPAsyncActionBase::InternalTick() { FiboNum++; LastResult = FibonacciNum(FiboNum); Tick.Broadcast(LastResult); if (FiboNum >= NumsToCalculate) { InternalCompleted(); } } void UBPAsyncActionBase::InternalCompleted() { if (WorldContext) { WorldContext->GetWorld()->GetTimerManager().ClearTimer(TimerHandle); TimerHandle.Invalidate(); Completed.Broadcast(LastResult); UBPAsyncActionBase::bActive=false; } } UBPAsyncActionBase* UBPAsyncActionBase::BPAsyncNode(const UObject* WorldContextObj, int32 Num) { UBPAsyncActionBase* Node = NewObject<UBPAsyncActionBase>(); if (Node) { Node->WorldContext = WorldContextObj; Node->NumsToCalculate=Num; } return Node; } void UBPAsyncActionBase::Activate() { if (UBPAsyncActionBase::bActive) { FFrame::KismetExecutionMessage(TEXT("Async action is already running"), ELogVerbosity::Warning); return; } FFrame::KismetExecutionMessage(TEXT("Started Activate!"), ELogVerbosity::Log); if (WorldContext) { UBPAsyncActionBase::bActive=true; FTimerDelegate TimerDelegate; TimerDelegate.BindUObject(this,&UBPAsyncActionBase::InternalTick); WorldContext->GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDelegate, 0.1f, true); } else { FFrame::KismetExecutionMessage(TEXT("Invalid world context obj"), ELogVerbosity::Error); } } |
Compile your code and search for BPAsync Node somewhere in your blueprint graph:
If the node doesn’t appear, restart the editor.
Creating a latent node with multiple input pins
To create a latent node with multiple inputs we need the following:
- A way to specify the input pins
- The logic of what’s going to happen once each pin has fired
To demonstrate this kind of node, we’re going to create a function that interpolates an actor between two locations simulating a ping pong effect.
From the engine’s class wizard, create a new C++ class which doesn’t inherit anything and add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
#include "CoreMinimal.h" #include "UObject/WeakObjectPtr.h" #include "GameFramework/Actor.h" #include "Engine/LatentActionManager.h" #include "Engine/Public/LatentActions.h" /** * PingPongAction is the class which will handle the interpolation of an Actor between two locations */ class LATENTNODES_API FPingPongAction : public FPendingLatentAction { public: /** Actor we're going to interpolate */ AActor* ActorToPingPong; /** Starting location of interpolation */ FVector InitialLocation; /** Target location of interpolation */ FVector TargetLocation; /** True when we're stopping the movement */ bool bComplete; /** Elapsed time of a single interpolation */ float TimeElapsed; /** Total time for the interpolation from Initial to Target location*/ float TotalTime; /** Function to execute on completion */ FName ExecutionFunction; /** Link to fire on completion */ int32 OutputLink; /** * Object to call callback on upon completion */ FWeakObjectPtr CallbackTarget; FPingPongAction(AActor* InActor, FVector InitLoc, FVector TargetLoc, const FLatentActionInfo& LatentInfo) : ActorToPingPong(InActor) , InitialLocation(InitLoc) , TargetLocation(TargetLoc) , bComplete(false) , TimeElapsed(0.f) , TotalTime(1.f) , ExecutionFunction(LatentInfo.ExecutionFunction) , OutputLink(LatentInfo.Linkage) , CallbackTarget(LatentInfo.CallbackTarget) { } virtual void UpdateOperation(FLatentResponse& Response) override { TimeElapsed+=Response.ElapsedTime(); float Alpha = TimeElapsed / TotalTime; if (ActorToPingPong) { FVector NewLocation = FMath::Lerp(InitialLocation,TargetLocation,Alpha); ActorToPingPong->SetActorLocation(NewLocation); //If we have reached the target location swap initial and target //Hardcoded tolernace just for demonstration purposes. if (NewLocation.Equals(TargetLocation,15.f)) { FVector TempLocation = TargetLocation; TargetLocation = InitialLocation; InitialLocation = TempLocation; TimeElapsed=0.f; //Restart timer as well } } Response.FinishAndTriggerIf(bComplete || Alpha>=1.f,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("FPingPongAction", "ActionTimeFmt", "Ping Pong ({0} seconds left)"), FText::AsNumber(TotalTime - TimeElapsed, &DelayTimeFormatOptions)).ToString(); } #endif }; |
Once you’re done with this code, add a new Blueprint Function Library class in your project and type in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
UENUM() namespace EPingPongStatus { enum Type { Start, Stop }; } /** * */ UCLASS() class LATENTNODES_API UMyBPFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** * Interpolates the given actor between the given locations * @param InActor - the actor to interpolate * @param LocA - the starting location of the interpolation * @param LocB - the target location of the interpolation * @param PingPongStatus - the status of the ping pong * @param LatentInfo - the latent info to handle the progress in the background */ UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", ExpandEnumAsExecs = "PingPongStatus"), Category = "MyBPFunctions") static void PingPong(AActor* InActor, FVector LocA, FVector LocB, TEnumAsByte<EPingPongStatus::Type> PingPongStatus, FLatentActionInfo LatentInfo); }; |
Then, inside the source file add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include "MyBPFunctionLibrary.h" #include "GameFramework/Actor.h" #include "Engine/Engine.h" #include "Engine/LatentActionManager.h" #include "PingPongAction.h" void UMyBPFunctionLibrary::PingPong(AActor* InActor, FVector LocA, FVector LocB, TEnumAsByte<EPingPongStatus::Type> PingPongStatus, FLatentActionInfo LatentInfo) { if (UWorld* World = GEngine->GetWorldFromContextObjectChecked(InActor)) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); FPingPongAction* PingPongAction = LatentActionManager.FindExistingAction<FPingPongAction>(LatentInfo.CallbackTarget,LatentInfo.UUID); //If not currently running if (!PingPongAction) { if (PingPongStatus == EPingPongStatus::Start) { PingPongAction = new FPingPongAction(InActor,LocA,LocB, LatentInfo); LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, PingPongAction); } } else { if (PingPongStatus == EPingPongStatus::Stop) { PingPongAction->bComplete=true; } } } } |
At this point, you can compile your code and test the ping pong action we have created! You can open up any blueprint graph and search for the PingPong node (if the node doesn’t appear, you may need to restart your editor).
To sum up, in order to create a node with multiple input execution pins we created:
- An “Action” class which is performing the logic of what’s happening when each pin fires and
- An Enum which allows us to expose the different options in the UE Editor
Ping Pong node
Hello! Thanks for the article. Is there a way, when we want to use the 2nd approach with
LatentActionManager
but want also the “default” (non-latent) exec” pin coming out from the node? I see there is that possibility by default for latent node made by the 1st approach.What I need is basically a latent node created by a latent manager having both latent, and non-latent nodes.