Viktor Pramberg Game Programmer

Neat Functions

October 14, 2023

A plugin for Unreal Engine that contains a collection of useful CallFunction nodes for cases where you want more functionality, such as exposing delegate pins inline on the node, or dynamic object creation nodes with ExposeOnSpawn pin support.

A collection of possible nodes that this plugin can make

This was a small side project I did to allow me to make better blueprint nodes more easily. These solve things I run into semi-frequently when writing systems in Unreal, that don’t warrant spending time to actually solve right then and there, but that itch my desire to make nicer workflows nonetheless. The project is available on GitHub.

Currently it has two types of nodes: inline delegate nodes (similar to async actions but simpler) and constructor nodes (like a generic Spawn Actor node). Let’s talk a bit about them.

Neat Delegate Function

Typically when you want to expose an event to blueprint, you’d use a dynamic multicast delegate property that allows BP to see and bind to the delegate.

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMemberDelegate);
UPROPERTY(BlueprintAssignable)
FMemberDelegate MyDelegate;
UFUNCTION()
void MyFunc()
{
UE_LOG(LogTemp, Display, TEXT("Hello!"));
}
void BindToMyDelegate()
{
MyDelegate.AddDynamic(this, &ThisClass::MyFunc);
}

I think it’s an awkward solution because it limits options in C++ (you can only bind UFUNCTIONs using MyDelegate.AddDynamic(this, &ThisClass::MyFunc)). You can see different examples in the engine of ways to deal with this:

  1. Just use AddDynamic and deal with its limitations. This is what a lot of AActor delegates do. This is a perfectly fine solution in cases where you don’t often interact with the delegate in C++.
  2. Have one dynamic and one non-dynamic delegate and call both whenever the event should trigger. This can be seen in UserWidget.h for example, in the OnVisibilityChanged and OnNativeVisibilityChanged delegates.
  3. Only use a non-dynamic delegate. This is my preferred solution in many cases. Obviously this used all over the place. These are often not exposed to blueprint, either because they aren’t needed, or because it requires some more work to do. When they are required in blueprint, it typically happens using async action nodes (although I will show a different method). See the Gameplay Ability System for examples of this.

Async actions?

Async actions are UObject-derived types that typically share the lifetime of the game instance, although it is up to each individual action to determine that. Some destroy themselves after firing once, while some live forever. They are supposed to encapsulate some asynchronous behavior, like downloading data from some server, listening for property changes, or whatever else you may think of. In cases like downloading data, the use of an object that can contain state may be warranted. But in cases where you just want to register a callback, async actions are overkill. They are very verbose. You have to create a new class for every single behavior, and every time you want to use it, you need to spawn a new instance of it. This is inefficient not only for the computer, but also for the developer.

Dynamic unicast delegates & non-dynamic multicast delegates

In order to avoid async actions, I like to use non-dynamic delegates, then write a UFUNCTION that takes a dynamic unicast delegate as parameter to bind to the multicast delegate. This way you get the best of both worlds: you can interact with the delegate in C++ however you want, and at the same time have it accessible in blueprint!

DECLARE_DYNAMIC_DELEGATE(FDynamicDelegate);
DECLARE_MULTICAST_DELEGATE(FMemberDelegate);
// The multicast delegate as a member of some object. Notice that it's not a `UPROPERTY` because it is not required, that is not how we bind to it.
FMemberDelegate MyDelegate;
UFUNCTION(BlueprintCallable)
void BindToMyDelegate(FDynamicDelegate InDelegate)
{
// This is essentially the same as what happens in AddDynamic(), but it allows us to do MyDelegate.AddLambda().
MyDelegate.AddUFunction(InDelegate.GetUObject(), InDelegate.GetFunctionName());
}
// Since delegate handles aren't blueprintable, we either have to do this to unbind, or create our own delegate handle wrapper.
// This should be good enough in most cases.
UFUNCTION(BlueprintCallable)
void UnbindFromMyDelegate(UObject* Object)
{
MyDelegate.RemoveAll(Object);
}

Adding the NeatDelegateFunction metadata

In many cases, that solution is enough. But I do enjoy how clean async actions look in the graph, which is why I created the NeatDelegateFunction meta tag. Adding that tag will create the appropriate event inline on the node itself, which makes it slightly more convenient to use.

UFUNCTION(BlueprintCallable, meta = (NeatDelegateFunction))
void BindToMyDelegate(FDynamicDelegate InDelegate)
{
// This is essentially the same as what happens in AddDynamic(), but it allows us to do MyDelegate.AddLambda().
MyDelegate.AddUFunction(InDelegate.GetUObject(), InDelegate.GetFunctionName());
}

Final result

A comparison between the two solutions above. Left node does not use the meta tag, while the right one does.

Neat Constructor

The NeatConstructor tag is useful in cases where you want to construct objects with a bit more flexibility. In short, it’s like a generic SpawnActor node. Let’s start out by examining how that works.

The SpawnActor node

Many nodes in the blueprint graph are just wrappers for other nodes. The SpawnActor node is one such node. When these nodes get compiled they are expanded into an underlying chain of nodes used to generate the actual blueprint bytecode. If you are familiar with blueprint macros, they can be seen as blueprint macros on steroids, where the programmer has full control over what happens when expanded. The SpawnActor node has three major stages when it gets expanded:

  1. Call an internal SpawnActor function that spawns the actor. Note that it has not called BeginPlay yet at this point, but the object itself has been constructed and can be referred to.
  2. Loop through all ExposeOnSpawn properties in the actor’s class, and spawn SetVariable nodes for each of them.
  3. Call the FinishSpawning function on the actor, which will call BeginPlay and ensure the actor has completed the spawn sequence.

Making this behavior generic

Looking at those three stages, we can derive what we need the generic node to do:

  1. Call a user specified function that spawns an object.
  2. Loop through all ExposeOnSpawn properties in the actor’s class, and spawn SetVariable nodes for each of them.
  3. [Optional] Call a user specified function that finishes spawning. For TSubclassOf<AActor>, default it to FinishSpawning.

Conceptually, it is that simple. But let’s think a bit at how a user might want to work with a concept like this:

// Step 1: Call "spawn" function.
UFUNCTION(BlueprintCallable, meta = (NeatConstructorFinish = "CustomCreateObjectFunctionWithFinish_Finish"))
static UObject* CustomCreateObjectFunctionWithFinish(TSubclassOf<UObject> Class)
{
// ...
}
// Step 2: Set ExposeOnSpawn properties
// Step 3: Call finish function
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = true))
static void CustomCreateObjectFunctionWithFinish_Finish(UObject* Object, float SomeExtraParameter)
{
// ...
}

This is essentially the way you interact with the NeatConstructor node.