A not-so-brief intro to K2Nodes
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_SpawnActorFromClass
and 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_GetClassDefaults
, K2Node_VariableGet
and 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 K2Node_GetDataTableRow
:
/** Constructing FText strings can be costly … */
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 UFUNCTION
s.
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 GetDataTableRow
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.
Main functions
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 AllocateDefaultPins()
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 CreatePin()
an FCreatePinParams
for making slightly more advanced Pins, in this case a Pass by Reference. This FCreatePinsParam
is then passed as an argument to the CreatePin()
function.
That’s all there is to this function, at this level.
Let’s have a look at ExpandNode()
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 UFunction
* 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:
Target: A UObject*
input argument.
VarName: An 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 bSuccess
.
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.
Happy coding!