Loading...

Creating K2 nodes in Unreal Engine 4

Introduction

I've made this tutorial because I couldn't find any up to date tutorials on how to make K2 nodes. Therefor I will use this tutorial to show the minimal structure for setting up K2 nodes with linked functionality. To make use of the K2 functionality you will need to set up an editor module as well. If you'd like to know how, I've created a tutorial that shows how to create an editor module here.

Unreal Engine 4 version: 4.20

Contents:

  • Create basic K2 Node functionality
  • Link K2 Node with a runtime blueprint function library
  • Implementation in editor module

Create a blueprint function library

To bind some functionality to our nodes we need to set up a BlueprintFunctionLibrary. You could theoretically put any functionality in there that you would want your K2 nodes to do.

Firstly create a BlueprintFunctionLibrary in the runtime module.

For the parent class, select BlueprintFunctionLibrary and click next.

Fill in a name for the new class and click Create Class. Let Unreal Engine recompile and open up the class you just created.

For demo purposes I will only create a simple method that takes in a FString and returns a bool.

header file

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TutorialBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS()
class TUTORIAL_API UTutorialBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
    UFUNCTION(meta = (BlueprintInternalUseOnly = "true"), BlueprintCallable, Category = "Tutorial")
    static bool TextContainsWhiteSpace(FString InText);
};

.cpp file

#include "TutorialBlueprintFunctionLibrary.h"
#include "CoreMinimal.h"

bool UTutorialBlueprintFunctionLibrary::TextContainsWhiteSpace(FString InText)
{
    if (InText.IsEmpty()) {
        return false;
    }
    return InText.Contains(" ", ESearchCase::IgnoreCase, ESearchDir::FromStart);
}

Creating the K2 Node classes.

Now that we have our blueprint function library set up, we can start adding K2 Node functionality so we can have some custom nodes in our blueprint editor!

We'll start by creating a new class for our K2 Node in the editor module. I found the easiest way to do this is by opening the engine and creating a new script from there.

Then, make sure to check "Show All Classes" to show all engine classes. We'll be looking for K2Node as a parent class.

When we click next we need to fill in a name for our K2 Node just like any other c++ class. However, I do recommend prefixing it with K2 or BP as is, or was, common to do.

The important thing here is, is that we want to create this class for our editor module as it will contain mostly editor only code. We can do this simply by clicking on the correct module as seen in the image here down below. Let Unreal Engine do it's thing for a while until it says we need to recompile this new class for ourselves. Close down Unreal Engine and in Visual Studio press ctrl + shift + b to start building the project. If all went well, we should see something like this in our output log:

========== Build: 2 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

Adding the basic structure

We'll need to override and implement a couple of methods for our K2Node to work. But first, add these entries to the TutorialEditor.build.cs file. There should already be a PrivateDependencyModuleNames array in there, so just add these entries to it.

PrivateDependencyModuleNames.AddRange(
            new string[]
            {
                "Engine",
                "UnrealEd",
                "KismetCompiler",
            });

Any dependencies must be added here. If you're starting to see errors with error LNK{NUMBERS}: then you're probably missing one or more entries.

Now, open up the UK2_TutorialNode class we've just created and we will start with implementing the UEdGraphNode class.

//Create our pins
virtual void AllocateDefaultPins() override;
//Implement our own node title
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
//Implement our own node tooltip text
virtual FText GetTooltipText() const override;

Next, we have a few methods from the UK2Node class we need to implement.

// Implement our own node category 
virtual FText GetMenuCategory() const override;
//This method works like a bridge and connects our K2Node to the actual Blueprint Library method. This is where the actual logic happens. 
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;

virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;

Complete header file:

#pragma once

#include "CoreMinimal.h"
#include "K2Node.h"
#include "K2_TutorialNode.generated.h"

UCLASS()
class TUTORIALEDITOR_API UK2_TutorialNode : public UK2Node
{
    GENERATED_BODY()
public:

    //UEdGraphNode implementation
    virtual void AllocateDefaultPins() override;
    virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
    virtual FText GetTooltipText() const override;
    //UEdGraphNode implementation

