Kazimieras Mikelis' Game Devlog

Designing Blueprint Function Nodes in C++

Learn to write Blueprint nodes for Unreal Engine in C++, and master the tools we have to communicate their intent for the visual programmer.

Mikelis' Game Devlog

Blueprint visual scripting is very popular among Unreal artists, designers, and rapid-prototyping programmers. And it is frequently the C++ developer’s job to expose parts of their programming with Blueprint-callable functions so that visual programmers can access them.

In this Devlog post, we will talk about the different designs our custom-built Blueprint function nodes might have, and how we can communicate their functionality through their form. I hope this post will be a useful high-level overview of how C++ and Blueprint can intermesh in Unreal Engine. Equally for those attempting it for the first time in their projects, and for those debugging their games.

If you already know how to expose functions to Blueprints, you may skip straight to “Conveying Functionality through Node Design,” but if you’d like to brush up on exposing C++ functions to Blueprint graphs and node contexts, please read ahead.

Exposing C++ Functions to Blueprint Graphs

To start with the basics, we should first learn how to expose our C++ functions as Blueprint function nodes. But even before we do that, we should briefly look into the idea of Blueprint node contexts.

Blueprint Node Contexts

If you have ever used a Blueprint graph in Unreal Engine, you will know that whenever you drag a line off a pin in an existing node, the Unreal Engine Editor will offer you a searchable list of nodes to be added. Those nodes are suggested based on the context of the current class and the class of the pin you are drawing from.

Mikelis' Game Devlog

The context, in the case of drawing from an execution pin, is the Blueprint class itself. And because this object’s Blueprint class derived from AActor, it will show all Blueprint functions inherited from the AActor class, any functions from the classes that AActor inherited, and miscellaneous actions that are just a part of the Blueprint system.

Once we pick a function or an action from the list, a node will be added to our graph.

Mikelis' Game Devlog

We can choose to ignore the context filter by unchecking “Context Sensitive” in the action list. Then we will be able to pick from a list of all available Blueprint nodes. But generally, this context filter is here to help us. It’s here to suggest that we choose the right functions and actions and not others.

The filter provides these suggestions by using primarily two contexts: the context of our current Blueprint class and the type of pin from which we are drawing the line. Then it adds some suggested actions for Blueprints in general.

In summation, whenever we wish to add a node to our Blueprint graphs, the Editor will suggest some nodes and hide others using the context filter to help us choose the compatible nodes. When we design new function nodes using C++, it’s fundamental to make sure that they appear in the right contexts.

Creating a new Blueprint class which inherits from a C++ class

With this intro to contexts out of the way, let’s expose our first C++ function as a Blueprint function node, and then look into how we can set the context from which it is accessible.

For brevity, I will assume that everyone reading this already knows how to create a new C++ class in an Unreal Engine project. Let’s create a new object class inheriting from AActor, add a few default components (like a static mesh) and bring an instance of this actor class into our level.

Afterward, let’s make a Blueprint class, inheriting from our C++ class. We can do so by selecting our actor in the level, clicking “Blueprint/Add Script” in the object details panel, and picking “New Subclass” as the creation method. As the description says, this will replace our actor with an instance of a Blueprint class that inherits from the parent class — a C++ class in this case.

Mikelis' Game Devlog

As soon as that is done, the Blueprint editor window will open.

Mikelis' Game Devlog

We can now switch to the Event Graph, which is the node graph that will execute when various events happen to our Blueprint object.

Mikelis' Game Devlog

This event graph’s context is the Blueprint class context, which now inherits functions from the C++ class of our actor. Therefore, we can access any Blueprint-exposed C++ functions from that parent C++ class as nodes on this graph.

Exposing a C++ Function

We can expose a C++ function for use with Blueprints by marking its declaration with a UFUNCTION() macro and one of the specifiers dedicated to exposing functions to Blueprint, like BlueprintCallable:

UFUNCTION(BlueprintCallable)
void RandomizeActorSize(float MinimumScale, float MaximumScale) const;

in the actor class

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ActorWithCustomNodes.generated.h"
UCLASS()
class BLOG_API AActorWithCustomNodes : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor's properties
    AActorWithCustomNodes(const FObjectInitializer& ObjectInitializer);
protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    UPROPERTY(EditAnywhere)
    class UStaticMeshComponent* StaticMesh;
    UFUNCTION(BlueprintCallable)
    void RandomizeActorSize(float MinimumScale, float MaximumScale) const;
public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;
};

BlueprintCallable, as the name implies, is a function specifier that says “this function is callable from Blueprints”.

Of course, we also need a function definition. Something like this is a great starting point:

void AActorWithCustomNodes::RandomizeActorSize(const float MinimumScale, const float MaximumScale) const
{
    const float Scale = MinimumScale + FGenericPlatformMath::FRand() * (MaximumScale - MinimumScale);
    StaticMesh->SetWorldScale3D(FVector(Scale, Scale, Scale));
}

As soon as we compile our code and hot-load our C++ class, we will find that our Blueprint class now has access to a Randomize Actor Size function node within its context. Nice!

Mikelis' Game Devlog

Mikelis' Game Devlog

We can set it up like in the screenshot above. And once the game starts, our actor will have a random size, between 1x and 5x its original.

Mikelis' Game Devlog

Congratulations on exposing a C++ function to Blueprint! Now every Blueprint graph will have access to this function, and it will be suggested in the action list if the list’s context is this Blueprint class, the base C++ class, or any class derived from those two.

Making our Function Available in Other Contexts

A significant takeaway from the above section is that our exposed function will not, by default, appear in the action list if the context of the Blueprint class is not inherited from this C++ class. That might be fine for our function, but what if we want to have a generic function node that should appear everywhere? Perhaps we want to build a common function library to be shared among many objects, or maybe we want every actor in the game to be scalable this way, not just actors derived from our C++ base class. Let’s explore that second option.

If we wish our random size function to be available in every Blueprint action context of our game, we can mark it as static. As you know, static C++ functions are not attached to their object instances, so we will need to convert our function to take our custom object as a pointer parameter. For convenience and a more practical example, let’s make the function work with any AActor object, although instead of AActor, we could use our object class, of course.

This is our new declaration:

UFUNCTION(BlueprintCallable)
static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale);

and our new definition:

void AActorWithCustomNodes::RandomizeActorSize(AActor* Target, const float MinimumScale, const float MaximumScale)
{
    // If the target is not valid, stop.
    if(!IsValid(Target)) return;
    // Get the target's mesh components.
    TArray<UStaticMeshComponent*> MeshPointers;
    Target->GetComponents<UStaticMeshComponent>(MeshPointers, true);
    // If the target actor has no mesh components, stop.
    if(!MeshPointers.Num()) return;
    // Get the first mesh component that is initialized and not marked for kill.
    UStaticMeshComponent* ValidMeshComponent = nullptr;
    for(int32 i = 0; i < MeshPointers.Num(); i++)
        if(IsValid(MeshPointers[i]))
        {
            ValidMeshComponent = MeshPointers[i];
            break;
        }
    // If there isn't one, stop.
    if(!ValidMeshComponent) return;
    // Set the size of the mesh component.
    const float Scale = MinimumScale + FGenericPlatformMath::FRand() * (MaximumScale - MinimumScale);
    ValidMeshComponent->SetWorldScale3D(FVector(Scale, Scale, Scale));
}

The new definition is much more involved. It attempts to find the first valid static mesh component of a given actor and will then try to scale it. We have to do this because, unlike our custom C++ class, the other C++ AActor-derived classes might not store pointers to their static meshes in a convenient way, or they might even not have static meshes at all. As you can see, our function may fail gracefully at many steps, and so far, we have no way to inform the visual programmer that it did so. More on doing that later in the post.

After compiling this code, our node will no longer have the Target pin pre-filled, but it will take any AActor-derived class — not just the specific class we wrote our function in. In any Blueprint graph, we can still use the Self node to get a reference to the object for which the graph at hand is being made. Or we could even use the UFUNCTION specifier meta=(DefaultToSelf=Target) to automatically populate that pin with "self".

Mikelis' Game Devlog

