UE4 K2Node笔记


1、说明:

以我自己写的一个Json解析插件为例:

GetChildNodeValue为一个蓝图节点,实现了Value这个输出引脚类型随着ChildJsonType这个枚举的改变而改变

其原理为当枚举类型改变的时候,蓝图通过我们在GetChildNodeValue节点内写的代码,刷新这个节点并改变指定的引脚,当我们点下蓝图编译的那一刻,蓝图会把这个节点替换为其他事先已经存在的函数

换言之,自定义实现的K2Node在蓝图内只是作为别的函数的一个跳板或者说wrapper,在蓝图编译时,蓝图会去查找真正的实现函数,如果找不到则编译失败

 

2、具体实现:

因为K2Node只是作为一个跳板,它并不会参与项目的打包,所以我们在插件内添加一个EditorNoly的模块,把K2Node相关的代码写到这个模块中

在插件的.uplugin文件内添加内容:

"Modules": [
		{
			"Name": "JsonParseEditor",
			"Type": "Editor", // 426或以上需要改为UncookedOnly
			"LoadingPhase": "PostEngineInit"
		}
	]

添加一个继承UK2Node的头文件:

UCLASS()
class JSONPARSEEDITOR_API UK2Node_JsonParse : public UK2Node
{
	GENERATED_BODY()

public:

	//创建默认显示的引脚
	virtual void AllocateDefaultPins() override;
	//当鼠标放到节点上时的提示
	virtual FText GetTooltipText() const override { return FText::FromString(TEXT("Get Child Json Node Value")); }
	//节点在蓝图内的名字
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override { return FText::FromString(TEXT("Get Child Node Value")); } 
	//节点在搜索时的分类
	virtual FText GetMenuCategory() const { return FText::FromString(TEXT("Json|JsonParser")); } 
	//初始化时
	virtual void ReallocatePinsDuringReconstruction(TArray<UEdGraphPin*>& OldPins) override; 
	//把这个节点注册到蓝图节点列表中
	virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
	//当蓝图编译时调用
	virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; 
	//当连接引脚的连线改变时
	virtual void PinConnectionListChanged(UEdGraphPin* Pin) override; 
	//当引脚的值改变时
	virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override;

	UEdGraphPin* GetThenPin() const;

protected:

	void OnEnumPinChanged(UEdGraphPin* Pin);

	UEdGraphPin* GetJsonValuePin() const;
	UEdGraphPin* GetJsonTypePin() const;
	UEdGraphPin* GetJsonKeyPin() const;
	UEdGraphPin* GetIsSucceedPin() const;
	UEdGraphPin* GetReturnValuePin() const;

private:

	UEdGraphPin* GetJsonTypePin(TArray<UEdGraphPin*>* OldPins);
};

 

在cpp文件中:
创建默认显示引脚的AllocateDefaultPins:

void UK2Node_JsonParse::AllocateDefaultPins()
{
	Super::AllocateDefaultPins();

	//创建输入引脚与输出引脚
	CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);

	//JsonStruct引脚
	UScriptStruct* JsonValueStruct = TBaseStructure<FJsonValueContent>::Get();
	UEdGraphPin* JsonValuePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, JsonValueStruct, FK2Node_JsonParsePinName::JsonValuePinName);

	//指定输入Json类型的Enum引脚,默认为None
	UEnum* const JsonTypeEnum = FindObjectChecked<UEnum>(ANY_PACKAGE, TEXT("EJsonValueTypeBP"), true);
	UEdGraphPin* const JsonTypePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, JsonTypeEnum, FK2Node_JsonParsePinName::JsonTypePinName);
	JsonTypePin->DefaultValue = JsonTypeEnum->GetNameStringByValue(static_cast<int>(EJsonValueTypeBP::NONE));

	//需要取的Json子节点的Key
	UEdGraphPin* JsonKeyPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FK2Node_JsonParsePinName::JsonKeyPinName);

	//是否取值成功的引脚
	UEdGraphPin* IsSucceedPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Boolean, FK2Node_JsonParsePinName::bIsSucceedPinName);

	//输出子节点Value的引脚,默认为泛型
	UEdGraphPin* ReturnValuePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, FK2Node_JsonParsePinName::ReturnValuePinName);
}

