Note
This plugin is Experimental, while being functionally complete there is room for usability improvements and new features.
This plugin is a proof-of-concept experiment to make blueprint async nodes "capture" state of variables prior to call in a friendly way.
If anyone finds this project interesting feel free to send feedback in Discord.
Due to nature of how "async" actions function in blueprint anyone working with blueprints encountered this problem:
Using value from caller will result in fail:
So how to make capturing work and be usable?
One of the solutions I saw was using a generic FInstancedStruct parameter that was used to pass capture context.
This method works but convenience is lacking.
Structs need to be prepared, 'Make/Break Struct' and 'Make/Get Instanced Struct' nodes.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FExampleResult,
FInstancedStruct, Captures,
int32, UserIndex, FString, UserName, const TArray<int32>&, Scores);
UCLASS(MinimalAPI)
class UExampleAction : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FExampleResult OnCompleted;
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly=true, WorldContext = "WorldContextObject"))
static UExampleAction* StartAction(const UObject* WorldContextObject, int32 UserIndex, FInstancedStruct Captures);
protected:
void OnResultReceived(FDataPack& Pack)
{
OnComplete.Broadcast(LocalCaptures, UserIndex, Pack.UserName, Pack.Scores);
}
UPROPERTY()
FInstancedStruct LocalCaptures;
};To provide best convenience when working in Blueprint Graph Editor plugin implements a custom UK2Node that matches behavior of engine UK2Node_AsyncAction and adding context-managing logic into chain.
To capture unrestricted amount of data while keeping type information FInstancedPropertyBag is a great tool, it supports almost any property type and container (even Maps support was added in 5.8).
To be able access proxy object from Custom Event one can't simply take the proxy pin (which can be exposed with ExposedAsyncProxy) as it will be overwritten on subsequent calls and point to most recent action.
Guaranteed access to proxy object is possible only if proxy passed as delegate parameter.
Custom graph node can be used with any UBlueprintAsyncAction class that satisfies following conditions:
- Delegate has a mandatory
const UObject*parameter that will be used to access proxy from generatedCustom Event - Proxy class has metadata
HasAsyncContextset to context parameter - Delegate invocation will pass
thisto context parameter
Optional:
- Proxy can have metadata
HasDedicatedAsyncNodeto prevent it from being used by engine node - Proxy can house own instance of container for context (see examples)
This how it looks in practice:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FExampleResult,
const UObject*, Context,
int32, UserIndex, FString, UserName, const TArray<int32>&, Scores);
UCLASS(MinimalAPI, meta=(HasAsyncContext=Context))
class UExampleAction : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FExampleResult OnCompleted;
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly=true, WorldContext = "WorldContextObject"))
static UExampleAction* StartAction(const UObject* WorldContextObject, int32 UserIndex);
protected:
void OnResultReceived(FDataPack& Pack)
{
OnComplete.Broadcast(this, UserIndex, Pack.UserName, Pack.Scores);
}
};In Blueprint Graph Editor node will look like this
At call site pin value will be stored into capture context, then read at delegate, and produce expected result.
With no limits to amount of data to capture
Standard Async Action Flow
- Call
ProxyFactory::ProxyFactoryFunctionto create newProxyClassinstance - For each multicast delegate in node:
- Create Custom Event
- Call Add Delegate and subscribe to event
- Call
ProxyClass::Activate - Continue Execution from Then Pin
- For each Custom Event:
- For each multicast delegate parameter:
- Assign value to a created local variable
- Continue execution from Event Pin
- For each multicast delegate parameter:
Custom Async Action Flow
Normal mode:
- Call
ProxyFactory::ProxyFactoryFunctionto create newProxyClassinstance - Call
CreateContextForObjectto create bound context for the proxy object - Call
SetupContextto configure context property bag (only for non-vararg mode) - For each capture parameter:
- Call
SetValueto capture value
- Call
- For each multicast delegate in node:
- Create Custom Event
- Call Add Delegate and subscribe to event
- Call
ProxyClass::Activate - Continue Execution from Then Pin
- For each Custom Event:
- Call
GetContextForObjectto acquire context for proxy object - For each capture parameter output:
- Call
GetValueto read captured value - Assign value to a created local variable
- Call
- For each multicast delegate parameter:
- Assign value to a created local variable
- Continue Execution from Event Pin
- Call
Variadic mode:
- Call
ProxyFactory::ProxyFactoryFunctionto create newProxyClassinstance - Call
CreateContextForObjectto create bound context for the proxy object - Call
SetValueVariadicto write captures values to storage (pin per captured property) - For each multicast delegate in node:
- Create Custom Event
- Call Add Delegate and subscribe to event
- Call
ProxyClass::Activate - Continue Execution from Then Pin
- For each Custom Event:
- Call
GetContextForObjectto acquire context for proxy object - Call
GetValueVariadicto read captured values from storage (pin per captured property)- Assign value to a created local variable
- For each multicast delegate parameter:
- Assign value to a created local variable
- Continue Execution from Event Pin
- Call
Custom Latent Action Flow
Normal mode:
- Call
CreateContextForLatentto create new context - Call
SetupContextto configure context property bag (only for non-vararg mode) - Create local variable for context handle that will be updated by latent action
- Write Properties one of:
- Call
SetValueVariadicto write captures values to storage (pin per captured property) - Call
SetValue[Type]to write captures values to storage (one call per captured property)
- Call
- Call latent action function
- Read Properties one of:
- Call
GetValueVariadicto read captures values from storage (pin per captured property) - Call
GetValue[Type]to read captures values from storage (one call per captured property)
- Call
- Call
DestroyContextto free used resources - Continue Execution
Event mode:
- Create a Custom Event node
- Call
CreateContextForLatentto create new context providing data about event - Call
SetupContextto configure context property bag (only for non-vararg mode) - Create local variable for context handle that will be updated by latent action
- Write Properties one of:
- Call
SetValueVariadicto write captures values to storage (pin per captured property) - Call
SetValue[Type]to write captures values to storage (one call per captured property)
- Call
- Call latent action function
- From Custom Event node:
- Read Properties one of:
- Call
GetValueVariadicto read captures values from storage (pin per captured property) - Call
GetValue[Type]to read captures values from storage (one call per captured property)
- Call
- Call
DestroyContextto free used resources - Continue Execution
Context created only if there is at least one capture pin connected.
- Write some tests
- Clean up debug logging and code
- Improve handling of malformed or changed nodes (there are lots of asserts for debug purposes right now)
- Maps support (has to wait for UE 5.8)
Plugin relies on Instanced Structs usage, requires at least UE 5.5
Plugin is available under the MIT license. See the LICENSE file for more info.





