Skip to content

Instantly share code, notes, and snippets.

@jeremyabel
Last active September 21, 2022 17:23
Show Gist options
  • Save jeremyabel/29d778a29145c82ef1811c325a219f2a to your computer and use it in GitHub Desktop.
Save jeremyabel/29d778a29145c82ef1811c325a219f2a to your computer and use it in GitHub Desktop.

Making Custom Blueprint Nodes The Hard Way:

As far as I can tell, there's three ways to create custom blueprint nodes: - regular UFUNCTIONs - writing custom nodes in C++ which interface directly to the BP compiler - writing custom nodes in C++ which generate pre-connected blueprint nodes in an intermediate compilation step

I'll be talking about the 3rd option here, as the 1st one is already something everybody does all the time, and I've never looked into the second option, since it seemed beyond my skillset at the time.

The basic gist is this: you will be creating a node that looks like a single node to the user, but will in fact expand into any number of additional regular blueprint nodes that can be connected like normal. This is only really useful for creating macros, effectively, only it allows you to use nodes you would normally not be allowed to use inside a typical BP macro.

The example I'll be using here is a node I wrote called "Register Tweakable Variable".

This node takes in a reference to a float blueprint variable, a few parameters, and two keyboard key selections. It automatically creates a few events that bind to the two keyboard inputs, and when one of them is pressed, it increments the variable up by some amount, and when the other is pressed, the variable is decremented. We use this a lot for dialing in animation and interaction feel, since it's a pain to quickly integrate with some UI slider widgets or whatever.

Looking at the code, everything up until the ExpandNode() function is boilerplate stuff and setting up the node parameters. ExpandNode() is where the juicy stuff is though. This function is ran during BP compilation, and it just poops out even more BP nodes that then also get compiled like normal.

So the entire process is basically just like building a normal blueprint, only it's thru C++ code. So lots of calls to functions like SpawnIntermediateNode() and TryCreateConnection(). I'm sure reading the code will make more sense than me trying to explain it here, so I'll just leave this off with one last tip:

If you want to verify the stuff your node expands into, use the node in a blueprint, and then hit the checkbox in File -> Developer -> Save Intermediate Build Products. Hit compile, and a bunch of graphs will be added. The one you want will be listed as functions under a category called "$$ Execute Ubergraph [Your Node Name] $$". It'll be all spaghetti, but if you tease it apart, you'll be able to make sense of it.