此处创建了当节点从Graph中创建出来时节点各引脚的默认状态

 

当蓝图编译时,执行的ExpandNode:

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

	static const FName JsonValueParamName(TEXT("JsonValue"));
	static const FName JsonKeyParamName(TEXT("Key"));
	static const FName JsonReturnValueName(TEXT("FoundValue"));

	UEdGraphPin* ExecPin = GetExecPin();
	UEdGraphPin* ThenPin = GetThenPin();

	UEdGraphPin* JsonValuePin = GetJsonValuePin();
	UEdGraphPin* JsonTypePin = GetJsonTypePin();
	UEdGraphPin* JsonKeyPin = GetJsonKeyPin();
	UEdGraphPin* IsSucceedPin = GetIsSucceedPin();
	UEdGraphPin* ReturnValuePin = GetReturnValuePin();

	if (ExecPin && ThenPin)
	{
		FString TypeStr = JsonTypePin->GetDefaultAsString();
		FName MyFUnctionName = FName();
		UK2Node_CallFunction* CallFuncNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph); // 创建节点
		//通过引擎提供的宏找到对应函数的FName
		if (TypeStr == TEXT("NONE")) 
		{
			//当类型指定为None时,我们需要这个节点编译失败
		}
		else if (TypeStr == TEXT("NODE"))
		{
			MyFUnctionName = GET_FUNCTION_NAME_CHECKED(UJsonParseHelper, GetChildJsonNode);
		}
		else if (TypeStr == TEXT("STRING"))
		{
			MyFUnctionName = GET_FUNCTION_NAME_CHECKED(UJsonParseHelper, GetStringValue);
		}
		else if (TypeStr == TEXT("BOOL"))
		{
			MyFUnctionName = GET_FUNCTION_NAME_CHECKED(UJsonParseHelper, GetBoolValue);
		}
		else if (TypeStr == TEXT("NUMBER"))
		{
			MyFUnctionName = GET_FUNCTION_NAME_CHECKED(UJsonParseHelper, GetNumberValue);
		}
		
		// 把各引脚绑到对应的函数上
		if (!MyFUnctionName.IsNone())
		{
			CallFuncNode->FunctionReference.SetExternalMember(MyFunctionName, UJsonParseHelper::StaticClass()); // 为这个节点设定执行函数
			CallFuncNode->AllocateDefaultPins(); // 初始化节点的所有引脚

			// 获取节点的所有引脚
			UEdGraphPin* CallJsonValue = CallFuncNode->FindPinChecked(JsonValueParamName);
			UEdGraphPin* CallJsonKey = CallFuncNode->FindPinChecked(JsonKeyParamName);
			UEdGraphPin* CallIsSucceed = CallFuncNode->GetReturnValuePin();
			UEdGraphPin* CallReturnValue = CallFuncNode->FindPinChecked(JsonReturnValueName);

			// 把K2Node的引脚移动到被执行函数上
			CompilerContext.MovePinLinksToIntermediate(*ExecPin, *CallFuncNode->GetExecPin());

			CompilerContext.MovePinLinksToIntermediate(*JsonValuePin, *CallJsonValue);
			CompilerContext.MovePinLinksToIntermediate(*JsonKeyPin, *CallJsonKey);
			CompilerContext.MovePinLinksToIntermediate(*IsSucceedPin, *CallIsSucceed);
			CompilerContext.MovePinLinksToIntermediate(*ReturnValuePin, *CallReturnValue);

			CompilerContext.MovePinLinksToIntermediate(*ThenPin, *CallFuncNode->GetThenPin());
		}
	}

	BreakAllNodeLinks();
}

