深入Unreal蓝图开发:实现蓝图模板函数
Unreal的藍圖和C++一樣,也是一種靜態類型的編程語言,它又不像其他靜態類型語言那樣支持模板,有些時候就覺得很不方便。思考了一下這個問題。想要藍圖節點支持任意類型的參數,主要分為兩種情況:
- UObject派生類對象:那很簡單了,使用基類指針作為參數就好,在C++里面可以Cast,或者取得對象的UClass,就可以根據反射信息做很多事了;
- Struct類型,或者TArray<MyStruct>類型:這個是本文的重點。
其實說藍圖完全不支持“模板”也是不對的,引擎中其實已經有很多能夠處理任意Struct或者TArray<MyStruct>類型的節點了!官方文檔中把這種情況叫做參數“Wildcard”(通配符)。感謝Unreal開源,通過閱讀源代碼,加上一點實驗,就能夠搞清楚具體實現方法和背后的細節。
下面主要探討使用UFUNCTION的CustomThunk描述符,實現自定義的Thunk函數;然后通過指定meta的CustomStructureParam和ArrayParm參數,來實現參數類型“通配符”!這中間的難點是:需要明確藍圖Stack的處理方式。Demo如下圖所示:
在上圖的Demo中:
完整的Demo工程可以從我的GitHub下載:https://github.com/neil3d/UnrealCookBook/tree/master/MyBlueprintNode
實現藍圖功能節點的幾種方式
在Unreal開發中可以使用C++對藍圖進行擴展,生成Unreal藍圖節點最方便的方法就是寫一個UFUNCTION,無論是定義在UBlueprintFunctionLibrary派生類里面的static函數,還是定義在UObject、AActor派生類里面的類成員函數,只要加上UFUNCTION宏修飾,并在宏里面添加BlueprintCallable標識符,就可以自動完成藍圖編輯節點、藍圖節點執行調用的整個過程。不過,由于C++和藍圖都屬于“靜態類型”編程語言,這種形式編寫的藍圖節點,所有的輸入、輸出參數的類型都必須是固定的,這樣引擎才能自動處理藍圖虛擬機的棧。
先來總結一下C++實現藍圖節點的幾種方式:
使用第3種方式,結合UFUNCTION的其它meta標識符,可以實現參數類型的“通配符”,就可以實現模板函數,也就是輸入、輸出參數可以處理多種數據類型,類似C++的泛型。這些meta標識符主要有:
引擎源代碼中,這種編程方式的典型的例子有:
- 藍圖編輯器中的“Utilities”->“Array”菜單中的所有節點,他們可以處理任意的UStruct類型的數組。這些節點對應的源代碼是:class UKismetArrayLibrary
- class UDataTableFunctionLibrary::GetDataTableRowFromName(UDataTable* Table, FName RowName, FTableRowBase& OutRow)
詳見官方文檔:UFunctions
CustomThunk函數
如果在UFUNCTION宏里面指定了CustomThunk,那么UHT就不會自動生成這個函數的“thunk”,而需要開發者自己實現。這里的“thunk”是什么呢?我們看個例子。
我們來做個最簡單的小試驗,在工程中建立一個Blueprint Function Library,添加一個簡單的UFUNCTION:
#pragma once#include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "MyBlueprintFunctionLibrary.generated.h"UCLASS() class MYBLUEPRINTNODES_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary {GENERATED_BODY() public:UFUNCTION(BlueprintCallable)static int Sum(int a, int b); };然后在對應的cpp文件中,使用C++實現這個函數:
#include "MyBlueprintFunctionLibrary.h"int UMyBlueprintFunctionLibrary::Sum(int a, int b) {return a + b; }項目build一下,然后你就可以在“Intermediate”目錄找到這個"MyBlueprintFunctionLibrary.generated.h"文件。在這個文件里面,你可以找到這樣一段代碼:
DECLARE_FUNCTION(execSum) \{ \P_GET_PROPERTY(UIntProperty,Z_Param_a); \P_GET_PROPERTY(UIntProperty,Z_Param_b); \P_FINISH; \P_NATIVE_BEGIN; \*(int32*)Z_Param__Result=UMyBlueprintFunctionLibrary::Sum(Z_Param_a,Z_Param_b); \P_NATIVE_END; \}這段代碼就是藍圖函數節點的thunk了!這段代碼做了這樣幾件事:
“thunk”函數是一個包裝,它完成的核心任務就是處理藍圖虛擬機的Stack,然后調用我們使用C++實現的函數。
我們還可以看一下UHT幫我們生成的另外一個文件:MyBlueprintFunctionLibrary.gen.cpp,在其中有這樣一段代碼:
void UMyBlueprintFunctionLibrary::StaticRegisterNativesUMyBlueprintFunctionLibrary(){UClass* Class = UMyBlueprintFunctionLibrary::StaticClass();static const FNameNativePtrPair Funcs[] = {{ "Sum", &UMyBlueprintFunctionLibrary::execSum },};FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));}這段代碼把剛才"MyBlueprintFunctionLibrary.generated.h"中聲明的excSum函數注冊到了UMyBlueprintFunctionLibrary::StaticClass()這個UClass對象之中,并指定它的名字為“Sum”,也就是我們原始C++代碼中聲明的函數名,也是在藍圖編輯器中顯示的名字。
看清楚了什么是“thunk函數”,“CustomThunk函數”也就不言自明了。在UFUNCTION中指定“CustomThunk”標識符,就是告訴UHT,不要在.generated.h中生成DECLARE_FUNCTION那部分代碼,這部分代碼改由手寫。為啥要拋棄自動生成,而手寫呢?回到本文主題:要實現“參數類型通配符”(或者叫做“藍圖模板節點”),就必須手寫thunk!
藍圖Stack探索
要實現自己的thunk函數,核心任務就是“準確的處理藍圖虛擬機的棧”,可惜的是官方并沒有這方面的文檔!下面我就把自己的一些探索記錄下來,請大家指正。
以上面的int Sum(int a, int b)函數為例,thunk函數使用P_GET_PROPERTY宏從Stack取值,這個宏P_GET_PROPERTY(UIntProperty,Z_Param_a)展開之后的代碼如下所示:
UIntProperty::TCppType Z_Param_a = UIntProperty::GetDefaultPropertyValue();Stack.StepCompiledIn<UIntProperty>(&Z_Param_a);其中UIntProperty派生自TProperty_Numeric<int32>,UIntProperty::TCppType就是“int32”無疑!
我們還需要處理TArray<MyStruct>這樣的數據,所以我們重點要看一下這種參數類型的棧處理。
假設我們有一個C++的UStruct:
類似這樣一個UFUNCTION:
UFUNCTION(BlueprintCallable) static void PrintMyStructArray(const TArray<FMyStruct>& MyStructArray);則在.h中的thunk函數為:
DECLARE_FUNCTION(execPrintMyStructArray) \{ \P_GET_TARRAY_REF(FMyStruct,Z_Param_Out_MyStructArray); \P_FINISH; \P_NATIVE_BEGIN; \UMyBlueprintFunctionLibrary::PrintMyStructArray(Z_Param_Out_MyStructArray); \P_NATIVE_END; \} \其中P_GET_TARRAY_REF(FMyStruct,Z_Param_Out_MyStructArray);這個宏展開之后的代碼為:
PARAM_PASSED_BY_REF(Z_Param_Out_MyStructArray, UArrayProperty, TArray<FMyStruct>)最終展開為:
TArray<FMyStruct> Z_Param_Out_MyStructArrayTemp; TArray<FMyStruct>& Z_Param_Out_MyStructArray = Stack.StepCompiledInRef<UArrayProperty, TArray<FMyStruct> >(&Z_Param_Out_MyStructArrayTemp);綜合上面兩個例子,我們發現核心操作都是調用template<class TProperty> void FFrame::StepCompiledIn(void*const Result)這個模板函數。通過跟蹤這個函數的執行,發現它實際調用了UObject::execInstanceVariable()函數。
再結合引擎中CustomThunk函數的實現源碼,可以得出這樣的結論:
通過調用Stack.StepCompiledIn()函數,就可以更新藍圖虛擬機的棧頂指針;
Stack.MostRecentPropertyAddress和Stack.MostRecentProperty這兩個變量,就是當前參數值的內存地址和反射信息。
有了具體變量的內存地址和類型的反射信息,就足夠做很多事了。下面我們就開始實踐。
實踐1:接受任意UStruct類型參數
下面我們就看一下文章開頭的這張圖里面的藍圖節點“Show Struct Fields”是如何接受任意類型UStruct參數的。
先上代碼, BlueprintWildcardLibrary.h
USTRUCT(BlueprintInternalUseOnly) struct FDummyStruct {GENERATED_USTRUCT_BODY()};UCLASS() class UNREALCOOKBOOK_API UBlueprintWildcardLibrary : public UBlueprintFunctionLibrary {GENERATED_BODY()public:UFUNCTION(BlueprintCallable, CustomThunk, Category = "MyDemo", meta = (CustomStructureParam = "CustomStruct"))static void ShowStructFields(const FDummyStruct& CustomStruct);static void Generic_ShowStructFields(const void* StructAddr, const UStructProperty* StructProperty);DECLARE_FUNCTION(execShowStructFields) {Stack.MostRecentPropertyAddress = nullptr;Stack.MostRecentProperty = nullptr;Stack.StepCompiledIn<UStructProperty>(NULL);void* StructAddr = Stack.MostRecentPropertyAddress;UStructProperty* StructProperty = Cast<UStructProperty>(Stack.MostRecentProperty);P_FINISH;P_NATIVE_BEGIN;Generic_ShowStructFields(StructAddr, StructProperty);P_NATIVE_END;} };BlueprintWildcardLibrary.cpp
#include "BlueprintWildcardLibrary.h" #include "Engine/Engine.h"void UBlueprintWildcardLibrary::Generic_ShowStructFields(const void* StructAddr, const UStructProperty* StructProperty) {UScriptStruct* Struct = StructProperty->Struct;for (TFieldIterator<UProperty> iter(Struct); iter; ++iter) {FScreenMessageString NewMessage;NewMessage.CurrentTimeDisplayed = 0.0f;NewMessage.Key = INDEX_NONE;NewMessage.DisplayColor = FColor::Blue;NewMessage.TimeToDisplay = 5;NewMessage.ScreenMessage = FString::Printf(TEXT("Property: [%s].[%s]"),*(Struct->GetName()),*(iter->GetName()));NewMessage.TextScale = FVector2D::UnitVector;GEngine->PriorityScreenMessages.Insert(NewMessage, 0);} }解釋一下這段代碼:
實踐2:對數組中的Struct的數值型求平均
下面我們再來一下文章開頭的這張圖里面的“Array Numeric Field Average”藍圖節點是如何通過“CustomThunk”函數來實現的。
參照引擎源代碼,我定義了這樣一個宏,用來從棧上取出泛型數組參數,并正確的移動棧指針:
#define P_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty) Stack.MostRecentProperty = nullptr;\Stack.StepCompiledIn<UArrayProperty>(NULL);\void* ArrayAddr = Stack.MostRecentPropertyAddress;\UArrayProperty* ArrayProperty = Cast<UArrayProperty>(Stack.MostRecentProperty);\if (!ArrayProperty) { Stack.bArrayContextFailed = true; return; }通過這個宏,可以得到兩個局部變量:
- void* ArrayAddr: 數組的起始內存地址;
- UArrayProperty* ArrayProperty: 數組的反射信息,ArrayProperty->Inner就是數組成員對應的類型了;
有了這個宏,我們就可以很方便的寫出thunk函數了:
DECLARE_FUNCTION(execArray_NumericPropertyAverage) {// get TargetArrayP_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty);// get PropertyNameP_GET_PROPERTY(UNameProperty, PropertyName);P_FINISH;P_NATIVE_BEGIN;*(float*)RESULT_PARAM = GenericArray_NumericPropertyAverage(ArrayAddr, ArrayProperty, PropertyName);P_NATIVE_END;}經過以上的準備,我們就已經可以正確的處理“泛型數組”了。下一步就是對這個數組中指定的數“值類型成員變量”求均值了,這主要依靠Unreal的反射信息,一步步抽絲剝繭,找到數組中的每個變量即可。反射系統的使用不是本文的重點,先看完整代碼吧。
BlueprintWildcardLibrary.h
#pragma once#include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "BlueprintWildcardLibrary.generated.h"#define P_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty) Stack.MostRecentProperty = nullptr;\Stack.StepCompiledIn<UArrayProperty>(NULL);\void* ArrayAddr = Stack.MostRecentPropertyAddress;\UArrayProperty* ArrayProperty = Cast<UArrayProperty>(Stack.MostRecentProperty);\if (!ArrayProperty) { Stack.bArrayContextFailed = true; return; }UCLASS() class UNREALCOOKBOOK_API UBlueprintWildcardLibrary : public UBlueprintFunctionLibrary {GENERATED_BODY()public:UFUNCTION(BlueprintPure, CustomThunk, meta = (DisplayName = "Array Numeric Property Average", ArrayParm = "TargetArray", ArrayTypeDependentParams = "TargetArray"), Category = "MyDemo")static float Array_NumericPropertyAverage(const TArray<int32>& TargetArray, FName PropertyName);static float GenericArray_NumericPropertyAverage(const void* TargetArray, const UArrayProperty* ArrayProperty, FName ArrayPropertyName);public:DECLARE_FUNCTION(execArray_NumericPropertyAverage) {// get TargetArrayP_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty);// get PropertyNameP_GET_PROPERTY(UNameProperty, PropertyName);P_FINISH;P_NATIVE_BEGIN;*(float*)RESULT_PARAM = GenericArray_NumericPropertyAverage(ArrayAddr, ArrayProperty, PropertyName);P_NATIVE_END;} };BlueprintWildcardLibrary.cpp
#include "BlueprintWildcardLibrary.h" #include "Engine/Engine.h"float UBlueprintWildcardLibrary::Array_NumericPropertyAverage(const TArray<int32>& TargetArray, FName PropertyName) {// We should never hit these! They're stubs to avoid NoExport on the class. Call the Generic* equivalent insteadcheck(0);return 0.f; }float UBlueprintWildcardLibrary::GenericArray_NumericPropertyAverage(const void* TargetArray, const UArrayProperty* ArrayProperty, FName PropertyName) {UStructProperty* InnerProperty = Cast<UStructProperty>(ArrayProperty->Inner);if (!InnerProperty) {UE_LOG(LogTemp, Error, TEXT("Array inner property is NOT a UStruct!"));return 0.f;}UScriptStruct* Struct = InnerProperty->Struct;FString PropertyNameStr = PropertyName.ToString();UNumericProperty* NumProperty = nullptr;for (TFieldIterator<UNumericProperty> iter(Struct); iter; ++iter) {if (Struct->PropertyNameToDisplayName(iter->GetFName()) == PropertyNameStr) {NumProperty = *iter;break;}}if (!NumProperty) {UE_LOG(LogTemp, Log, TEXT("Struct property NOT numeric = [%s]"),*(PropertyName.ToString()));}FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);int Count = ArrayHelper.Num();float Sum = 0.f;if(Count <= 0)return 0.f;if (NumProperty->IsFloatingPoint())for (int i = 0; i < Count; i++) {void* ElemPtr = ArrayHelper.GetRawPtr(i);const uint8* ValuePtr = NumProperty->ContainerPtrToValuePtr<uint8>(ElemPtr);Sum += NumProperty->GetFloatingPointPropertyValue(ValuePtr);}else if (NumProperty->IsInteger()) {for (int i = 0; i < Count; i++) {void* ElemPtr = ArrayHelper.GetRawPtr(i);const uint8* ValuePtr = NumProperty->ContainerPtrToValuePtr<uint8>(ElemPtr);Sum += NumProperty->GetSignedIntPropertyValue(ValuePtr);}}// TODO: else if(enum類型)return Sum / Count; }總結
以上是生活随笔為你收集整理的深入Unreal蓝图开发:实现蓝图模板函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 栈的应用场景(高频面试题)
- 下一篇: 计算机主板 辐射,想当年单反镜头竟然还有