What is more, our Blueprint function node will now appear in practically every context. So our visual programmers can now expect to find it in all sorts of places, like the level Blueprint, for example. That is what the static keyword gives us, regardless of if the function takes any actor or just an actor of a specific class.

Mikelis' Game Devlog

Woah, now the node is available (almost) everywhere and works on any actor, even the ones we haven’t made!

Whether we want our exposed function to appear in all contexts or not is a matter of design. Is this a function to be used in all AActors or even all UObjects? If not, it may be more helpful for the visual designer if the C++ function is available only in the narrow context of the class it was declared in, and any classes derived from it. Otherwise, it should be a static function accessible from everywhere in Blueprint, just as it would be available without a reference to a particular object in C++.

By the way, if you want your static const functions to remain blue callable nodes with execution pins (more on why you could want that in the next chapter), make sure to use the UFUNCTION() specifier BlueprintPure=false. Otherwise the static function will always be presumed blueprint pure, even if you use the specifier BlueprintCallable rather than BlueprintPure.

Conveying Functionality through Node Design

Now that we finally know how to expose C++ functions to Blueprint function nodes let’s consider how we can make the visual design of these nodes convey their functionality to the visual programmer.

Function Specifiers

You may have noticed that we have used the specifier BlueprintCallable in our UFUNCTION() macro above. Why did we do that?

To start answering this question, let’s have a look at the Function Specifiers’ documentation. As we can see, not all UFUNCTION()-marked functions will be available as Blueprint function nodes. Actually, marking functions with UFUNCTION() only exposes them to the Unreal Engine’s reflection system. How the engine uses our functions then is entirely up to it, but we can give it some direction with specifiers. And what’s more important, through these directions we can shape how our function nodes look, and what they tell their users — visual programmers.

In this post, we will only focus on two of the specifiers that start with “Blueprint”, as they are the most commonly used specifiers for exposing C++ functions as nodes.

BlueprintCallable vs. BlueprintPure

The most common specifiers you will use and see in exposing C++ functions for Unreal Engine’s Blueprints are BlueprintCallable and BlueprintPure. As seen above, BlueprintCallable functions will have an input execution node and an output execution node. To the visual programmer, this implies that they must be executed sequentially - at a specific time - with something else: something happens before, and something may happen after. Moreover, BlueprintCallable functions can have output execution pins that branch into two or more, depending on what happens while the function is executing. There is another implication in practice — the “blue nodes” are used for functions changing their target object.

In contrast, BlueprintPure functions are mostly used as getters, accessors, and otherwise const functions. They convey to the visual programmer that the object they refer to will not be changed, just as if the function was pure in the functional programming sense. Their function nodes can have input and output pins, but they have no execution pins either in or out. BlueprintPure functions are executed when a different node requires their output, and the same node can be executed in a lot of different execution branches, at different times:

Mikelis' Game Devlog

In this example, the green nodes (that’s how BlueprintPure nodes look) Get Inventory Resource Count and To String will be executed at a time when the Sentiment variable’s value is being set. Moreover, as you can see, we can have different execution branches drawing values from the same BlueprintPure nodes, saving a lot of space on the graph. In this graph, when and which of the pure nodes execute is entirely determined by the nodes that want their output.

Generally, one may be guided by this principle when deciding whether to use BlueprintCallable or BlueprintPure: if you can answer yes to any of the questions, use BlueprintCallable, otherwise — use BlueprintPure:

  1. Does this function have a target object which it will change?
  2. Should we signify the importance of this function being executed once per node and in a particular execution branch?
  3. Does this function need to have execution pins, such as output execution pins? (more on execution nodes branching later in the post)
  4. Does this function have multiple output pins?
  5. Does this function do a much more complex (and slower) operation than a simple getter?

We can also tell the engine to use our functions in more exotic ways with Blueprints. For example, BlueprintNativeEvent specifier will allow our function to have a default definition in C++ but also be overridable with a Blueprint-defined function; BlueprintImplementableEvent will allow this function to be implemented in Blueprint classes deriving from our C++ base class; CallInEditor will let us write functions that can be called in the Editor, through the object details panel, even when the game is not being played. If you are building an online game, BlueprintCosmetic will come in handy as it will declare that the function isn’t to be called on a dedicated server, whether it is actually cosmetic or not.

