
Иногда надо создать функцию, которая должна быть и доступна в blueprints, и адаптироваться под входные данные. Особенно это касается wildcard
.
Можно прибегнуть к ручной прописке рефлексии UFUNCTION
. Однако, у этого есть свои ограничения. Для таких случаев в движке есть довольно старый класс – UK2Node
. Ниже приведены примеры движковых реализации этого класса.

Что такое K2 Node
ВАЖНО! Работа с нодами должна проходить в uncooked модуле.
Стоит начать с того, что UK2Node
это довольно старый класс, в котором всю работу надо писать руками.
Что это означает? Вам надо самим:
Добавить каждый пин.
Обновить тип пина при изменении.
Обновить отображаемый тип пина при изменении.
Зарегистрировать ноду в контекстном меню.
Перемещать/удалять/создавать соединения между пинами.
Прописать название(опционально).
Прописать описание(опционально).
Добавить описание каждого пина(опционально).
Звучит как что-то не очень увлекательное(так и есть), но другого выбора нет. Зато открывается больше возможностей для кастомизации.
Создаем класс
Для примера, создадим ноду, которая будет принимать на вход структуру Input Action Value
и сам объект Action Value
, а на выходе получать значение нужного нам типа. Если наше действие работает с float
, то на выходе мы получим float, если bool
, то bool
, и т.д.
Приступим.
h. файл
#pragma once
#include "CoreMinimal.h"
#include "K2Node.h"
#include "K2Node_GetInputValue.generated.h"
UCLASS()
class UK2Node_GetInputValue : public UK2Node
{
GENERATED_BODY()
public:
//~UEdGraphNode interface
virtual void PostReconstructNode() override;
virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override;
virtual void ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
//~End of UEdGraphNode interface
//~UK2Node interface
virtual void AllocateDefaultPins() override;
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual bool IsNodePure() const override { return true; }
// Здесь прописываем текст, который будет появляться при наведении на ноду
virtual FText GetTooltipText() const override
{
return NSLOCTEXT("K2Node", "GetInputValue", "Extract input value with type from action.");
}
// Пишем отображаемое название нашей функции
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override
{
return NSLOCTEXT("K2Node", "GetInputValue", "Get Input Value");
}
// Пишем название категории, в которой будет находиться наша функция
virtual FText GetMenuCategory() const override { return NSLOCTEXT("K2Node", "InputCategory", "Enhanced Input"); }
//~End of UK2Node interface
private:
void RefreshOutputPinType();
// Просто для удобства
UEdGraphPin* GetActionValuePin() const { return FindPinChecked(TEXT("ActionValue")); }
UEdGraphPin* GetActionPin() const { return FindPinChecked(TEXT("Action")); }
UEdGraphPin* GetOutputPin() const { return FindPinChecked(TEXT("Value")); }
};
Создаем входные и выходные пины
AllocateDefaultPins
void UK2Node_GetInputValue::AllocateDefaultPins()
{
/** Creates input pins for ActionValue and Action, and an output pin for Value. */
// Action value pin
UEdGraphPin* ActionValuePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, FInputActionValue::StaticStruct(),
TEXT("ActionValue"));
ActionValuePin->PinToolTip = TEXT("Value received from the input system for the specified action.");
// Action object pin
UEdGraphPin* ActionPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UInputAction::StaticClass(),
TEXT("Action"));
ActionPin->PinToolTip = TEXT(
"Input action to extract the expected value type from (used to determine output type).");
// Output action value type pin
UEdGraphPin* ValuePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, TEXT("Value"));
ValuePin->PinToolTip = TEXT(
"The extracted value of the input action, matching the expected type (bool, float, or Vector2D).");
Super::AllocateDefaultPins();
}
Эта функция отвечает за само создание пинов. В ней мы и прописываем входные и выходные параметры. Тут мы сразу указываем их базовый тип, который и будет отображаться при начальном отображении ноды при создании в blueprint graph.
Итак, что у нас тут есть? Мы создали 3 пина:
Входной пин структуры Input Action Value.
Входной пин самого Input Action.
Выходной пин типа wildcard, который мы и будем динамически менять.
Также, сразу даем им описание, которое высветится при наведении на них курсором.
Обновляем отображаемый тип пинов
PostReconstructNode
и PinDefaultValueChanged
Эти 2 функции нам нужны именно для пункта про ручное обновление отображаемого типа пинов. Они вызываются при реконстракте и ручном изменении значении пинов в редакторе. Для этих двух методов в private
секции дополнительно создана функция RefreshOutputPinType
. Т.к. её мы и будем вызывать в обоих случаях.
Обновление пинов
void UK2Node_GetInputValue::PostReconstructNode()
{
Super::PostReconstructNode();
RefreshOutputPinType();
}
void UK2Node_GetInputValue::PinDefaultValueChanged(UEdGraphPin* ChangedPin)
{
if (ChangedPin == GetActionPin())
{
RefreshOutputPinType();
}
}
void UK2Node_GetInputValue::RefreshOutputPinType()
{
/** Updates the output pin type based on the selected Action's ValueType. */
UEdGraphPin* OutputPin = GetOutputPin();
if (!OutputPin) return;
OutputPin->Modify();
// Resets pin type before updating
OutputPin->PinType = FEdGraphPinType();
const UEdGraphPin* ActionPin = GetActionPin();
if (!ActionPin || !ActionPin->DefaultObject)
{
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
return;
}
const UInputAction* InputAction = Cast<UInputAction>(ActionPin->DefaultObject);
if (!InputAction) return;
switch (InputAction->ValueType)
{
case EInputActionValueType::Boolean:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
break;
case EInputActionValueType::Axis1D:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Real;
OutputPin->PinType.PinSubCategory = TEXT("double");
break;
case EInputActionValueType::Axis2D:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
OutputPin->PinType.PinSubCategoryObject = TBaseStructure<FVector2D>::Get();
break;
case EInputActionValueType::Axis3D:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
OutputPin->PinType.PinSubCategoryObject = TBaseStructure<FVector>::Get();
break;
default:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
break;
}
// Notifies the system about pin type changes
GetSchema()->ForceVisualizationCacheClear();
GetGraph()->NotifyGraphChanged();
}
К первым двум функциям особо уже добавить нечего. Разве что в PinDefaultValueChanged
мы обновляем наш выходной пин только при изменении нашего объекта Input Action
, т.к. с него мы и получим наш выходной тип.
RefreshOutputPinType
Здесь и кроется вся логика отображаемого(важно) выходного значения.
OutputPin->Modify();
OutputPin->PinType = FEdGraphPinType();
Говорим движку, что мы меняем выходной пин(нужно для системы undo/redo), а также сбрасываем его тип перед изменением.
Далее мы пытаемся вытащить из нашего объекта, Input Action
, тип его принимаемого значения и на его основе уже устанавливаем отображаемый тип нашего выходного пина. Т.к. это обычный switch, то сделаю акцент лишь на основных трех полях.
OutputPin->PinType.PinCategory
OutputPin->PinType.PinSubCategoryObject
OutputPin->PinType.PinSubCategory
Именно здесь мы и задаем тот самый тип, который потом будет отображен в blueprint graph.
GetSchema()->ForceVisualizationCacheClear();
GetGraph()->NotifyGraphChanged();
Тут мы обновляем "рендер", чтобы наша нода корректно отобразилась после изменений и сообщаем о том, что blueprint изменился и надо бы его скомпилировать.
Регистрируем ноду в контекстном меню
virtual FText GetMenuCategory() const override { return NSLOCTEXT("K2Node", "InputCategory", "Enhanced Input"); }
void UK2Node_GetInputValue::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
/** Registers this node in the blueprint action database. */
if (!ActionRegistrar.IsOpenForRegistration(GetClass())) return;
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
check(NodeSpawner != nullptr);
NodeSpawner->NodeClass = GetClass();
ActionRegistrar.AddBlueprintAction(GetClass(), NodeSpawner);
}
Эти функции нам нужны исключительно для того, чтобы нашу ноду можно было вызвать в контекстном меню блупринта.