// Fill out your copyright notice in the Description page of Project Settings.
#include "GNNode_RegisterTweakableVariable.h"
#include "GNBaseHUD.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "EdGraphSchema_K2.h"
#include "EdGraph/EdGraphNodeUtils.h"
#include "K2Node_InputKeyEvent.h"
#include "K2Node_CallFunction.h"
#include "K2Node_InputKey.h"
#include "K2Node_VariableSetRef.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "EditorCategoryUtils.h"
#include "KismetCompiler.h"
#include "ScopedTransaction.h"
#include "Kismet/KismetMathLibrary.h"
#define LOCTEXT_NAMESPACE "GNNode_RegisterTweakableVariable"
static FName TargetVariablePinName(TEXT("Target Variable"));
static FName ChangeAmountPinName(TEXT("Change Amount"));
static FName HUDPinName(TEXT("HUD"));
UGNNode_RegisterTweakableVariable::UGNNode_RegisterTweakableVariable(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void UGNNode_RegisterTweakableVariable::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
CachedNodeTitle.Clear();
CachedTooltip.Clear();
}
void UGNNode_RegisterTweakableVariable::AllocateDefaultPins()
{
Super::AllocateDefaultPins();
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
// Create exec pins for input, update, and finished events
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
// Target variable pin
UEdGraphPin* TargetVariablePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Float, TargetVariablePinName);
SetPinToolTip(*TargetVariablePin, LOCTEXT("UpKeyPinDescription", "The target variable."));
TargetVariablePin->PinType.bIsReference = true;
// Name pin
// Change amount pin
UEdGraphPin* ChangeAmountPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Float, ChangeAmountPinName);
SetPinToolTip(*ChangeAmountPin, LOCTEXT("ChangeAmountPinDescription", "Amount the variable changes on each keypress."));
K2Schema->SetPinAutogeneratedDefaultValue(ChangeAmountPin, TEXT("0.1"));
ChangeAmountPin->bNotConnectable = true;
// HUD pin
UEdGraphPin* HUDPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, AGNBaseHUD::StaticClass(), HUDPinName);
SetPinToolTip(*HUDPin, LOCTEXT("HUDPinDescription", "The HUD which will display the tweakable variable text info."));
}
FText UGNNode_RegisterTweakableVariable::GetIncrementKeyText() const
{
return IncrementInputKey.GetDisplayName();
}
FText UGNNode_RegisterTweakableVariable::GetDecrementKeyText() const
{
return DecrementInputKey.GetDisplayName();
}
FText UGNNode_RegisterTweakableVariable::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
if (CachedNodeTitle.IsOutOfDate(this))
{
FFormatNamedArguments Args;
Args.Add(TEXT("IncrementKey"), GetIncrementKeyText());
Args.Add(TEXT("DecrementKey"), GetDecrementKeyText());
// FText::Format() is slow, so we cache this to save on performance
CachedNodeTitle.SetCachedText(FText::Format(NSLOCTEXT("GNNode", "RegisterTweakableVariable_Name_WithModifiers", "Register Tweakable Variable: Shift-{IncrementKey} / {DecrementKey}"), Args), this);
}
return CachedNodeTitle;
}
FText UGNNode_RegisterTweakableVariable::GetTooltipText() const
{
if (CachedTooltip.IsOutOfDate(this))
{
FText IncrementKeyText = GetIncrementKeyText();
FText DecrementKeyText = GetDecrementKeyText();
// FText::Format() is slow, so we cache this to save on performance
CachedTooltip.SetCachedText(FText::Format(NSLOCTEXT("K2Node", "RegisterTweakableVariable_Tooltip_Modifiers", "Creates events for tweaking a variable. The variable increments by the given amount when pressing Shift-{0}, and decrements when pressing Shift-{1}."), IncrementKeyText, DecrementKeyText), this);
}
return CachedTooltip;
}
FSlateIcon UGNNode_RegisterTweakableVariable::GetIconAndTint(FLinearColor& OutColor) const
{
OutColor = GetNodeTitleColor();
static FSlateIcon Icon("EditorStyle", EKeys::GetMenuCategoryPaletteIcon(IncrementInputKey.GetMenuCategory()));
return Icon;
}
void UGNNode_RegisterTweakableVariable::SetPinToolTip(UEdGraphPin& MutatablePin, const FText& PinDescription) const
{
MutatablePin.PinToolTip = UEdGraphSchema_K2::TypeToText(MutatablePin.PinType).ToString();
UEdGraphSchema_K2 const* const K2Schema = Cast<const UEdGraphSchema_K2>(GetSchema());
if (K2Schema != nullptr)
{
MutatablePin.PinToolTip += TEXT(" ");
MutatablePin.PinToolTip += K2Schema->GetPinDisplayName(&MutatablePin).ToString();
}
MutatablePin.PinToolTip += FString(TEXT("\n")) + PinDescription.ToString();
}
void UGNNode_RegisterTweakableVariable::PinDefaultValueChanged(UEdGraphPin* Pin)
{
}
void UGNNode_RegisterTweakableVariable::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
// actions get registered under specific object-keys; the idea is that
// actions might have to be updated (or deleted) if their object-key is
// mutated (or removed)... here we use the node's class (so if the node
// type disappears, then the action should go with it)
UClass* ActionKey = GetClass();
// to keep from needlessly instantiating a UBlueprintNodeSpawner, first
// check to make sure that the registrar is looking for actions of this type
// (could be regenerating actions for a specific asset, and therefore the
// registrar would only accept actions corresponding to that asset)
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
{
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
check(NodeSpawner != nullptr);
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
}
}
FText UGNNode_RegisterTweakableVariable::GetMenuCategory() const
{
static FNodeTextCache CachedCategory;
if (CachedCategory.IsOutOfDate(this))
{
// FText::Format() is slow, so we cache this to save on performance
CachedCategory.SetCachedText(FEditorCategoryUtils::BuildCategoryString(FCommonEditorCategory::Math, LOCTEXT("DevCategory", "Development")), this);
}
return CachedCategory;
}
bool UGNNode_RegisterTweakableVariable::IsCompatibleWithGraph(UEdGraph const* Graph) const
{
// This node expands into event nodes and must be placed in a Ubergraph
EGraphType const GraphType = Graph->GetSchema()->GetGraphType(Graph);
bool bIsCompatible = (GraphType == EGraphType::GT_Ubergraph);
if (bIsCompatible)
{
UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(Graph);
UEdGraphSchema_K2 const* K2Schema = Cast<UEdGraphSchema_K2>(Graph->GetSchema());
bool const bIsConstructionScript = (K2Schema != nullptr) ? UEdGraphSchema_K2::IsConstructionScript(Graph) : false;
bIsCompatible = (Blueprint != nullptr) && Blueprint->SupportsInputEvents() && !bIsConstructionScript && Super::IsCompatibleWithGraph(Graph);
}
return bIsCompatible;
}
void UGNNode_RegisterTweakableVariable::ValidateNodeDuringCompilation(class FCompilerResultsLog& MessageLog) const
{
Super::ValidateNodeDuringCompilation(MessageLog);
// Check increment key validity
if (!IncrementInputKey.IsValid())
{
MessageLog.Warning(*FText::Format(NSLOCTEXT("KismetCompiler", "Invalid_IncrementInputKey_Warning", "IncrementInputKey Event specifies invalid FKey'{0}' for @@"), FText::FromString(IncrementInputKey.ToString())).ToString(), this);
}
else if (!IncrementInputKey.IsBindableInBlueprints())
{
MessageLog.Warning(*FText::Format(NSLOCTEXT("KismetCompiler", "NotBindanble_IncrementInputKey_Warning", "IncrementInputKey Event specifies FKey'{0}' that is not blueprint bindable for @@"), FText::FromString(IncrementInputKey.ToString())).ToString(), this);
}
// Check decrement key validity
if (!DecrementInputKey.IsValid())
{
MessageLog.Warning(*FText::Format(NSLOCTEXT("KismetCompiler", "Invalid_DecrementInputKey_Warning", "DecrementInputKey Event specifies invalid FKey'{0}' for @@"), FText::FromString(DecrementInputKey.ToString())).ToString(), this);
}
else if (!DecrementInputKey.IsBindableInBlueprints())
{
MessageLog.Warning(*FText::Format(NSLOCTEXT("KismetCompiler", "NotBindanble_DecrementInputKey_Warning", "DecrementInputKey Event specifies FKey'{0}' that is not blueprint bindable for @@"), FText::FromString(DecrementInputKey.ToString())).ToString(), this);
}
// Check if sharing the same key
if (IncrementInputKey.IsValid() && DecrementInputKey.IsValid())
{
if (GetIncrementKeyText().EqualTo(GetDecrementKeyText()))
{
MessageLog.Warning(*FText::Format(NSLOCTEXT("KismetCompiler", "SameKey_Warning", "IncrementInputKey Event and DecrementInputKey Event cannot both specify FKey'{0}' for @@"), FText::FromString(DecrementInputKey.ToString())).ToString(), this);
}
}
}
void UGNNode_RegisterTweakableVariable::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
Super::ExpandNode(CompilerContext, SourceGraph);
bool bIsErrorFree = true;
const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema();
FName UpdateHUDNativeFunctionName = GET_FUNCTION_NAME_CHECKED(AGNBaseHUD, UpdateTweakableVariable);
UEdGraphPin* HUDPin = FindPin(HUDPinName);
FName TweakableVariableName = NAME_None;
// Get the name string for the target variable, if there is one. Add a prefix with the input key names.
if (FindPin(TargetVariablePinName)->LinkedTo.Num() > 0)
{
FText VariableName = FindPin(TargetVariablePinName)->LinkedTo[0]->GetDisplayName();
TweakableVariableName = FName(*FString::Printf(TEXT("%s / %s: %s"), *IncrementInputKey.GetDisplayName().ToString(), *DecrementInputKey.GetDisplayName().ToString(), *VariableName.ToString()));
}
///////////////////////////////////////////////////////////
// Create increment input key event
UK2Node_InputKeyEvent* IncrementInputKeyEvent = CompilerContext.SpawnIntermediateEventNode<UK2Node_InputKeyEvent>(this, nullptr, SourceGraph);
IncrementInputKeyEvent->CustomFunctionName = FName(*FString::Printf(TEXT("IncInpActEvt_%s_%s"), *IncrementInputKey.ToString(), *IncrementInputKeyEvent->GetName()));
IncrementInputKeyEvent->InputChord.Key = IncrementInputKey;
IncrementInputKeyEvent->InputChord.bShift = true;
IncrementInputKeyEvent->bInternalEvent = true;
IncrementInputKeyEvent->AllocateDefaultPins();
// Create Set By Ref node for Increment
UK2Node_VariableSetRef* SetIncrementRefNode = CompilerContext.SpawnIntermediateNode<UK2Node_VariableSetRef>(this, SourceGraph);
SetIncrementRefNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(SetIncrementRefNode, this);
// Create Add Float node
FName AddFloatNativeFunctionName = GET_FUNCTION_NAME_CHECKED(UKismetMathLibrary, Add_FloatFloat);
UK2Node_CallFunction* AddFloatNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
AddFloatNode->FunctionReference.SetExternalMember(AddFloatNativeFunctionName, UKismetMathLibrary::StaticClass());
AddFloatNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(AddFloatNode, this);
// Connect up the Add node
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *AddFloatNode->FindPin(TEXT("A")), true);
Schema->CopyPinLinks(*FindPin(ChangeAmountPinName), *AddFloatNode->FindPin(TEXT("B")), true);
// Connect up the Increment Set Ref node
Schema->TryCreateConnection(AddFloatNode->GetReturnValuePin(), SetIncrementRefNode->GetValuePin());
Schema->TryCreateConnection(Schema->FindExecutionPin(*IncrementInputKeyEvent, EGPD_Output), SetIncrementRefNode->GetExecPin());
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *SetIncrementRefNode->GetTargetPin(), true);
// Create HUD UpdateTweakableVariable Increment function call
UK2Node_CallFunction* IncrementUpdateHUDNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
IncrementUpdateHUDNode->FunctionReference.SetExternalMember(UpdateHUDNativeFunctionName, AGNBaseHUD::StaticClass());
IncrementUpdateHUDNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(IncrementUpdateHUDNode, this);
// Connect up the HUD UpdateTweakableVariable Increment function call node
Schema->TryCreateConnection(Schema->FindExecutionPin(*SetIncrementRefNode, EGPD_Output), IncrementUpdateHUDNode->GetExecPin());
Schema->CopyPinLinks(*HUDPin, *IncrementUpdateHUDNode->FindPin(UEdGraphSchema_K2::PN_Self), true);
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *IncrementUpdateHUDNode->FindPin(TEXT("Value")), true);
Schema->TrySetDefaultValue(*IncrementUpdateHUDNode->FindPin(TEXT("Name")), TweakableVariableName.ToString());
///////////////////////////////////////////////////////////
// Create decrement input key event
UK2Node_InputKeyEvent* DecrementInputKeyEvent = CompilerContext.SpawnIntermediateEventNode<UK2Node_InputKeyEvent>(this, nullptr, SourceGraph);
DecrementInputKeyEvent->CustomFunctionName = FName(*FString::Printf(TEXT("DecInpActEvt_%s_%s"), *DecrementInputKey.ToString(), *DecrementInputKeyEvent->GetName()));
DecrementInputKeyEvent->InputChord.Key = DecrementInputKey;
DecrementInputKeyEvent->InputChord.bShift = true;
DecrementInputKeyEvent->bInternalEvent = true;
DecrementInputKeyEvent->AllocateDefaultPins();
// Create Set By Ref node for Decrement
UK2Node_VariableSetRef* SetDecrementRefNode = CompilerContext.SpawnIntermediateNode<UK2Node_VariableSetRef>(this, SourceGraph);
SetDecrementRefNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(SetDecrementRefNode, this);
// Create Subtract Float node
FName SubtractFloatNativeFunctionName = GET_FUNCTION_NAME_CHECKED(UKismetMathLibrary, Subtract_FloatFloat);
UK2Node_CallFunction* SubtractFloatNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
SubtractFloatNode->FunctionReference.SetExternalMember(SubtractFloatNativeFunctionName, UKismetMathLibrary::StaticClass());
SubtractFloatNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(SubtractFloatNode, this);
// Connect up the Subtract node
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *SubtractFloatNode->FindPin(TEXT("A")), true);
Schema->CopyPinLinks(*FindPin(ChangeAmountPinName), *SubtractFloatNode->FindPin(TEXT("B")), true);
// Connect up the Decrement Set Ref node
Schema->TryCreateConnection(SubtractFloatNode->GetReturnValuePin(), SetDecrementRefNode->GetValuePin());
Schema->TryCreateConnection(Schema->FindExecutionPin(*DecrementInputKeyEvent, EGPD_Output), SetDecrementRefNode->GetExecPin());
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *SetDecrementRefNode->GetTargetPin(), true);
// Create HUD UpdateTweakableVariable Decrement function call
UK2Node_CallFunction* DecrementUpdateHUDNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
DecrementUpdateHUDNode->FunctionReference.SetExternalMember(UpdateHUDNativeFunctionName, AGNBaseHUD::StaticClass());
DecrementUpdateHUDNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(DecrementUpdateHUDNode, this);
// Connect up the HUD UpdateTweakableVariable Decrement function call node
Schema->TryCreateConnection(Schema->FindExecutionPin(*SetDecrementRefNode, EGPD_Output), DecrementUpdateHUDNode->GetExecPin());
Schema->CopyPinLinks(*HUDPin, *DecrementUpdateHUDNode->FindPin(UEdGraphSchema_K2::PN_Self), true);
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *DecrementUpdateHUDNode->FindPin(TEXT("Value")), true);
Schema->TrySetDefaultValue(*DecrementUpdateHUDNode->FindPin(TEXT("Name")), TweakableVariableName.ToString());
///////////////////////////////////////////////////////////
// Create HUD UpdateTweakableVariable Initial function call
UK2Node_CallFunction* InitialUpdateHUDNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
InitialUpdateHUDNode->FunctionReference.SetExternalMember(UpdateHUDNativeFunctionName, AGNBaseHUD::StaticClass());
InitialUpdateHUDNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(InitialUpdateHUDNode, this);
// Connect up the HUD UpdateTweakableVariable Initial function call node
CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *InitialUpdateHUDNode->GetExecPin());
CompilerContext.MovePinLinksToIntermediate(*FindPin(UEdGraphSchema_K2::PN_Then), *InitialUpdateHUDNode->GetThenPin());
Schema->CopyPinLinks(*HUDPin, *InitialUpdateHUDNode->FindPin(UEdGraphSchema_K2::PN_Self), true);
Schema->CopyPinLinks(*FindPin(TargetVariablePinName), *InitialUpdateHUDNode->FindPin(TEXT("Value")), true);
Schema->TrySetDefaultValue(*InitialUpdateHUDNode->FindPin(TEXT("Name")), TweakableVariableName.ToString());
if (!bIsErrorFree)
{
CompilerContext.MessageLog.Error(*LOCTEXT("InternalConnectionError", "GNNode_SimpleTween: Internal connection error. @@").ToString(), this);
}
// Cleanup links to ourself and we are done!
BreakAllNodeLinks();
}
#undef LOCTEXT_NAMESPACE
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "EdGraph/EdGraphPin.h"
#include "Textures/SlateIcon.h"
#include "K2Node.h"
#include "GNNode_RegisterTweakableVariable.generated.h"
class FBlueprintActionDatabaseRegistrar;
class UEdGraph;
/**
*
*/
UCLASS()
class GENESISNOIREDITOR_API UGNNode_RegisterTweakableVariable : public UK2Node
{
GENERATED_UCLASS_BODY()
UPROPERTY(EditAnywhere, Category = "Input")
FKey IncrementInputKey;
UPROPERTY(EditAnywhere, Category = "Input")
FKey DecrementInputKey;
FText GetIncrementKeyText() const;
FText GetDecrementKeyText() const;
//FText GetTargetVariableText() const;
//~ Begin UObject Interface
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
//~ End UObject Interface
//~ Begin UEdGraphNode Interface.
virtual void AllocateDefaultPins() override;
virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override;
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual FText GetTooltipText() const override;
virtual FSlateIcon GetIconAndTint(FLinearColor& OutColor) const override;
virtual bool IsCompatibleWithGraph(UEdGraph const* Graph) const override;
//~ End UEdGraphNode Interface.
//~ Begin UK2Node Interface.
virtual bool IsNodePure() const override { return false; }
virtual bool ShouldShowNodeProperties() const override { return true; }
virtual void ValidateNodeDuringCompilation(class FCompilerResultsLog& MessageLog) const override;
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual FText GetMenuCategory() const override;
//~ End UK2Node Interface.
private:
/** Constructing FText strings can be costly, so we cache the node's title/tooltip */
FNodeTextCache CachedTooltip;
FNodeTextCache CachedNodeTitle;
/**
* Takes the specified "MutatablePin" and sets its 'PinToolTip' field (according
* to the specified description)
*
* @param MutatablePin The pin you want to set tool-tip text on
* @param PinDescription A string describing the pin's purpose
*/
void SetPinToolTip(UEdGraphPin& MutatablePin, const FText& PinDescription) const;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment