Creating Asynchronous Blueprint Nodes

You will learn how to create your own asynchronous blueprint node with multiple outputs.

Updated over 1 year ago Edit Page Revisions

Overview

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 a node that stops execution and resumes it after one frame, so I'll call by class DelayOneFrame.

File:CreateAsyncNode CreateClass.PNG

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 a 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.

It'll also need to register itself with the game instance, otherwise garbage collection may gobble it up. This means that when the async task is finished, it will also need to unregister itself from the game instance. These tasks are accomplished with the base async action methods RegisterWithGameInstance and SetReadyToDestroy respectively.

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();
    BlueprintNode->WorldContextObject = WorldContextObject;
    BlueprintNode->MyFloatInput = SomeInputVariables;
    // Register with the game instance to avoid being garbage collected
    BlueprintNode->RegisterWithGameInstance(WorldContextObject);
    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();
    // remember to unregister with the game instance so it can be
    // garbage collected now that we're done
    SetReadyToDestroy();
}

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.

File: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);
    // remember to unregister with the game instance so it can be
    // garbage collected now that we're done
    SetReadyToDestroy();
}

Result:

File: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();
    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);
    // remember to unregister with the game instance so it can be
    // garbage collected now that we're done
    SetReadyToDestroy();
}

More Examples

Mini-timer

This node executes its output every X seconds for Y seconds total.

File: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();
    Node->WorldContextObject = WorldContextObject;
    Node->TimerDuration = TimerDuration;
    Node->TimerInterval = TimerInterval;
    Node->RegisterWithGameInstance(WorldContextObject);
    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 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;
    SetReadyToDestroy();
}