Основная функция
ExpandNode
void UK2Node_GetInputValue::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
/** Expands the node into a function call from UEnhancedInputLibrary. */
Super::ExpandNode(CompilerContext, SourceGraph);
UEdGraphPin* ActionValuePin = GetActionValuePin();
const UEdGraphPin* ActionPin = GetActionPin();
UEdGraphPin* OutputPin = GetOutputPin();
if (!ActionValuePin || !OutputPin)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("MissingPins", "GetInputValue: Missing pins").ToString(), this);
return;
}
if (!ActionPin || !ActionPin->DefaultObject)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("InvalidInputAction", "GetInputValue: Action pin is invalid or not set").ToString(), this);
return;
}
const UInputAction* InputAction = Cast<UInputAction>(ActionPin->DefaultObject);
if (!InputAction)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("InvalidInputAction", "GetInputValue: Action pin does not contain a valid InputAction").ToString(),
this);
return;
}
// Determines which function to call based on ValueType
FName FunctionName;
FName ActionValueName = TEXT("InValue");
switch (InputAction->ValueType)
{
case EInputActionValueType::Boolean:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToBool);
break;
case EInputActionValueType::Axis1D:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToAxis1D);
break;
case EInputActionValueType::Axis2D:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToAxis2D);
break;
case EInputActionValueType::Axis3D:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToAxis3D);
ActionValueName = TEXT("ActionValue");
break;
default:
CompilerContext.MessageLog.Error(
*LOCTEXT("UnsupportedType", "GetInputValue: Unsupported Action Value Type").ToString(), this);
return;
}
if (OutputPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("WildcardError", "GetInputValue: Output pin type is still Wildcard!").ToString(), this);
return;
}
// Creates a CallFunction node for the selected function
UK2Node_CallFunction* GetValueNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
if (!GetValueNode)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("NodeSpawnError", "GetInputValue: Failed to create intermediate function node").ToString(), this);
return;
}
GetValueNode->FunctionReference.SetExternalMember(FunctionName, UEnhancedInputLibrary::StaticClass());
GetValueNode->AllocateDefaultPins();
// Ensures the function has the correct input pin
UEdGraphPin* InValuePin = GetValueNode->FindPin(ActionValueName);
if (!InValuePin)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("MissingInputPin", "GetInputValue: Could not find expected input pin on GetValueNode").ToString(),
this);
return;
}
// Moves links from ActionValuePin -> InValuePin
CompilerContext.MovePinLinksToIntermediate(*ActionValuePin, *InValuePin);
// Moves links from OutputPin -> GetValueNode output
CompilerContext.MovePinLinksToIntermediate(*OutputPin, *GetValueNode->GetReturnValuePin());
// Breaks all links on this node since it's no longer needed
BreakAllNodeLinks();
}
В этой функции мы раскрываем нашу ноду на последовательность других нод(либо даже на одну), с которыми уже и будем соединять наши пины. Да, на самом деле K2 ноды скрытно раскрываются вплоть до целой серии вызовов других. Конкретно в нашем случае мы будем вызывать одну из уже существующих BlueprintInternalUseOnly
функций из плагина инпутов.
Итак, что мы делаем:
Проверяем валидность наших пинов.
Выбираем функцию для конвертации.
Создаем ноду этой функции в графе.
Перетаскиваем соединения наших пинов на пины только что размещенной ноды.
Обрываем все связи нашей ноды.
Дополнительная настройка
virtual bool IsNodePure() const override { return true; }
virtual FText GetTooltipText() const override
{
return NSLOCTEXT("K2Node", "GetInputValue", "Extract input value with type from action.");
}
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override
{
return NSLOCTEXT("K2Node", "GetInputValue", "Get Input Value");
}
Тут мы указываем, pure наша нода или impure(имеет execution пины или нет), а также даем отображаемое название вместе с описанием при наведении.
По желанию, можно еще настроить цвета всего и вся.

