For the past three weeks I have been, on and off, dipping my toes into the confusing world of Slate and K2Nodes. The goal was to simply learn more about the lower levels of the Engine, however, me being me, I needed a more concrete goal to work towards to maintain motivation and focus.
To that end, I decided that I would make two K2Nodes:
- A Node that takes in a UObject Reference, and an FName, then Gets and returns the Variable of the same name found inside the UObject (if it exists).
- A Node that takes in a UObject Reference, an FName, and a Wildcard, then Sets and returns the Variable of the same name found inside the UObject (if it exists).
Both would also return a “bSuccess” boolean for safety.
Spoiler alert, here are the two nodes in action:
This blog won’t take you step by step through the process of making these Nodes (though the full code will be provided at the end). Instead, it will attempt to dissect the more important aspects of the K2Nodes and shed some light on their overall structure and conventions. In the end, I’m hoping that my journey will also help you with your own adventures into K2Nodes.
So, the first main question:
What are K2Nodes?
Seems obvious, but it was a hard question to answer coming in blind, and there aren’t many resources online that explain it simply.
In short, a K2Node is a very fancy Blueprint Node that can be used to wrap more advanced and dynamic functionality than your average
UFUNCTION. For example, having the inputs and outputs of a Node change depending on other inputs and outputs, or having core functionality of the Node completely change depending on input types. One way to think of it is a Blueprint Node that has two or more functions inside of it; one that takes the inputs, passes it around through N functions, then another function that returns the output, like a subgraph in Blueprints.
You can find Engine K2Nodes in
Source/Editor/BlueprintGraph/Classes/ and I highly recommend reading through some time. Two good examples are
K2Node_GetDataTableRow as they have some solid comments as well as fairly straightforward outcomes (not the code though, it is hard to read without getting to know K2Node syntax).
Bringing this back to my intended outcomes, why did I have to use K2Nodes? You can absolutely achieve the same result through normal UFUNCTIONS. However I wanted to be lazy in the most difficult way; rather than having a Node per variable type I wanted a single master Node that does it all.
Initially I tried using a CustomThunk which is how you can accept Wildcards into normal
UFUNCTIONS. While there is a lot to learn there, I won’t go into it in this blog – it didn’t do the job as I couldn’t get Wildcard return values. So the only choice was K2Nodes.
So I began digging.
Anatomy of a K2Node
First I had to do some reading on the subject. Here are a few good resources that I initially utilised:
But your most helpful resource, after reading through all those (which you should), is going to be existing Nodes in the Engine, especially a Node that has similar functionality to what you’re looking to achieve. In my case this was
K2Node_VariableSet. And yes, those last two are the Get/Set variable Nodes that are in every Blueprint graph ever.
One thing that became apparent is that there is a solid amount of boilerplate present in all K2Nodes that is responsible for how it looks and behaves. Take these for example:
As you can see, these three functions are simply responsible for giving the Node its name, tooltip, and category. It’s worth noting that they return
LOCTEXT types, which is because this is technically Slate code, so the text has to be clean, optimised, and localisable.
Another is this:
This function adds your K2Node to the right-click context menu in the Blueprint Graph, so it’s quite important.
The other big chunks of boilerplate are these:
These are handy little functions for getting references to all the pins (inputs and outputs) that this Node has. You don’t need these functions, but they do make your life easier. Note that they don’t override anything, so their implementation is up to you. Here’s how I did it:
Note the lack of a function to Get the Exec pin. This is because it is already declared in the super
K2Node.h and doesn’t need overriding.
Last up on our tour of boilerplates is the handling and storing of FNames. In my travels I came across three different methods for caching FText/FNames, one of which has a clear mechanical purpose, while the other two are more straightforward but less versatile.
We cache these because, according to a handy comment in
Since a lot of Slate stuff is run on UI Tick, it is wise to minimise overhead where possible.
The first method is as follows:
In this example I am returning the FName for my Setter Functions in a Blueprint Function Library. These are just normal
This method can be accessed with
FSetterFunctionNames::FloatSetterName and is pretty straightforward.
The second method:
Very similar, this method uses a Struct in place of the Namespace, and static const functions in place of static const variables. You would access via
FGetPinName::GetTargetPinName() which is pretty much the same as the Namespace method.
I could be wrong (prefix that onto every statement in this blog), but I consider these two approaches interchangeable, and it comes down to your own preferences and coding standards.
The last method is this:
I didn’t end up using this approach as I only came across it after the fact, but given it utilises an Engine Struct I believe it would be the most reliable. It also allows for dynamic cached FTexts, which is rad because your Node title (in this example) can change depending on input values (which is what the
Node does, check it out). This means that it is only constructing the new FText once when needed, rather than every tick.
Those are the bulk of the boilerplate. Next we’ll move onto the main functions you need to call to make the magic happen.
There are many, many functions in the super
K2Node class, that do a host of different things. You won’t need most of them most of the time, but there are some that you will need all the time, like these two main functions (in addition to those listed above) that you will always need to override:
This function is where you add input and output pins to the Node, at creation.
This function is what runs when you compile and run the Node, so it can be considered the Runtime functionality of the Node, rather than the Editor functionality.
Let’s look at my definitions for the Get Node, first
As you can see, the
CreatePin() function is the weightlifter in this case. Calling it will add and register the resulting Pin to the K2Node. No need to store as a variable unless you need to do something to it, such as setting its default value (its value when it isn’t connected to another pin).
The only other fancy part is creating and passing
FCreatePinParams for making slightly more advanced Pins, in this case a Pass by Reference. This
FCreatePinsParam is then passed as an argument to the
That’s all there is to this function, at this level.
Let’s have a look at
That’s a bit more than the other, so let’s break it down a bit.
After calling the super I call one of my own functions
* BlueprintFunction = FindSetterFunctionByType(GetNewValuePin()->PinType); which simply returns an appropriate Set function based on the
FPinType argument, so if the Input Pin is a Boolean, it returns the
SetBooleanByName() function reference by accessing the
FSetterFunctionNames namespace mentioned earlier.
Once we have stored that
UFUNCTION and confirmed it exists, we then make a
UK2Node_CallFunction Node. This can be thought of as a basic K2Node wrapper for any function, which generates its own Input and Output pins based on the declaration of the
UFUNCTION it is wrapped around. So in this section:
CallFunction is the
K2Node_CallFunction, and we pass it our stored
UFUNCTION, then run its
AllocateDefaultPins() to get it set up.
Now we’re ready to simply plug the inputs and outputs from our own K2Node to the K2Node_CallFunction “Node” that we have spawned.
To explain; the
UFUNCTION that I have stored has five plus two “Pins”:
Exec: Which is the input that runs the function, with no real comparison in raw c++
Then: Which is the output signal once the function has finished running, again with no direct raw c++ comparison.
Then the actual arguments:
UObject* input argument.
FName input argument.
NewValue: The new value to be input. So if we are modifying a boolean value this would be a bool, if it’s a float value it would be a float, etc.
OutValue: A const output value, with the same type as NewValue.
Return: The default return type for the function, as declared in the c++, which for all of these was a Boolean, ie
Here is it’s declaration (for the boolean version) for reference:
Looking back + moving forward
Looking back, K2Nodes aren’t nearly as complex as they seem, and it is more a matter of understanding what types of pins and functions work best for what you are trying to achieve. I’ve glossed over a few functions of importance, but the broad strokes are there.
If you want to check out the full, bug-ridden and insufficiently commented code for my two Nodes, click here. It’s worth noting that currently my Nodes don’t handle structs and enums. They’re a little more complex and so will take some more work. I will continue to tinker with them and improve them as my understanding grows.
Otherwise, if you have any questions, get in touch with me on Twitter via @_nFerrar. And, of course, subscribe below to get more tutorials and updates from the team.