Finally, I will let you in on a secret — at the risk of angering a few functional programmers, BlueprintPure functions do not need to be const or static. They can actually modify their objects. It’s useful for debugging, but it’s inadvisable to have them change their objects in other scenarios because that’s not the meaning their green nodes convey. That would be like making a const function change its object.

Even with all these options, C++ programmers for Unreal Engine find themselves almost always using BlueprintCallable or BlueprintPure.

Input Pins and Output Pins

A simple C++ function typically only returns one value, but we can pass variables in by reference as arguments, and set them inside our function. In effect, this is often used to “return” as many things from a function as we would like.

Blueprint function nodes take it to the next level. Any C++ function arguments you have declared to be passed in by reference will be used as output pins in a Blueprint function node by default. This way, we can name output pins and have multiple of them.

For example, if we would like our Randomize Actor Size function to return the new bounding box size, this approach allows us to do it.

Let’s look at what this declaration:

UFUNCTION(BlueprintCallable) static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX, float& NewSizeY, float& NewSizeZ);

and this definition:

void AActorWithCustomNodes::RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
    float& NewSizeY, float& NewSizeZ)
{
    // If the target is not valid, stop.
    if(!IsValid(Target)) return;
    // Get the target's mesh components.
    TArray<UStaticMeshComponent *> MeshPointers;
    Target->GetComponents<UStaticMeshComponent>(MeshPointers, true);
    // If the target actor has no mesh components, stop.
    if(!MeshPointers.Num()) return;
    // Get the first mesh component that is initialized and not marked for kill.
    UStaticMeshComponent* ValidMeshComponent = nullptr;
    for(int32 i = 0; i < MeshPointers.Num(); i++)
        if(IsValid(MeshPointers[i]))
        {
            ValidMeshComponent = MeshPointers[i];
            break;
        }
    // If there isn't one, stop.
    if(!ValidMeshComponent) return;
    // Set the size of the mesh component.
    const float Scale = MinimumScale + FGenericPlatformMath::FRand() * (MaximumScale - MinimumScale);
    ValidMeshComponent->SetWorldScale3D(FVector(Scale, Scale, Scale));
    // Get the new size
    const FVector Bounds = ValidMeshComponent->GetStaticMesh()->GetBoundingBox().GetSize() * Scale;
    NewSizeX = Bounds.X;
    NewSizeY = Bounds.Y;
    NewSizeZ = Bounds.Z;
}

yields:

Mikelis' Game Devlog

You might be reading this and thinking — what about genuinely passing arguments in by reference? That is possible, too. If you’d like to pass an argument in by reference, you should add UPARAM(ref) right before that argument’s declaration in the function declaration, like this:

UFUNCTION(BlueprintCallable)
static void RandomizeActorSize(AActor* Target, UPARAM(ref) const float& ParticularScale, float& NewSizeX,
        float& NewSizeY, float& NewSizeZ);

It would result in this reference argument being treated as an input pin, rather than output.

Mikelis' Game Devlog

I suppose the function name doesn’t make sense if we give it an exact particular scale, but you get the point. We’ll undo these changes to the function before proceeding : )

Overall, the benefits of named pins compared to a returned array or just one unnamed return value are many. Firstly, the visual programmer will immediately see what the output pins really output — not only what they should be used for, but also the type of variable the outputs are, as the pins will change color depending on it. Secondly, they will not have to parse an array. Thirdly, we even managed to get rid of one memory copy operation on return from a C++ function, which can be time-consuming.

Exec Output Pins

Sometimes our C++ function may be given a task that may be impossible to carry out in certain circumstances, just like our Randomize Actor Size function may fail to carry out its work if a non-valid actor is given to it, or the actor has no static meshes. If we anticipate this, in the C++ world, we may design our function to return a boolean — true or false — depending on whether it could complete its task. Or sometimes, we could return an error code, and returning 0 instead would mean success.

We can do the same in Blueprint function nodes, but when the nodes already output a bunch of data, it’s not uncommon for visual programmers to simply hook up the other return pins and forgo the logic of checking whether our boolean output is true and it’s safe to execute, or whether it’s false and the other pins are outputting garbage.