Полный код
.h файл(да, еще раз)
#pragma once
#include "CoreMinimal.h"
#include "K2Node.h"
#include "K2Node_GetInputValue.generated.h"
UCLASS()
class UK2Node_GetInputValue : public UK2Node
{
GENERATED_BODY()
public:
//~UEdGraphNode interface
virtual void PostReconstructNode() override;
virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override;
virtual void ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
//~End of UEdGraphNode interface
//~UK2Node interface
virtual void AllocateDefaultPins() override;
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual bool IsNodePure() const override { return true; }
virtual FText GetTooltipText() const override
{
return NSLOCTEXT("K2Node", "GetInputValue", "Extract input value with type from action.");
}
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override
{
return NSLOCTEXT("K2Node", "GetInputValue", "Get Input Value");
}
virtual FText GetMenuCategory() const override { return NSLOCTEXT("K2Node", "InputCategory", "Enhanced Input"); }
//~End of UK2Node interface
private:
void RefreshOutputPinType();
UEdGraphPin* GetActionValuePin() const { return FindPinChecked(TEXT("ActionValue")); }
UEdGraphPin* GetActionPin() const { return FindPinChecked(TEXT("Action")); }
UEdGraphPin* GetOutputPin() const { return FindPinChecked(TEXT("Value")); }
};
.cpp файл
#include "K2/K2Node_GetInputValue.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "InputAction.h"
#include "InputActionValue.h"
#include "KismetCompiler.h"
#include "K2Node_CallFunction.h"
#include "EnhancedInputLibrary.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(K2Node_GetInputValue)
#define LOCTEXT_NAMESPACE "K2Node"
void UK2Node_GetInputValue::PostReconstructNode()
{
Super::PostReconstructNode();
RefreshOutputPinType();
}
void UK2Node_GetInputValue::PinDefaultValueChanged(UEdGraphPin* ChangedPin)
{
if (ChangedPin == GetActionPin())
{
RefreshOutputPinType();
}
}
void UK2Node_GetInputValue::AllocateDefaultPins()
{
/** Creates input pins for ActionValue and Action, and an output pin for Value. */
// Action value pin
UEdGraphPin* ActionValuePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, FInputActionValue::StaticStruct(),
TEXT("ActionValue"));
ActionValuePin->PinToolTip = TEXT("Value received from the input system for the specified action.");
// Action object pin
UEdGraphPin* ActionPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UInputAction::StaticClass(),
TEXT("Action"));
ActionPin->PinToolTip = TEXT(
"Input action to extract the expected value type from (used to determine output type).");
// Output action value type pin
UEdGraphPin* ValuePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, TEXT("Value"));
ValuePin->PinToolTip = TEXT(
"The extracted value of the input action, matching the expected type (bool, float, or Vector2D).");
Super::AllocateDefaultPins();
}
void UK2Node_GetInputValue::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
/** Registers this node in the blueprint action database. */
if (!ActionRegistrar.IsOpenForRegistration(GetClass())) return;
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
check(NodeSpawner != nullptr);
NodeSpawner->NodeClass = GetClass();
ActionRegistrar.AddBlueprintAction(GetClass(), NodeSpawner);
}
void UK2Node_GetInputValue::RefreshOutputPinType()
{
/** Updates the output pin type based on the selected Action's ValueType. */
UEdGraphPin* OutputPin = GetOutputPin();
if (!OutputPin) return;
OutputPin->Modify();
// Resets pin type before updating
OutputPin->PinType = FEdGraphPinType();
const UEdGraphPin* ActionPin = GetActionPin();
if (!ActionPin || !ActionPin->DefaultObject)
{
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
return;
}
const UInputAction* InputAction = Cast<UInputAction>(ActionPin->DefaultObject);
if (!InputAction) return;
switch (InputAction->ValueType)
{
case EInputActionValueType::Boolean:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
break;
case EInputActionValueType::Axis1D:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Real;
OutputPin->PinType.PinSubCategory = TEXT("double");
break;
case EInputActionValueType::Axis2D:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
OutputPin->PinType.PinSubCategoryObject = TBaseStructure<FVector2D>::Get();
break;
case EInputActionValueType::Axis3D:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
OutputPin->PinType.PinSubCategoryObject = TBaseStructure<FVector>::Get();
break;
default:
OutputPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
break;
}
// Notifies the system about pin type changes
GetSchema()->ForceVisualizationCacheClear();
GetGraph()->NotifyGraphChanged();
}
void UK2Node_GetInputValue::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
/** Expands the node into a function call from UEnhancedInputLibrary. */
Super::ExpandNode(CompilerContext, SourceGraph);
UEdGraphPin* ActionValuePin = GetActionValuePin();
const UEdGraphPin* ActionPin = GetActionPin();
UEdGraphPin* OutputPin = GetOutputPin();
if (!ActionValuePin || !OutputPin)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("MissingPins", "GetInputValue: Missing pins").ToString(), this);
return;
}
if (!ActionPin || !ActionPin->DefaultObject)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("InvalidInputAction", "GetInputValue: Action pin is invalid or not set").ToString(), this);
return;
}
const UInputAction* InputAction = Cast<UInputAction>(ActionPin->DefaultObject);
if (!InputAction)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("InvalidInputAction", "GetInputValue: Action pin does not contain a valid InputAction").ToString(),
this);
return;
}
// Determines which function to call based on ValueType
FName FunctionName;
FName ActionValueName = TEXT("InValue");
switch (InputAction->ValueType)
{
case EInputActionValueType::Boolean:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToBool);
break;
case EInputActionValueType::Axis1D:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToAxis1D);
break;
case EInputActionValueType::Axis2D:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToAxis2D);
break;
case EInputActionValueType::Axis3D:
FunctionName = GET_FUNCTION_NAME_CHECKED(UEnhancedInputLibrary, Conv_InputActionValueToAxis3D);
ActionValueName = TEXT("ActionValue");
break;
default:
CompilerContext.MessageLog.Error(
*LOCTEXT("UnsupportedType", "GetInputValue: Unsupported Action Value Type").ToString(), this);
return;
}
if (OutputPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("WildcardError", "GetInputValue: Output pin type is still Wildcard!").ToString(), this);
return;
}
UE_LOG(LogTemp, Warning, TEXT("OutputPin Type: %s"), *OutputPin->PinType.PinCategory.ToString());
// Creates a CallFunction node for the selected function
UK2Node_CallFunction* GetValueNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
if (!GetValueNode)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("NodeSpawnError", "GetInputValue: Failed to create intermediate function node").ToString(), this);
return;
}
GetValueNode->FunctionReference.SetExternalMember(FunctionName, UEnhancedInputLibrary::StaticClass());
GetValueNode->AllocateDefaultPins();
// Ensures the function has the correct input pin
UEdGraphPin* InValuePin = GetValueNode->FindPin(ActionValueName);
if (!InValuePin)
{
CompilerContext.MessageLog.Error(
*LOCTEXT("MissingInputPin", "GetInputValue: Could not find expected input pin on GetValueNode").ToString(),
this);
return;
}
// Moves links from ActionValuePin -> InValuePin
CompilerContext.MovePinLinksToIntermediate(*ActionValuePin, *InValuePin);
// Moves links from OutputPin -> GetValueNode output
CompilerContext.MovePinLinksToIntermediate(*OutputPin, *GetValueNode->GetReturnValuePin());
// Breaks all links on this node since it's no longer needed
BreakAllNodeLinks();
}
#undef LOCTEXT_NAMESPACE
.build.cs файл
using UnrealBuildTool;
public class MyModuleUncooked : ModuleRules
{
public MyModuleUncooked(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"BlueprintGraph",
"EnhancedInput"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"KismetCompiler",
"UnrealEd"
}
);
}
}
Итог
Создание кастомной K2 ноды в Unreal Engine — не такая уж тяжелая задача, но очень много мелочей, которые надо учитывать. В этой статье мы прошли весь путь: от идеи до рабочей GetInputValue
, которая умеет подхватывать InputAction
и возвращать значение нужного типа, не заставляя самим возиться с конвертацией.



Благодаря классу UK2Node можно серьезно расширить функционал блупринтов, однако придется поработать.