Creating Asynchronous Blueprint Nodes

From Epic Wiki

Overview

Author:


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:

  1. Create asynchronous node with its inputs and multiple exec outputs
  2. Trigger output signals to different exec outputs from node
  3. 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 .

CreateAsyncNode CreateClass.PNG

Creating Outputs

Outputs from async nodes in UE4 are done using dynamic multicast delegates ( Documentation , ). 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.

CreateAsyncNode BlueprintNode.PNG

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:

CreateAsyncNode BlueprintNode2.PNG

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.

CreateAsyncNode MiniTimer.PNG

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;
}