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下编译成功并运行了