One way to emphasize whether our node could carry out the task it should is to have multiple execution output pins. This way, the execution line branches into two, and even the most sloppy visual programmer will generally only hook up the Success branch to the rest of their code. Even if Failure branch is hooked up to nothing and the end-user doesn’t see any hint that something failed, that’s still better than continuing in an unsafe state, with values in variables outside the design envelope.

Mikelis' Game Devlog

A practical example: sometimes a process would fail to start, and the script attempted to use its Process ID anyways, resulting in a crash. Now, nothing is hooked up to the failure branch, so the script only attempts to use Process ID down the Success execution branch.

Let’s look at a less conceptual example and implement this design into our Randomize Actor Size function. After all, we have defined many conditions in which the function will fail.

To add output execution pins, we need to define an enumerated list of all outcomes and pass it as a reference to our function.

At the top of the header file, we will define the new enumerated list:

UENUM(BlueprintType)
enum EOutcomePins
{
    Failure,
    Success
};

and we will change our function declaration to this:

UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome"))
    static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
        float& NewSizeY, float& NewSizeZ, TEnumAsByte<EOutcomePins>& Outcome);

Now, let’s update the definition of our function to set the value of Outcome depending on when we return from the function.

void AActorWithCustomNodes::RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
    float& NewSizeY, float& NewSizeZ,  TEnumAsByte<EOutcomePins>& Outcome)
{
    // By default, set the outcome to failure, and only change if everything else executed successfully
    Outcome = EOutcomePins::Failure;

    // If the target is not valid, stop.
    if(!IsValid(Target)) return;
    // Get the target's mesh components.
    TArray<UStaticMeshComponent *> MeshPointers;
    Target->GetComponents<UStaticMeshComponent>(MeshPointers, true);
    // If the target actor has no mesh components, stop.
    if(!MeshPointers.Num()) return;
    // Get the first mesh component that is initialized and not marked for kill.
    UStaticMeshComponent* ValidMeshComponent = nullptr;
    for(int32 i = 0; i < MeshPointers.Num(); i++)
        if(IsValid(MeshPointers[i]))
        {
            ValidMeshComponent = MeshPointers[i];
            break;
        }
    // If there isn't one, stop.
    if(!ValidMeshComponent) return;
    // Set the size of the mesh component.
    const float Scale = MinimumScale + FGenericPlatformMath::FRand() * (MaximumScale - MinimumScale);
    ValidMeshComponent->SetWorldScale3D(FVector(Scale, Scale, Scale));
    // Get the new size
    const FVector Bounds = ValidMeshComponent->GetStaticMesh()->GetBoundingBox().GetSize() * Scale;
    NewSizeX = Bounds.X;
    NewSizeY = Bounds.Y;
    NewSizeZ = Bounds.Z;
    // Everything has finished executing, let's set Outcome to success.
    Outcome = EOutcomePins::Success;
}

If we were to compile this and take a look at our Blueprint node, it would now have two execution output pins, and fire from the one to which the Outcome variable was set when the C++ function returned.

Mikelis' Game Devlog

You may even have three, four, or more output execution pins if you wish to differentiate the different states in which the C++ function returned. For example, you could use multiple pins for AI, where the state of an actor could determine what execution branch should play out next. But let’s not digress for now.

Having a node design like this signals obviously to the visual programmer that there are two distinct outcomes — success and failure, and that they need to be handled separately. Even if the visual programmer doesn’t hook up the failure execution pin, that still ensures that the output data will only be used if the node carried out its operation successfully.

Passing Arguments in by Reference vs. by Copy

Like C++ functions, Blueprint function nodes can receive their inputs as references to data, or copies of the data itself. As C++ programmers, we should generally use const references when we can with large data types for performance reasons. But in Blueprint, this choice has node design implications as well.

In our Randomize Actor Size function, we take Minimum Scale and Maximum Scale inputs as copies of float while Target is a pointer.

static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float & NewSizeX,
float& NewSizeY, float& NewSizeZ, TEnumAsByte<EOutcomePins>& Outcome);

Mikelis' Game Devlog

