Ok, below is my final solution. It includes 3 new nodes:
AsyncForLoop: standard loop using an async task broadcasting each loop
AsyncDelayForLoop: inserts a delay between each loop
AsyncBurstForLoop: inserts a delay between X loops (e.g. every 3 index insert a delay)
AsyncForLoop.cpp
#include "AsyncLoops/Public/AsyncForLoop.h"
void UAsyncForLoop::Activate()
{
if ( Delay > 0.f ) {
DelayLoop();
return;
}
for ( int32 Index = FirstIndex; Index <= LastIndex; ++Index ) {
LoopBody.Broadcast( Index );
}
Completed.Broadcast( 0 );
SetReadyToDestroy();
}
void UAsyncForLoop::DelayLoop()
{
if ( ! IsValid( World ) ) {
Completed.Broadcast( 0 );
SetReadyToDestroy();
return;
}
World->GetTimerManager().ClearTimer( DelayTimerHandle );
LoopBody.Broadcast( CurrentIndex );
if ( CurrentIndex == LastIndex ) {
Completed.Broadcast( 0 );
SetReadyToDestroy();
return;
}
++CurrentIndex;
if ( Burst > 1 ) {
for ( int32 Index = FirstIndex; Index <= ( Burst - 1 ); ++Index ) {
LoopBody.Broadcast( CurrentIndex );
++CurrentIndex;
if ( CurrentIndex == LastIndex ) {
Completed.Broadcast( 0 );
SetReadyToDestroy();
return;
}
}
}
World->GetTimerManager().SetTimer( DelayTimerHandle, this, &UAsyncForLoop::DelayLoop, Delay, false, Delay );
}
UAsyncForLoop* UAsyncForLoop::AsyncForLoop(
int FirstIndex,
int LastIndex
) {
UAsyncForLoop* BlueprintNode = NewObject<UAsyncForLoop>();
BlueprintNode->FirstIndex = FirstIndex;
BlueprintNode->LastIndex = LastIndex;
return BlueprintNode;
}
UAsyncForLoop* UAsyncForLoop::AsyncDelayForLoop(
UObject* WorldContextObject,
int FirstIndex,
int LastIndex,
float Delay
) {
UAsyncForLoop* BlueprintNode = NewObject<UAsyncForLoop>();
if ( IsValid( WorldContextObject ) ) {
BlueprintNode->World = WorldContextObject->GetWorld();
}
BlueprintNode->FirstIndex = FirstIndex;
BlueprintNode->LastIndex = LastIndex;
BlueprintNode->Delay = Delay;
return BlueprintNode;
}
UAsyncForLoop* UAsyncForLoop::AsyncBurstForLoop(
UObject* WorldContextObject,
int FirstIndex,
int LastIndex,
float Delay,
int Burst
) {
UAsyncForLoop* BlueprintNode = NewObject<UAsyncForLoop>();
if ( IsValid( WorldContextObject ) ) {
BlueprintNode->World = WorldContextObject->GetWorld();
}
BlueprintNode->FirstIndex = FirstIndex;
BlueprintNode->LastIndex = LastIndex;
BlueprintNode->Delay = Delay;
BlueprintNode->Burst = Burst;
return BlueprintNode;
}
AsyncForLoop.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Delegates/IDelegateInstance.h"
#include "AsyncForLoop.generated.h"
UCLASS( Blueprintable )
class ASYNCLOOPS_API UAsyncForLoop : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOAsyncForLoopSignature, int, Index );
virtual void Activate() override;
UPROPERTY( BlueprintAssignable, Category = "AsyncLoops" )
FOAsyncForLoopSignature LoopBody;
UPROPERTY( BlueprintAssignable, Category = "AsyncLoops" )
FOAsyncForLoopSignature Completed;
UFUNCTION( BlueprintCallable, meta = ( BlueprintInternalUseOnly = "true" ), Category = "AsyncLoops", DisplayName = "Async For Loop" )
static UAsyncForLoop* AsyncForLoop(
int FirstIndex = 0,
int LastIndex = 0
);
UFUNCTION( BlueprintCallable, meta = ( BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject" ), Category = "AsyncLoops", DisplayName = "Async Delay For Loop" )
static UAsyncForLoop* AsyncDelayForLoop(
UObject* WorldContextObject,
int FirstIndex = 0,
int LastIndex = 0,
float Delay = 0.01f
);
UFUNCTION( BlueprintCallable, meta = ( BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject" ), Category = "AsyncLoops", DisplayName = "Async Burst For Loop" )
static UAsyncForLoop* AsyncBurstForLoop(
UObject* WorldContextObject,
int FirstIndex = 0,
int LastIndex = 0,
float Delay = 0.1f,
int Burst = 5
);
protected:
class UWorld* World = nullptr;
int FirstIndex = 0;
int LastIndex = 0;
int CurrentIndex = 0;
float Delay = 0.f;
int Burst = 0;
FTimerHandle DelayTimerHandle;
UFUNCTION()
void DelayLoop();
};
It’s probably not perfect and there’s probably better ways to code this, but for now works great.