其中UJsonParseHelper为存放真正执行函数的蓝图函数库,GetChildJsonNode等为真正执行的函数名
JsonTypeEnum默认需要为NONE,并且当其为NONE时需要让其编译失败的原因是因为K2Node是在编译阶段就改变了这个节点里真正执行的函数,并不能处理运行阶段时传入的Enum节点,如果ChildJsonType这个引脚的值从外部传入的话,它在K2Node的编译阶段则会变成默认值的NONE,从而让其编译失败。有点类似于c++的宏、模板之类的。

 

当引脚的值改变时:

void UK2Node_JsonParse::OnEnumPinChanged(UEdGraphPin* Pin)
{
	//查找当前Value这个引脚是否有连接,如果有则暂时保存下来
	UEdGraphPin* ReturnValuePin = GetReturnValuePin();
	TArray<UEdGraphPin*> LinkedPins;
	if (ReturnValuePin)
	{
		LinkedPins = ReturnValuePin->LinkedTo;
		ReturnValuePin->BreakAllPinLinks(true);
		RemovePin(ReturnValuePin);
	}

	//创建ChildJsonType对应的Value引脚
	FString TypeStr = Pin->GetDefaultAsString();
	if (TypeStr == TEXT("NONE") || Pin->LinkedTo.Num() > 0)
	{
		CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, FK2Node_JsonParsePinName::ReturnValuePinName);
	}
	else if (TypeStr == TEXT("NODE"))
	{
		UScriptStruct* JsonValueStruct = TBaseStructure<FJsonValueContent>::Get();
		CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Struct, JsonValueStruct, FK2Node_JsonParsePinName::ReturnValuePinName);
	}
	else if (TypeStr == TEXT("STRING"))
	{
		CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_String, FK2Node_JsonParsePinName::ReturnValuePinName);
	}
	else if (TypeStr == TEXT("BOOL"))
	{
		CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Boolean, FK2Node_JsonParsePinName::ReturnValuePinName);
	}
	else if (TypeStr == TEXT("NUMBER"))
	{
		CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Float, FK2Node_JsonParsePinName::ReturnValuePinName);
	}

	//尝试把之前保存的连接重新接上
	ReturnValuePin = GetReturnValuePin();
	if (ReturnValuePin && LinkedPins.Num() > 0)
	{
		const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

		for (UEdGraphPin* LinkedPin : LinkedPins)
		{
			K2Schema->TryCreateConnection(ReturnValuePin, LinkedPin);
		}
	}

	GetGraph()->NotifyGraphChanged();
	FBlueprintEditorUtils::MarkBlueprintAsModified(GetBlueprint());
}

 

插件的完整代码在我的github:https://github.com/g23p/JsonParsePlugin_UE4

 

----------------------------------------------------------------------------------------------------------------------------

2023.8.28更新:

ue5之后,蓝图支持了double,而ue的json api获取number的方法本身就是支持double的,只是如果用float去接数据的话会经过一个强转从而丢精度

既然ue5的蓝图已经支持double了,那自然也可以让从api获取的double数据直接传递到蓝图

1、遇到的问题:

由于是适配ue5,分开ue4和ue5两份插件来写固然简单,但是本着“如无必要,勿增实体”的原则,还是希望在只有一个插件的情况下,能同时通过ue4和ue5的编译

K2Node作为一个跳板,在蓝图里是需要真正被执行的函数的,所以被执行的函数需要被标记为UFUNCTION(BlueprintCallable)以供蓝图调用

下面为改写之前的获取Number的函数的声明:

UFUNCTION(BlueprintCallable, Category = "Json|JsonParser", meta = (BlueprintInternalUseOnly = "true"))
	static bool GetNumberValue(const FJsonValueContent& JsonValue, const FString& Key, float& FoundValue);

要同时适配ue4和ue5,第一时间想到的便是ue自己提供的一个宏:

#if ENGINE_MAJOR_VERSION == 4
#elif ENGINE_MAJOR_VERSION == 5
#endif