If we pass these float arguments by copy, as shown above, the visual programmer will see an input box on the node like to the ones next to Minimum Scale and Maximum Scale input nodes. They will also have the option to pass the value by copy into the pins. Nevertheless, there is a suggestion here to hard-code values.

But if we pass the arguments to the function by reference, the input pins will no longer have the input box, hinting strongly to the visual programmer that they should hook something else here, instead of hard-coding values.

If you have been programming for Unreal Engine in C++ for a while, you may also like to pass some things by a pointer. With Blueprint function nodes, you can only pass other UObjects by a pointer. Attempting to use a pointer argument for anything else will upset the Unreal Header Tool and it will fail compilation.

All in all, passing inputs by copy or by reference in Blueprint function nodes is a consideration for design and functionality rather than performance. Depending on which we choose, the visual programmer will either see or not see an input box for values on the node. The input box could encourage hard-coding values.

P.S. One important and advanced note before we wrap up this section - everything passed into the Blueprint VM from raw C++ will be passed by copy. This means that if a raw C+±defined function takes in an argument by reference and modifies it, the data that's being referenced will change. But if a function implemented in Blueprint (for example, a BlueprintImplementableEvent) takes in a value by reference when called from C++ and modifies the value in a Blueprint graph, the raw C++ data that's referenced will not be modified. Passing data by reference between Blueprint functions works as expected, though - the original data will be modified if something is done to the reference. To sum up - avoid passing data by non-const reference to BlueprintNativeEvents and BlueprintCallable functions when calling them from C++, or know that these functions might fail to modify the raw data you pass in by reference even if they might attempt to do so in their Blueprint-side definitions.

Advanced Pin Tips

Now that we've learned the basics, let's look at some advanced tricks:

  1. We can specify the name that each input pin and output pin will have using UPARAM(DisplayName="Name"):

    // Name the output pin "Succeeded".
    UFUNCTION(BlueprintCallable)
    UPARAM(DisplayName="Succeeded") bool PerformOperation(bool ArgumentOne, int32 ArgumentTwo);
    
  2. We can hide some function pins by default but allow the programmers to see them if they expand the function node:

    UFUNCTION(BlueprintCallable, meta=(AdvancedDisplay="AdvancedArgument"))
    void PerformOperation(bool Argument, int32 AdvancedArgument = 0);
    
  3. When using reference arguments, if they are const, then they will be shown as an input pin, and if they are not, they will be shown as an output pin:

    UFUNCTION(BlueprintCallable)
    void PerformOperation(const bool& Argument /*This is an input pin.*/, bool& Success /*This is an output pin.*/);
    
  4. However, even non-const references could be made into an input pin:

    UFUNCTION(BlueprintCallable)
    void PerformOperation(UPARAM(Ref) bool& ArgumentOne /*This is an input pin.*/, bool& Success /*This is an output pin.*/);
    
  5. Unreal Header Tool (at the time of writing) does not allow default values for container arguments, so the following will not compile:

    UFUNCTION(BlueprintCallable)
    void PerformOperation(TArray<bool> Argument = {});
    

    However, what if we really want a container to be optional? We can use the AutoCreateRefTerm specifier to automatically populate any referenced input argument with the default-initialized value:

    // Here, the Argument input pin will be optional and effectively have the same default value as in the non-compiling code snipped above.
    UFUNCTION(BlueprintCallable, meta=(AutoCreateRefTerm="Argument"))
    void PerformOperation(const TArray<bool>& Argument);
    

Tool-tips

Sometimes we want to document our Blueprint function nodes more, and what better way than tool-tips? When the visual programmer hovers the cursor over our node, a message will appear. We can define what it says in the meta specifier like this:

UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", ToolTip = "Try to ranomize the size of actor's static mesh component.\n\nNewSizeX, Y, Z - bounding box for the new actor's scale."))
    static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
        float& NewSizeY, float& NewSizeZ, TEnumAsByte<EOutcomePins>& Outcome);

Mikelis' Game Devlog

Tool-tips allow us to convey a large amount of meaningful information to the visual programmer about the function. It’s especially a life-saver for functions with not-so-descriptive names that we encounter every now and then.

Javadoc-style Tool-tips