    //K2Node implementation 
    virtual FText GetMenuCategory() const override;
    virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
    virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
    //K2Node implementation 

};

.CPP implementation

Includes:

#include "K2_TutorialNode.h"

#include "TutorialBlueprintFunctionLibrary.h"

#include "KismetCompiler.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "K2Node_CallFunction.h"

#define LOCTEXT_NAMESPACE "K2Node_Tutorial"

We need to define our pin names first so we can always get a reference to them later:

struct FGetPinName {

    static const FName& GetTextPin() {
        static const FName TextPinName(TEXT("InputText"));
        return TextPinName;
    }

    static const FName& GetOutputPin() {
        static const FName OutputPinName(TEXT("IsValidText"));
        return OutputPinName;
    }
};

To actually create the in and out pins, we'll need to implement the AllocateDefaultPins().

void UK2_TutorialNode::AllocateDefaultPins()
{
    Super::AllocateDefaultPins();
    const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

    /*Create our pins*/

    // Execution pins
    CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
    CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);

    //Input
    UEdGraphPin* InTextPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FGetPinName::GetTextPin());
    K2Schema->SetPinAutogeneratedDefaultValue(InTextPin, FString("This is a default string value"));

    //Output
    UEdGraphPin* OutValidPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Boolean, FGetPinName::GetOutputPin());
    K2Schema->SetPinAutogeneratedDefaultValueBasedOnType(OutValidPin);

}

To make sure our K2 node actually shows up in the blueprint context menu we need to implement a couple of methods that to just that. The function names are pretty self explanatory.

FText UK2_TutorialNode::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
    return LOCTEXT("TutorialK2Node_Title", "Tutorial K2 Node");
}

FText UK2_TutorialNode::GetTooltipText() const
{
    return LOCTEXT("TutorialK2Node_Tooltip", "This is our Tutorial K2 Node Tooltip information!");
}

FText UK2_TutorialNode::GetMenuCategory() const
{
    return LOCTEXT("TutorialK2Node_MenuCategory", "Custom K2 Nodes");
}

Most of the actual functionality is implemented in the ExpandNode() method. In this method we link our blueprint method with the in and outputs of our K2 node. Essentially we create another node under the hood and pass the information we have on to that node which in turn does the actual work.

void UK2_TutorialNode::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
    Super::ExpandNode(CompilerContext, SourceGraph);

    //This is just a hard reference to the static method that lives in the BlueprintLibrary. Probably not the best of ways.
    UFunction* BlueprintFunction = UTutorialBlueprintFunctionLibrary::StaticClass()->FindFunctionByName(FName(TEXT("TextContainsWhiteSpace")));

    if (BlueprintFunction == NULL) {
        CompilerContext.MessageLog.Error(*LOCTEXT("InvalidFunctionName", "The function has not been found.").ToString(), this);
        return;
    }

    UK2Node_CallFunction* CallFunction = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);

    CallFunction->SetFromFunction(BlueprintFunction);
    CallFunction->AllocateDefaultPins();
    CompilerContext.MessageLog.NotifyIntermediateObjectCreation(CallFunction, this);

    //Input
    CompilerContext.MovePinLinksToIntermediate(*FindPin(FGetPinName::GetTextPin()), *CallFunction->FindPin(TEXT("InText")));

    //Output
    CompilerContext.MovePinLinksToIntermediate(*FindPin(FGetPinName::GetOutputPin()), *CallFunction->GetReturnValuePin());

    //Exec pins
    UEdGraphPin* NodeExec = GetExecPin();
    UEdGraphPin* NodeThen = FindPin(UEdGraphSchema_K2::PN_Then);

    UEdGraphPin* InternalExec = CallFunction->GetExecPin();
    CompilerContext.MovePinLinksToIntermediate(*NodeExec, *InternalExec);

    UEdGraphPin* InternalThen = CallFunction->GetThenPin();
    CompilerContext.MovePinLinksToIntermediate(*NodeThen, *InternalThen);

        //After we are done we break all links to this node (not the internally created one) 
    BreakAllNodeLinks();
}

