Иногда надо создать функцию, которая должна быть и доступна в 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
Эта функция отвечает за само создание пинов. В ней мы и прописываем входные и выходные параметры. Тут мы сразу указываем их базовый тип, который и будет отображаться при начальном отображении ноды при создании в 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 пина:
-
Входной пин структуры 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); }
Эти функции нам нужны исключительно для того, чтобы нашу ноду можно было вызвать в контекстном меню блупринта.
Основная функция
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 функций из плагина инпутов.
Итак, что мы делаем:
-
Проверяем валидность наших пинов.
-
Выбираем функцию для конвертации.
-
Создаем ноду этой функции в графе.
-
Перетаскиваем соединения наших пинов на пины только что размещенной ноды.
-
Обрываем все связи нашей ноды.
Дополнительная настройка
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/
Добавить комментарий