However, I will concede that the UFUNTION() decorator is becoming a little bit overwhelming to read. Luckily, Unreal Engine’s reflection system also imports function descriptions in the Javadoc/Doxygen format, same as many C++ IDEs.

Here is some alternative code to achieve the same result as above:

/**
    *	Try to ranomize the size of actor's static mesh component.
    * 
    * NewSizeX, Y, Z - bounding box for the new actor's scale.
    *
    * @param Target					The actor to scale.
    * @param MinimumScale				The minimum actor scale that can result from this function call.
    * @param MaximumScale				The maximum actor scale that can result from this function call.
    * @param NewSizeX				The X extent of a bounding box for this actor, after it has been scaled.
    * @param NewSizeY				The Y extent of a bounding box for this actor, after it has been scaled.
    * @param NewSizeZ				The Z extent of a bounding box for this actor, after it has been scaled.
    * @param Outcome				Whether the scale operation was a success or failure.
    */
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome"))
    static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
        float& NewSizeY, float& NewSizeZ, TEnumAsByte<EOutcomePins>& Outcome);

However, Javadoc-style also allows us to document function parameters. These will appear as tool-tips when a visual programmer hovers their mouse cursor on the pins. Now that’s good documentation.

Deprecation

Eventually, in developing our Blueprint function nodes, we will have to retire the old ones. Depending on how your team likes to work, they may be okay with us just removing the old C++ code for Blueprint function nodes and breaking them, forcing everyone to scramble and implement your new, better solution. But some teams will want their game to work all throughout development, and only carry out upgrades at set times. If you work in the latter kind of organization, then deprecation messages will be handy to you.

Whenever we wish to retire an old Blueprint node, we can mark it as deprecated in advance, so that our visual programmers can take their due time in removing it from their scripts, or replacing it with something else.

We can mark a C+±exposed Blueprint function as deprecated in its declaration. In particular, the UFUNCTION() macro:

UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", ToolTip = "Try to ranomize the size of actor's static mesh component.\n\nNewSizeX, Y, Z - bounding box for the new actor's scale.", DeprecatedFunction, DeprecationMessage="Please switch to the new and improved Set Actor Scale Neue node."))

Blueprint scripts that use this node will now throw a warning when they attempt to compile. Moreover, the visual programmer will not be able to add these function nodes to the Blueprint graph anymore except if they copy and paste old nodes.

Mikelis' Game Devlog

By marking our Blueprint-exposed functions as deprecated, we create a checklist of nodes to remove for visual programmers. Instead of just breaking these scripts by removing our C++ functions, we allow visual programmers time to adapt to the changes and switch to new nodes.

There is just one advanced caveat here - when the project is cooked (when Blueprint classes are being recompiled while cooking, usually before the project is packaged to a bunch of binaries), if the cook is configured to report warnings as errors, the deprecated function warnings will show up as an error in cooks. This could break some continuous integration processes.

Categories

We can categorize our exposed functions in the action list if we’d like to. To set the category of a function, we can add a Category specifier in the meta group of the UFUNCTION().

UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", Category="Actor", ToolTip = "Try to ranomize the size of actor's static mesh component.\n\nNewSizeX, Y, Z - bounding box for the new actor's scale.", DeprecatedFunction, DeprecationMessage="Please switch to the new and improved Set Actor Scale Neue node."))

It is also possible to create nested categories like this: Category=” Category|Subcategory|Subsubcategory|etc”.

However, know that all functions are by default categorized under their class name. So there is no need to add a class name manually. On the other hand, this allows for creative hacks like seemingly adding functions to other classes, especially useful for static functions dealing with structs that don’t support UFUNCTIONS() in Unreal.

Further Learning

If you would like to learn more about exposing your functions to Blueprint function nodes or even exposing C++ function to other parts of the engine, I suggest reading the Unreal Engine documentation on function specifiers, and trying them out over time.

If you’d like to better explore how Unreal Blueprints fit into the picture of programming games with Unreal Engine, there is probably no better source than the Blueprints vs. C++: How They Fit Together and Why You Should Use Both video by Alex Forsythe.

Concluding Notes

