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