Делаем свою Blueprint K2 Node в Unreal Engine

от автора

Созданная в конце гайда нода.

Созданная в конце гайда нода.

Иногда надо создать функцию, которая должна быть и доступна в blueprints, и адаптироваться под входные данные. Особенно это касается wildcard.

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

K2 ноды в Unreal Engine

K2 ноды в Unreal Engine

Что такое K2 Node

ВАЖНО! Работа с нодами должна проходить в uncooked модуле.

Стоит начать с того, что UK2Node это довольно старый класс, в котором всю работу надо писать руками.

Что это означает? Вам надо самим:

  1. Добавить каждый пин.

  2. Обновить тип пина при изменении.

  3. Обновить отображаемый тип пина при изменении.

  4. Зарегистрировать ноду в контекстном меню.

  5. Перемещать/удалять/создавать соединения между пинами.

  6. Прописать название(опционально).

  7. Прописать описание(опционально).

  8. Добавить описание каждого пина(опционально).

Звучит как что-то не очень увлекательное(так и есть), но другого выбора нет. Зато открывается больше возможностей для кастомизации.


Создаем класс

Для примера, создадим ноду, которая будет принимать на вход структуру 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

Эта функция отвечает за само создание пинов. В ней мы и прописываем входные и выходные параметры. Тут мы сразу указываем их базовый тип, который и будет отображаться при начальном отображении ноды при создании в blueprint graph.

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(); }

Итак, что у нас тут есть? Мы создали 3 пина:

  1. Входной пин структуры Input Action Value.

  2. Входной пин самого Input Action.

  3. Выходной пин типа 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); }

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

Отображение функции после регистрации

Отображение функции после регистрации

Основная функция

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(); }

В этой функции мы раскрываем нашу ноду на последовательность других нод(либо даже на одну), с которыми уже и будем соединять наши пины. Да, на самом деле K2 ноды скрытно раскрываются вплоть до целой серии вызовов других. Конкретно в нашем случае мы будем вызывать одну из уже существующих BlueprintInternalUseOnly функций из плагина инпутов.

Итак, что мы делаем:

  1. Проверяем валидность наших пинов.

  2. Выбираем функцию для конвертации.

  3. Создаем ноду этой функции в графе.

  4. Перетаскиваем соединения наших пинов на пины только что размещенной ноды.

  5. Обрываем все связи нашей ноды.

Дополнительная настройка

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 можно серьезно расширить функционал блупринтов, однако придется поработать.


ссылка на оригинал статьи https://habr.com/ru/articles/899108/