Developing well-designed Blueprint function nodes that are self-documenting will give the artists, designers, visual programmers on your team an intuitive understanding of the node functionality. You will see a significant productivity boost across the board when everyone else on your team is well-informed about the scripting tools they have. It will also save you debugging headaches if these tools are designed to promote good visual programming habits, like handling exceptions through separate execution branches.

Unreal Engine gives us a plethora of function specifiers and means to create Blueprint function nodes that convey their functionality and intent. I hope you find this Devlog article useful in developing excellent function nodes in the future.

Next week on Devlog, we will be diving into a much simpler topic of taking screenshots and using them as FColor arrays or textures at runtime. The week after that, we will talk about saving arbitrary data (like those screenshots) within Unreal Engine’s save game slots. If you have enjoyed this week’s Devlog post or are looking forward to the coming weeks’, feel free to say so in the comments.

Source Code

Here’s the final source code for the custom AActor-derived object class we have used for demonstration all along this overview:

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ActorWithCustomNodes.generated.h"
UENUM(BlueprintType)
enum EOutcomePins
{
    Failure,
    Success
};
UCLASS()
class BLOG_API AActorWithCustomNodes : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor's properties
    AActorWithCustomNodes(const FObjectInitializer& ObjectInitializer);
protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    UPROPERTY(EditAnywhere)
    class UStaticMeshComponent* StaticMesh;
        UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", Category="Actor|Mesh|Scale", ToolTip = "Try to ranomize the size of actor's static mesh component.\n\nNewSizeX, Y, Z - bounding box for the new actor's scale.", DeprecatedFunction, DeprecationMessage="Please switch to the new and improved Set Actor Scale Neue node."))
    static void RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
        float& NewSizeY, float& NewSizeZ, TEnumAsByte<EOutcomePins>& Outcome);
public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;
};
#include "ActorWithCustomNodes.h"
// Sets default values
AActorWithCustomNodes::AActorWithCustomNodes(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = false;
    StaticMesh = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("Static Mesh"));
    SetRootComponent(StaticMesh);
}
// Called when the game starts or when spawned
void AActorWithCustomNodes::BeginPlay()
{
    Super::BeginPlay();

}
void AActorWithCustomNodes::RandomizeActorSize(AActor* Target, float MinimumScale, float MaximumScale, float& NewSizeX,
    float& NewSizeY, float& NewSizeZ,  TEnumAsByte<EOutcomePins> & Outcome)
{
    // By default, set the outcome to failure, and only change if everything else executed successfully
    Outcome = EOutcomePins::Failure;

    // If the target is not valid, stop.
    if(!Target || !IsValid(Target)) return;
    // Get the target's mesh components.
    TArray<UStaticMeshComponent *> MeshPointers;
    Target->GetComponents<UStaticMeshComponent>(MeshPointers, true);
    // If the target actor has no mesh components, stop.
    if(!MeshPointers.Num()) return;
    // Get the first mesh component that is initialized and not marked for kill.
    UStaticMeshComponent * ValidMeshComponent = nullptr;
    for(int32 i = 0; i < MeshPointers.Num(); i++)
        if(IsValid(MeshPointers[i]))
        {
            ValidMeshComponent = MeshPointers[i];
            break;
        }
    // If there isn't one, stop.
    if(!ValidMeshComponent) return;
    // Set the size of the mesh component.
    const float Scale = MinimumScale + FGenericPlatformMath::FRand() * (MaximumScale - MinimumScale);
    ValidMeshComponent->SetWorldScale3D(FVector(Scale, Scale, Scale));
    // Get the new size
    const FVector Bounds = ValidMeshComponent->GetStaticMesh()->GetBoundingBox().GetSize() * Scale;
    NewSizeX = Bounds.X;
    NewSizeY = Bounds.Y;
    NewSizeZ = Bounds.Z;
    // Everything has finished executing, let's set Outcome to success.
    Outcome = EOutcomePins::Success;
}
// Ticks are disabled, but I wanted to leave the source code close to auto-generated so that you could get your bearings : )
void AActorWithCustomNodes::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

#Engineering #Unreal Engine #Cpp #Game Development #Blueprint #Nodes #Longform

- 2 toasts