日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入Unreal蓝图开发:实现蓝图模板函数

發布時間:2023/12/20 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入Unreal蓝图开发:实现蓝图模板函数 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Unreal的藍圖和C++一樣,也是一種靜態類型的編程語言,它又不像其他靜態類型語言那樣支持模板,有些時候就覺得很不方便。思考了一下這個問題。想要藍圖節點支持任意類型的參數,主要分為兩種情況:

  • UObject派生類對象:那很簡單了,使用基類指針作為參數就好,在C++里面可以Cast,或者取得對象的UClass,就可以根據反射信息做很多事了;
  • Struct類型,或者TArray<MyStruct>類型:這個是本文的重點。

其實說藍圖完全不支持“模板”也是不對的,引擎中其實已經有很多能夠處理任意Struct或者TArray<MyStruct>類型的節點了!官方文檔中把這種情況叫做參數“Wildcard”(通配符)。感謝Unreal開源,通過閱讀源代碼,加上一點實驗,就能夠搞清楚具體實現方法和背后的細節。

下面主要探討使用UFUNCTION的CustomThunk描述符,實現自定義的Thunk函數;然后通過指定meta的CustomStructureParam和ArrayParm參數,來實現參數類型“通配符”!這中間的難點是:需要明確藍圖Stack的處理方式。Demo如下圖所示:

在上圖的Demo中:

  • 自定義了一個藍圖Struct:MyStruct
  • 使用C++實現了一個藍圖節點“Show Struct Fields”:可以接受任意UStruct的引用,具體類型可以由C++或者藍圖定義;
  • 藍圖節點“Array Numeric Field Average”:可以接受任意類型的TArray<MyStruct>,并對數組中指定的數值型字段求平均;
  • 完整的Demo工程可以從我的GitHub下載:https://github.com/neil3d/UnrealCookBook/tree/master/MyBlueprintNode

    實現藍圖功能節點的幾種方式

    在Unreal開發中可以使用C++對藍圖進行擴展,生成Unreal藍圖節點最方便的方法就是寫一個UFUNCTION,無論是定義在UBlueprintFunctionLibrary派生類里面的static函數,還是定義在UObject、AActor派生類里面的類成員函數,只要加上UFUNCTION宏修飾,并在宏里面添加BlueprintCallable標識符,就可以自動完成藍圖編輯節點、藍圖節點執行調用的整個過程。不過,由于C++和藍圖都屬于“靜態類型”編程語言,這種形式編寫的藍圖節點,所有的輸入、輸出參數的類型都必須是固定的,這樣引擎才能自動處理藍圖虛擬機的棧。

    先來總結一下C++實現藍圖節點的幾種方式:

  • UFUNCTION,上面已經說過了;
  • 實現class UK2Node的派生類,這是最強大的方式,是對藍圖節點最深入的定制開發,如果你需要動態的添加、刪除藍圖節點的針腳,就只能用這種方式了。例如我們常用的“Format Text”節點,可以根據輸入字符串中的“{index}”來動態增加輸入節點,輸入節點的類型也是動態的,這個就是通過class UK2Node_FormatText這個類來實現的;
  • 還有介于上面兩者之間的一種方式,就是在UFUNCTION中使用“CustomThunk”標識,告訴UHT(Unreal Header Tool)不要生成默認的藍圖包裝函數,而是由我們手工實現。這種方式,需要手工控制藍圖虛擬機的“棧”,但是不用處理藍圖編輯器UI部分,相對第2種來說代碼量要少很多,相對第1種來說,又多了很多控制力;
  • 另外,藍圖的“宏”–Macros,也可以實現自己的節點。
  • 使用第3種方式,結合UFUNCTION的其它meta標識符,可以實現參數類型的“通配符”,就可以實現模板函數,也就是輸入、輸出參數可以處理多種數據類型,類似C++的泛型。這些meta標識符主要有:

  • ArrayParm="Parameter1, Parameter2, ..":說明 BlueprintCallable 函數應使用一個Call Array Function節點,且列出的參數應被視為通配符數組屬性;
  • ArrayTypeDependentParams="Parameter":使用 ArrayParm 時,此說明符將指定一個參數,其將確定 ArrayParm 列表中所有參數的類型;
  • CustomStructureParam="Parameter1, Parameter2, ..":列出的參數都會被視為通配符。
  • 引擎源代碼中,這種編程方式的典型的例子有:

    • 藍圖編輯器中的“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了!這段代碼做了這樣幾件事:

  • 聲明了一個名為“execSum”的函數,函數的簽名為:void func( UObject* Context, FFrame& Stack, RESULT_DECL )
  • 使用P_GET_PROPERTY宏,從“FFrame& Stack”(也就是藍圖虛擬機的棧)中取出函數參數;
  • 調用P_FINISH宏;
  • 使用取出的這些參數調用我們實現的UMyBlueprintFunctionLibrary::Sum()函數;
  • “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:

    USTRUCT(Blueprintable) struct FMyStruct {GENERATED_USTRUCT_BODY()UPROPERTY(EditAnywhere, BlueprintReadWrite)FString Name;UPROPERTY(EditAnywhere, BlueprintReadWrite)int Value; };

    類似這樣一個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()函數。

  • 更新"FFrame::PropertyChainForCompiledIn"這個成員變量;
  • 使用更新后的“FFrame::PropertyChainForCompiledIn”值,更新了"FFrame::MostRecentPropertyAddress"成員變量。
  • 再結合引擎中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);} }

    解釋一下這段代碼:

  • 首先聲明了一個UFunction:static void ShowStructFields(const FDummyStruct& CustomStruct);,其參數類型是“FDummyStruct”,這只是一個占位符;
  • 在UFUNCTION宏里面指定“CustomThunk”和“CustomStructureParam”;
  • 實現一個execShowStructFields函數。這個函數很簡單,主要是處理藍圖的Stack,從中取出需要的參數,然后對用C++的實現;
  • 具體功能實現在:static void Generic_ShowStructFields(const void* StructAddr, const UStructProperty* StructProperty)這個函數中。
  • 實踐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蓝图开发:实现蓝图模板函数的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 强伦人妻一区二区三区 | 三级网站免费观看 | 久久久精品免费 | 国产香蕉尹人视频在线 | 黄色av一级 | 一级特黄肉体裸片 | 在线观看高h | 亚洲综合在线观看视频 | av日韩在线免费观看 | 欧美日韩在线观看一区二区三区 | 在线电影一区二区 | 国产成人精品一区二区三区福利 | 老司机午夜免费视频 | 一级片在线 | 国产日本在线播放 | 亚洲av无码乱码国产精品久久 | 国产成人一区二区三区 | 久久大胆视频 | 欧美日韩国语 | 久久久九九 | 久久久久免费视频 | 亚洲另类一区二区 | 成人精品视频一区二区三区尤物 | 日韩女同互慰一区二区 | 国产无遮挡18禁无码网站不卡 | 多男调教一女折磨高潮高h 国内毛片毛片毛片毛片毛片 | 五月激情丁香网 | 钻石午夜影院 | 国产ts变态重口人妖hd | 日韩在线国产 | 精品动漫一区二区三区在线观看 | 日韩成人短视频 | 精品中文字幕在线播放 | 黄色片免费播放 | 真实人妻互换毛片视频 | 欧美精品一区二区三区在线 | 免费看成人毛片 | 人妻激情偷乱频一区二区三区 | av福利网 | 国产亚洲女人久久久久毛片 | 日本精品视频网站 | 国产精品jizz在线观看老狼 | 日本wwwxxxx | 国产精品视频一二三 | 草女人视频| 99国产精品久久久久久久久久久 | 日韩人妻无码精品久久久不卡 | 懂色一区二区二区av免费观看 | 少妇特黄一区二区三区 | 翔田千里一区二区 | av鲁丝一区鲁丝二区鲁丝三区 | 国产91免费视频 | 免费h漫禁漫天天堂 | 国产精品色综合 | 日本男女激情视频 | 97超碰导航 | 久久久久国色av免费观看性色 | 国产成人三级一区二区在线观看一 | 久热网| 国产交换配乱淫视频免费 | www.超碰在线 | 91精品免费视频 | 精品人妻一区二区三区潮喷在线 | 91视频国产免费 | 免费看黄网站在线 | 韩国久久久久 | 国产亚洲性欧美日韩在线观看软件 | 午夜国产在线 | 三级全黄视频 | 国产精品成人一区 | 99热91| 亚洲色图吧 | 美乳人妻一区二区三区 | 九九视频国产 | 99久久久无码国产精品衣服 | 亚洲先锋影音 | 国产精品日韩精品 | 亚洲一区二区三区不卡视频 | 高跟91娇喘 | 天堂资源中文在线 | 免费无遮挡网站 | 久久精品国产免费 | 欧美日韩国产免费一区二区三区 | 91网站免费观看 | 精品国产一区二区三区在线观看 | 最新日韩视频 | 欧美亚洲国产视频 | 日韩欧美一区二区三区四区五区 | 女人的洗澡毛片毛多 | 4444亚洲人成无码网在线观看 | 一道本在线播放 | 亚洲AV无码片久久精品 | 韩日激情视频 | 欧美精品一区二区三区久久久 | 黄色片hd | 国产精品三级在线 | 日韩精品久 | 日日夜夜添 | 茄子视频A |