Complete .CPP implementation

#include "K2_TutorialNode.h"

#include "TutorialBlueprintFunctionLibrary.h"

#include "KismetCompiler.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "K2Node_CallFunction.h"

#define LOCTEXT_NAMESPACE "K2Node_Tutorial"

struct FGetPinName {

    static const FName& GetTextPin() {
        static const FName TextPinName(TEXT("InputText"));
        return TextPinName;
    }

    static const FName& GetOutputPin() {
        static const FName OutputPinName(TEXT("IsValidText"));
        return OutputPinName;
    }
};

void UK2_TutorialNode::AllocateDefaultPins()
{
    Super::AllocateDefaultPins();
    const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

    /*Create our pins*/

    // Execution pins
    CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
    CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);

    //Input
    UEdGraphPin* InTextPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FGetPinName::GetTextPin());
    K2Schema->SetPinAutogeneratedDefaultValue(InTextPin, FString("This is a default string value"));

    //Output
    UEdGraphPin* OutValidPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Boolean, FGetPinName::GetOutputPin());
    K2Schema->SetPinAutogeneratedDefaultValueBasedOnType(OutValidPin);

}

FText UK2_TutorialNode::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
    return LOCTEXT("TutorialK2Node_Title", "Tutorial K2 Node");
}

FText UK2_TutorialNode::GetTooltipText() const
{
    return LOCTEXT("TutorialK2Node_Tooltip", "This is our Tutorial K2 Node Tooltip information!");
}

FText UK2_TutorialNode::GetMenuCategory() const
{
    return LOCTEXT("TutorialK2Node_MenuCategory", "Custom K2 Nodes");
}

void UK2_TutorialNode::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
    Super::ExpandNode(CompilerContext, SourceGraph);

    //This is just a hard reference to the static method that lives in the BlueprintLibrary. Probably not the best of ways.
    UFunction* BlueprintFunction = UTutorialBlueprintFunctionLibrary::StaticClass()->FindFunctionByName(FName(TEXT("TextContainsWhiteSpace")));

    if (BlueprintFunction == NULL) {
        CompilerContext.MessageLog.Error(*LOCTEXT("InvalidFunctionName", "The function has not been found.").ToString(), this);
        return;
    }

    UK2Node_CallFunction* CallFunction = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);

    CallFunction->SetFromFunction(BlueprintFunction);
    CallFunction->AllocateDefaultPins();
    CompilerContext.MessageLog.NotifyIntermediateObjectCreation(CallFunction, this);

    //Input
    CompilerContext.MovePinLinksToIntermediate(*FindPin(FGetPinName::GetTextPin()), *CallFunction->FindPin(TEXT("InText")));

    //Output
    CompilerContext.MovePinLinksToIntermediate(*FindPin(FGetPinName::GetOutputPin()), *CallFunction->GetReturnValuePin());

    //Exec pins
    UEdGraphPin* NodeExec = GetExecPin();
    UEdGraphPin* NodeThen = FindPin(UEdGraphSchema_K2::PN_Then);

    UEdGraphPin* InternalExec = CallFunction->GetExecPin();
    CompilerContext.MovePinLinksToIntermediate(*NodeExec, *InternalExec);

    UEdGraphPin* InternalThen = CallFunction->GetThenPin();
    CompilerContext.MovePinLinksToIntermediate(*NodeThen, *InternalThen);

    BreakAllNodeLinks();
}

//This method adds our node to the context menu
void UK2_TutorialNode::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
    Super::GetMenuActions(ActionRegistrar);

    UClass* Action = GetClass();

    if (ActionRegistrar.IsOpenForRegistration(Action)) {
        UBlueprintNodeSpawner* Spawner = UBlueprintNodeSpawner::Create(GetClass());
        check(Spawner != nullptr);

        ActionRegistrar.AddBlueprintAction(Action, Spawner);
    }
}
#undef LOCTEXT_NAMESPACE

Be the first to comment

Post Comment

This website uses cookies to ensure you get the best experience on my website