Hit enter to search or ESC to close

Designing Blueprint Function Nodes in C++

Kazimieras

Blueprint visual scripting is very popular among Unreal artists and beginner 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 Blueprint scripting 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 actions to be performed. Those actions are suggested based on the context of the current class and the class of the pin you are drawing from.

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.

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 does its best to provide these suggestions by using primarily two contexts: the context of our current Blueprint class and the 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 are in the right context. This is especially true if we expose multiple functions with similar names, but for different object classes or use purposes.

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 it 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.

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

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

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!

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.

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 math 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 have to mark it as static. As you know, static C++ functions are not attached to their objects, so we will need to convert our function to take our custom class 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(!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));
}

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.

What is more, our Blueprint function node will now appear in 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.


Leave a Reply

Your email address will not be published.Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.