Skip to main content
  1. posts/

Async Blueprint nodes

·739 words·4 mins
UnrealEngine Blueprints GameDev C++ Async
Table of Contents

Async nodes what are they and why to use them.
#

Async blueprint nodes enables you to handle additional exec nodes that may fire at some different time. This allows us to do some background logic and even async actions that will fire event somewhere in future.

Note! Those nodes can be only used in Event graphs. Those nodes can’t be created in functions! Async Node

Use Case
#

It is hard to understand without example so I have one that I needed for my project. I have one class named Anomaly it is Actor with collision box, that when player is colliding with it starts playing anomaly action and that action have some duration. I would love to have easy way to:

  1. Spawn Anomaly that will not take much of performance.
  2. Get Notification when Anomaly is spawned
  3. Get Notification when Anomaly starts
  4. Get Notification when Anomaly ends

To do so I need to:

  1. Create c++ class that derives from UBlueprintAsyncActionBase.
  2. create static function creating our node.
  3. Create dynamic, multicast delegate without parameters, one for each exec node.
  4. Override Activate function.

Creating out node
#

Create task class
#

UCLASS()
class ANOMALIES_API ULoadAnomaly : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
}

Create fields that will hold important references
#

const UObject* WorldContextObject;
TSoftClassPtr<AAnomaly> AnomalyToLoad;
TObjectPtr<AAnomaly> LoadedAnomaly;
TSharedPtr<FStreamableHandle> LoadingObject;

Create static function used to create this node
#

// .h
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category = "Flow Control")
	static ULoadAnomaly* LoadAnomalyAsync(const UObject* WorldContextObject, TSoftClassPtr<AAnomaly> AnomalyToLoad);
// .cpp
ULoadAnomaly* ULoadAnomaly::LoadAnomalyAsync(const UObject* WorldContextObject, TSoftClassPtr<AAnomaly> AnomalyToLoad)
{
	ULoadAnomaly* BlueprintNode = NewObject<ULoadAnomaly>();
	BlueprintNode->WorldContextObject = WorldContextObject;
	BlueprintNode->AnomalyToLoad = AnomalyToLoad;

	return BlueprintNode;
}

Note! that you can have how many arguments you want in this function (WorldContext is must have) and this is place where you can provide any parameters for our task in blueprint.

Note! task nodes can take a long time to finish and sometimes you want to prolong their life time (they are usually bind to blueprint context, usually lifetime of blueprint it is created in). In case you want task node to life longer then it’s context you can pin it to game instance by adding this line to static function (before return).

BlueprintNode->RegisterWithGameInstance(WorldContextObject);

This will bind this node life time to GameInstance lifetime(whole play session time), if you get to the point that you want to destroy node, and release it’s resources, before it’s planed life time, you can call

SetReadyToDestroy()

it will mark it for destroy and remove in next garbage collection cycle.

Create dynamic, multicast delegate without parameters, one for each exec node
#

We will simply create 3 delegates:

// .h
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FLoadAnomalyOutputPin);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FStartAnomalyOutputPin);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FEndAnomalyOutputPin);

and then inside of class we create those delegates fields:

// .h
class ANOMALIES_API ULoadAnomaly : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
public:
//...
	UPROPERTY(BlueprintAssignable)
	FLoadAnomalyOutputPin AnomalyLoaded;
	UPROPERTY(BlueprintAssignable)
	FStartAnomalyOutputPin StartAnomaly;
	UPROPERTY(BlueprintAssignable)
	FEndAnomalyOutputPin EndAnomaly;
}

As we later need to bind UFUNCTIONs we are creating simple delegate functions:

// .h
	class ANOMALIES_API ULoadAnomaly : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
public:
//...
	UFUNCTION()
	void OnAnomalyStarted();
	UFUNCTION()
	void OnAnomalyEnded();
}
void ULoadAnomaly::OnAnomalyStarted()
{
	StartAnomaly.Broadcast();
}

void ULoadAnomaly::OnAnomalyEnded()
{
	EndAnomaly.Broadcast();
}

Those delegates are used as output exec pins of our task node.

Override Activate function
#

// .h
	virtual void Activate() override;

Firstly we will ask AssetManager to load our soft class that we provide when we created this blueprint node.

// .cpp
	if(UAssetManager* AssetManager = UAssetManager::GetIfInitialized())
	{
		...
		LoadingObject = AssetManager->LoadAssetList({AnomalyToLoad.ToSoftObjectPath()}, Delegate);
	}

Then we will create delegate that is called when AssetManager finishes loading our class into memory. I will bind it into Lambda that have life time connected to this task node. Inside lambda we are just binding another delegates of Anomaly actors to task functions that will simply call our exec nodes. At the end of this lambda we call AnomalyLoaded.Broadcast() that is our Exec node.

// .cpp

	FStreamableDelegate Delegate{};
	Delegate.BindWeakLambda(this, [this]()
	{
		if(LoadingObject && LoadingObject->HasLoadCompleted())
		{
			LoadedAnomaly = Cast<AAnomaly>(WorldContextObject->GetWorld()->SpawnActor(AnomalyToLoad.Get()));
			if(LoadedAnomaly != nullptr)
			{
				LoadedAnomaly->OnAnomalyStarted.AddUniqueDynamic(this, &ULoadAnomaly::OnAnomalyStarted);
				LoadedAnomaly->OnAnomalyEnded.AddUniqueDynamic(this, &ULoadAnomaly::OnAnomalyEnded);
			}
		}
		AnomalyLoaded.Broadcast();
	});

Finished Activate function in .cpp file looks like this:

// .cpp
void ULoadAnomaly::Activate()
{
	if(UAssetManager* AssetManager = UAssetManager::GetIfInitialized())
	{
		FStreamableDelegate Delegate{};
		Delegate.BindWeakLambda(this, [this]()
		{
			if(LoadingObject && LoadingObject->HasLoadCompleted())
			{
				LoadedAnomaly = Cast<AAnomaly>(WorldContextObject->GetWorld()->SpawnActor(AnomalyToLoad.Get()));
				if(LoadedAnomaly != nullptr)
				{
					LoadedAnomaly->OnAnomalyStarted.AddUniqueDynamic(this, &ULoadAnomaly::OnAnomalyStarted);
					LoadedAnomaly->OnAnomalyEnded.AddUniqueDynamic(this, &ULoadAnomaly::OnAnomalyEnded);
				}
			}
			AnomalyLoaded.Broadcast();
		});
		LoadingObject = AssetManager->LoadAssetList({AnomalyToLoad.ToSoftObjectPath()}, Delegate);
	}
}

Sources
#

End
#

This is just short post, that I will need into future so I can share with you all.