但是由于UFUNCTION是由UHT处理的,而UHT的执行在c++的宏展开之前,所以不能写成类似于下面这种样子的代码:

#if ENGINE_MAJOR_VERSION == 4
UFUNCTION(BlueprintCallable, Category = "Json|JsonParser", meta = (BlueprintInternalUseOnly = "true")) // 无法通过编译
	static bool GetNumberValue(const FJsonValueContent& JsonValue, const FString& Key, float& FoundValue);
#elif ENGINE_MAJOR_VERSION == 5
UFUNCTION(BlueprintCallable, Category = "Json|JsonParser", meta = (BlueprintInternalUseOnly = "true")) // 无法通过编译
	static bool GetNumberValue(const FJsonValueContent& JsonValue, const FString& Key, double& FoundValue);
#endif

那除了用宏之外还有什么办法是能让这个函数的入参在ue4时是float,在ue5时是double的吗,想了想之后,感觉用泛型应该是可行的

2、解决过程:

首先改写获取Number的函数,使其输出的引脚变成泛型引脚,具体做法可以参考我的这篇博文:UE4 CustomThunk笔记

UFUNCTION(BlueprintCallable, CustomThunk, Category = "Json|JsonParser", meta = (BlueprintInternalUseOnly = "true", CustomStructureParam = "FoundValue"))
	static bool GetNumberValue(const FJsonValueContent& JsonValue, const FString& Key, int32& FoundValue);

DECLARE_FUNCTION在这里就省略了

再声明一个处理GetNumberValue的参数从蓝图传入后的函数:

static bool Generic_GetNumberValue(const FJsonValueContent& JsonValue, const FString& Key, void* ValueAddr, FProperty* ValueType);

其中ValueAddr为存放输出值这个变量的指针,ValueType为输出值的类型

接着在cpp文件中实现这个函数:

bool UJsonParseHelper::Generic_GetNumberValue(const FJsonValueContent& JsonValue, const FString& Key, void* ValueAddr, FProperty* ValueType)
{
#if ENGINE_MAJOR_VERSION == 4
	FFloatProperty* FloatProperty = CastField<FFloatProperty>(ValueType);
	float* OutPtr = FloatProperty->GetPropertyValuePtr(ValueAddr);
#elif ENGINE_MAJOR_VERSION == 5
	FDoubleProperty* DoubleProperty = CastField<FDoubleProperty>(ValueType);
	double* OutPtr = DoubleProperty->GetPropertyValuePtr(ValueAddr);
#endif

	if (JsonValue.ValueType == FJsonValueType::JVT_OBJECT)
	{
		double OutNumber;
		if (JsonValue.Value->AsObject()->TryGetNumberField(Key, OutNumber))
		{
			*OutPtr = OutNumber;
			return true;
		}
	}
 
	...
}

通过泛型获取到输出值的指针和变量类型,再通过ENGINE_MAJOR_VERSION宏去区分ue4和ue5从而区分float和double,便绕过了普通的c++宏无法和UFUNCTION宏同时使用的问题


执行函数的问题解决了,我们先进入蓝图,调出GetChildNodeValue,并把ChildJsonType这个枚举选为Number,然后点击编译,便可以看到,蓝图编译失败了

编译的报错为:

在vs里全局搜索报错的关键字,可以在KismetCompiler.cpp中找到这段输出代码:

if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard)
{
	// Wildcard pins should never be seen by the compiler; they should always be forced into a particular type by wiring.
	MessageLog.Error(*LOCTEXT("UndeterminedPinType_Error", "The type of @@ is undetermined.  Connect something to @@ to imply a specific type.").ToString(), Pin, Pin->GetOwningNodeUnchecked());				
}

看判断逻辑即可得知,蓝图编译错误的原因是因为我们给GetNumberValue改的泛型节点

