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下編譯成功並運行了