Creating Asynchronous Blueprint Nodes
Contents
Overview
Author: Daniel ~b617 Janowski
In this tutorial, you will learn how to create your own asynchronous blueprint node with multiple outputs like that fancy "AI Move To" node.
You will learn how to:
- Create asynchronous node with its inputs and multiple exec outputs
- Trigger output signals to different exec outputs from node
- Add output variables to your async node
Requirements
You must have some understanding of using C++ in Unreal Engine. Experience with delegates in C++ will be helpful too.
Getting Started
Let's start by creating your own class extending BlueprintAsyncActionBase. For this tutorial I'll create node that stops execution and resumes it after one frame, so I'll call by class DelayOneFrame.
Creating Outputs
Outputs from async nodes in UE4 are done using dynamic multicast delegates (Documentation, Great tutorial by Rama). These are in essence your event dispatchers that can be found in normal blueprints.
First we have to define how our output will look. For now I'll create a node with just Exec outputs, we'll add output variables in later part of this tutorial.
DelayOneFrame.h
#pragma once
#include "Kismet/BlueprintAsyncActionBase.h"
#include "DelayOneFrame.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDelayOneFrameOutputPin);
UCLASS()
class BP_TESTS_API UDelayOneFrame : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FDelayOneFrameOutputPin AfterOneFrame;
/*...*/
};
The DECLARE_DYNAMIC_MULTICAST_DELEGATE is used to create a template for our output, showing what kind of variables we have there and what are their names. Since I don't want any output variables, just execution pins, I'm using DECLARE_DYNAMIC_MULTICAST_DELEGATE without the _OneParam, _TwoParams etc. postfix.
Property AfterOneFrame is our actual execution pin output. It has to be an UPROPERTY with BlueprintAssignable property modifier for it to show correctly in engine.
If you wanted more outputs from your node (like success/failure thing) you'd basically just add more variables like that. I recommend you use one template for them (like FDelayOneFrameOutputPin in my example), as having several different templates for outputs tends to sometimes work not as you'd expected it to.
Function Definition
The whole class we're creating is that one little node from our output. The node's going to represent an instance from our class. The function we're gonna expose to the blueprint should then create this instance, initialize it with input variables and return created instance.
Other than that we define a function as we'd do in a standard blueprint function library.
DelayOneFrame.h
/* Changed GENERATED_BODY() to GENERATED_UCLASS_BODY() to create a constructor to reset variables in */
public:
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category = "Flow Control")
static UDelayOneFrame* WaitForOneFrame(const UObject* WorldContextObject, const float SomeInputVariables);
private:
UObject* WorldContextObject;
float MyFloatInput;
DelayOneFrame.cpp
UDelayOneFrame::UDelayOneFrame(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), WorldContextObject(nullptr), MyFloatInput(0.0f)
{
}
UDelayOneFrame* UDelayOneFrame::WaitForOneFrame(const UObject* WorldContextObject, const float SomeInputVariables)
{
UDelayOneFrame* BlueprintNode = NewObject<UDelayOneFrame>();
BlueprintNode->WorldContextObject = WorldContextObject;
BlueprintNode->MyFloatInput = SomeInputVariables;
return BlueprintNode;
}
Executing Function - Node's behaviour
Our parent's class, UBlueprintAsyncActionBase presents us with a nice clean way to setup the actual node's code. When the node gets executed, a virtual function Activate() is triggered. So for our little example it'd look like this:
DelayOneFrame.h
// UBlueprintAsyncActionBase interface
virtual void Activate() override;
//~UBlueprintAsyncActionBase interface
private:
UFUNCTION()
void ExecuteAfterOneFrame();
DelayOneFrame.cpp
void UDelayOneFrame::Activate()
{
// Any safety checks should be performed here. Check here validity of all your pointers etc.
// You can log any errors using FFrame::KismetExecutionMessage, like that:
// FFrame::KismetExecutionMessage(TEXT("Valid Player Controller reference is needed for ... to start!"), ELogVerbosity::Error);
// return;
WorldContextObject->GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UDelayOneFrame::ExecuteAfterOneFrame);
}
void UDelayOneFrame::ExecuteAfterOneFrame()
{
AfterOneFrame.Broadcast();
}
As you can see, we're triggering Exec output pins by using Broadcast() on them like on any other delegate.
Using it in Blueprint
After all that, after a compile we should be able to add this async node to any Event graph.
Sometimes, if you can't see it in your blueprints immediately, try recompiling whole project source.
Adding variables to outputs
At this point adding output variables is a child's play. We have to just modify our output pin template and it's broadcasts:
DelayOneFrame.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDelayOneFrameOutputPin, float, InputFloatPlusOne, float, InputFloatPlusTwo);
DelayOneFrame.cpp
void UDelayOneFrame::ExecuteAfterOneFrame()
{
AfterOneFrame.Broadcast(MyFloatInput + 1.0f, MyFloatInput + 2.0f);
}
Result:
Final code
DelayOneFrame.h
#pragma once
#include "Kismet/BlueprintAsyncActionBase.h"
#include "DelayOneFrame.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDelayOneFrameOutputPin, float, InputFloatPlusOne, float, InputFloatPlusTwo);
UCLASS()
class BP_TESTS_API UDelayOneFrame : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UPROPERTY(BlueprintAssignable)
FDelayOneFrameOutputPin AfterOneFrame;
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category = "Flow Control")
static UDelayOneFrame* WaitForOneFrame(const UObject* WorldContextObject, const float SomeInputVariables);
// UBlueprintAsyncActionBase interface
virtual void Activate() override;
//~UBlueprintAsyncActionBase interface
private:
UFUNCTION()
void ExecuteAfterOneFrame();
private:
const UObject* WorldContextObject;
float MyFloatInput;
};
DelayOneFrame.cpp
#include "DelayOneFrame.h"
UDelayOneFrame::UDelayOneFrame(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), WorldContextObject(nullptr), MyFloatInput(0.0f)
{
}
UDelayOneFrame* UDelayOneFrame::WaitForOneFrame(const UObject* WorldContextObject, const float SomeInputVariables)
{
UDelayOneFrame* BlueprintNode = NewObject<UDelayOneFrame>();
BlueprintNode->WorldContextObject = WorldContextObject;
BlueprintNode->MyFloatInput = SomeInputVariables;
return BlueprintNode;
}
void UDelayOneFrame::Activate()
{
// Any safety checks should be performed here. Check here validity of all your pointers etc.
// You can log any errors using FFrame::KismetExecutionMessage, like that:
// FFrame::KismetExecutionMessage(TEXT("Valid Player Controller reference is needed for ... to start!"), ELogVerbosity::Error);
// return;
WorldContextObject->GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UDelayOneFrame::ExecuteAfterOneFrame);
}
void UDelayOneFrame::ExecuteAfterOneFrame()
{
AfterOneFrame.Broadcast(MyFloatInput + 1.0f, MyFloatInput + 2.0f);
}
More Examples
Mini-timer
This node executes its output every X seconds for Y seconds total.
MiniTimer.h
#pragma once
#include "Kismet/BlueprintAsyncActionBase.h"
#include "MiniTimer.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMiniTimerOutputPin);
UCLASS()
class BP_TESTS_API UMiniTimer : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UPROPERTY(BlueprintAssignable)
FMiniTimerOutputPin Update;
UPROPERTY(BlueprintAssignable)
FMiniTimerOutputPin Finished;
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category = "Timer|Mini-Timer")
static UMiniTimer* MiniTimer(const UObject* WorldContextObject, const float TimerInterval, const float TimerDuration);
// UBlueprintAsyncActionBase interface
virtual void Activate() override;
//~UBlueprintAsyncActionBase interface
private:
UFUNCTION()
void _Update();
UFUNCTION()
void _Finish();
private:
const UObject* WorldContextObject;
bool Active;
FTimerHandle Timer;
float TimerInterval;
float TimerDuration;
};
MiniTimer.cpp
UMiniTimer::UMiniTimer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer),
TimerInterval(1.0f), TimerDuration(5.0f), WorldContextObject(nullptr), Active(false)
{
}
UMiniTimer* UMiniTimer::MiniTimer(const UObject* WorldContextObject, const float TimerInterval, const float TimerDuration)
{
UMiniTimer* Node = NewObject<UMiniTimer>();
Node->WorldContextObject = WorldContextObject;
Node->TimerDuration = TimerDuration;
Node->TimerInterval = TimerInterval;
return Node;
}
void UMiniTimer::Activate()
{
if (nullptr == WorldContextObject)
{
FFrame::KismetExecutionMessage(TEXT("Invalid WorldContextObject. Cannot execute MiniTimer."), ELogVerbosity::Error);
return;
}
if (Active)
{
FFrame::KismetExecutionMessage(TEXT("MiniTimer is already running."), ELogVerbosity::Warning);
return;
}
if (TimerDuration <= 0.0f)
{
FFrame::KismetExecutionMessage(TEXT("Minitimer's TimerDuration cannot be less or equal to 0."), ELogVerbosity::Warning);
return;
}
if (TimerInterval <= 0.0f)
{
FFrame::KismetExecutionMessage(TEXT("Minitimer's TimerInterval cannot be less or equal to 0."), ELogVerbosity::Warning);
return;
}
Active = true;
FTimerHandle ShuttingOffTimer;
WorldContextObject->GetWorld()->GetTimerManager().SetTimer(Timer, this, &UMiniTimer::_Update, TimerInterval, true);
WorldContextObject->GetWorld()->GetTimerManager().SetTimer(ShuttingOffTimer, this, &UMiniTimer::_Finish, TimerDuration);
}
void UMiniTimer::_Update()
{
Update.Broadcast();
}
void UMiniTimer::_Finish()
{
WorldContextObject->GetWorld()->GetTimerManager().ClearTimer(Timer);
Timer.Invalidate();
Finished.Broadcast();
Active = false;
}