在GetChildNodeValue节点中,输出结果的引脚(Value引脚)已经变成了double(ue5下,ue4下便是float),而GetNumberValue函数输出结果的引脚默认还是泛型,两者不匹配所以导致了编译失败

那么接下来要做的便是让GetNumberValue这个节点的输出引脚在ue4下自动变成float,在ue5下自动变成double

因为K2Node在这里作为一个跳板,那么关键就在于当蓝图编译时,被调用的ExpandNode函数

观察ExpandNode函数,找到引脚相关的代码段

if (!MyFUnctionName.IsNone())
{
	CallFuncNode->FunctionReference.SetExternalMember(MyFunctionName, UJsonParseHelper::StaticClass()); // 为这个节点设定执行函数
	CallFuncNode->AllocateDefaultPins(); // 初始化节点的所有引脚

	// 获取节点的所有引脚
	UEdGraphPin* CallJsonValue = CallFuncNode->FindPinChecked(JsonValueParamName);
	UEdGraphPin* CallJsonKey = CallFuncNode->FindPinChecked(JsonKeyParamName);
	UEdGraphPin* CallIsSucceed = CallFuncNode->GetReturnValuePin();
	UEdGraphPin* CallReturnValue = CallFuncNode->FindPinChecked(JsonReturnValueName);

	// 把K2Node的引脚移动到被执行函数上
	CompilerContext.MovePinLinksToIntermediate(*ExecPin, *CallFuncNode->GetExecPin());

	CompilerContext.MovePinLinksToIntermediate(*JsonValuePin, *CallJsonValue);
	CompilerContext.MovePinLinksToIntermediate(*JsonKeyPin, *CallJsonKey);
	CompilerContext.MovePinLinksToIntermediate(*IsSucceedPin, *CallIsSucceed);
	CompilerContext.MovePinLinksToIntermediate(*ReturnValuePin, *CallReturnValue);

	CompilerContext.MovePinLinksToIntermediate(*ThenPin, *CallFuncNode->GetThenPin());
}

找到输出结果的引脚,让这个引脚在K2Node要输出Number值的时候变成对应的类型

修改后的结果为:

if (!MyFunctionName.IsNone())
{
	CallFuncNode->FunctionReference.SetExternalMember(MyFunctionName, UJsonParseHelper::StaticClass());
	CallFuncNode->AllocateDefaultPins();

	UEdGraphPin* CallJsonValue = CallFuncNode->FindPinChecked(JsonValueParamName);
	UEdGraphPin* CallJsonKey = CallFuncNode->FindPinChecked(JsonKeyParamName);
	UEdGraphPin* CallIsSucceed = CallFuncNode->GetReturnValuePin();
	UEdGraphPin* CallReturnValue = CallFuncNode->FindPinChecked(JsonReturnValueName);
	if(TypeStr == TEXT("NUMBER"))
	{
#if ENGINE_MAJOR_VERSION == 4
	CallReturnValue->PinType.PinCategory = UEdGraphSchema_K2::PC_Float;
#elif ENGINE_MAJOR_VERSION == 5
	CallReturnValue->PinType.PinCategory = UEdGraphSchema_K2::PC_Real;
	CallReturnValue->PinType.PinSubCategory = UEdGraphSchema_K2::PC_Double;
#endif
	}

	CompilerContext.MovePinLinksToIntermediate(*ExecPin, *CallFuncNode->GetExecPin());

	CompilerContext.MovePinLinksToIntermediate(*JsonValuePin, *CallJsonValue);
	CompilerContext.MovePinLinksToIntermediate(*JsonKeyPin, *CallJsonKey);
	CompilerContext.MovePinLinksToIntermediate(*IsSucceedPin, *CallIsSucceed);
	CompilerContext.MovePinLinksToIntermediate(*ReturnValuePin, *CallReturnValue);

	CompilerContext.MovePinLinksToIntermediate(*ThenPin, *CallFuncNode->GetThenPin());
}

至此,这个插件就实现了一份代码同时能在ue4和ue5下编译成功并运行了


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM