[UE5]使用C++借助反射批量獲取/修改藍(lán)圖參數(shù)

說說前情
上周美術(shù)大哥提了一個(gè)需求,希望能修改Ultra Dynamic Sky里純藍(lán)圖類Dynamic Sky的一些參數(shù),以實(shí)現(xiàn)在基礎(chǔ)的時(shí)間變化上增添“億”些細(xì)節(jié),他已經(jīng)把參數(shù)名稱(41組*3種預(yù)設(shè))總結(jié)到Excel了。對這么多參數(shù),我本想著性能差一點(diǎn)直接向后期框一樣直接暴力拷貝參數(shù)結(jié)構(gòu)體就好,但這個(gè)插件的作者著實(shí)是比較猛,參數(shù)沒有外層包裹、需要一個(gè)一個(gè)調(diào)整,同時(shí)又沒有C++基類,是純藍(lán)圖做的。

如果用藍(lán)圖做還容易錯(cuò),正好最近在學(xué)UEC++,不如查查看有沒有什么用C++批量改參數(shù)的方法,在論壇一頓搜索之后發(fā)現(xiàn)了這篇(https://forums.unrealengine.com/t/reading-struct-attributes-in-c/471834/2),又看到了大釗老師在知乎上InsideUnreal中對反射的講解(https://zhuanlan.zhihu.com/p/61042237),那不如用C++整個(gè)活。
美術(shù)大哥提的需求里主要需要獲取浮點(diǎn)數(shù)(5.1中默認(rèn)雙精度,其實(shí)對應(yīng)的是double)、布爾值(bool)、顏色值(FLinearColor)三種類型。考慮到后續(xù)升級和復(fù)用,我創(chuàng)建了一個(gè)C++的ActorComponent掛載到Dynamic Sky Actor上。使用這個(gè)Component作為作為數(shù)據(jù)控制器。接下來就是如何把數(shù)據(jù)從三種已經(jīng)調(diào)好的流送關(guān)卡中讀取出來、寫到DataTable,然后在切換預(yù)設(shè)的時(shí)候把參數(shù)再寫到主場景的Dynamic Sky?Actor中。簡單來說就是:
讀取藍(lán)圖數(shù)據(jù)——寫入DataTable——讀取DataTable——寫入藍(lán)圖數(shù)據(jù)

利用反射讀取藍(lán)圖參數(shù)
通過一些資料的查閱,讀取方面分為以下三步:
首先需要通過給定的名稱找到藍(lán)圖中反射參數(shù)FProperty,主要函數(shù)是PropertyAccessUtil::FindPropertyByName()或FindFProperty<FProperty>()
轉(zhuǎn)換成需要的類型, CastField<FTargetTypeProperty>(FProperty)
然后通過FProperty的內(nèi)置函數(shù)把參數(shù)讀出來FTargetTypeProperty->GetPropertyValue_InContainer(Target)
這邊需要注意FProperty是4.25以后版本中對外暴露的項(xiàng)取代了之前的UProperty,早些的文章中可能會(huì)使用UProperty,本文以5.1為演示平臺,使用時(shí)注意版本對應(yīng)。
獲得double值(藍(lán)圖中的float)參考代碼:
特別注意下面這行中不再傳入Target->GetClass()(或Target->StaticClass())而是直接傳入需要獲取的對象指針,在有些文章里有編碼錯(cuò)誤,導(dǎo)致無法獲取實(shí)例中的值。
為了便于開發(fā)Debug,下面寫了一個(gè)Debug編輯器函數(shù)進(jìn)行嘗試,獲取名稱為MoveAmplitude的藍(lán)圖變量,在可以在編輯器中修改為其他的float值名稱。
能夠成功獲取該變量

那么根據(jù)上邊的方式稍作修改就可以獲取其他類型變量的值,可以根據(jù)每一個(gè)需要修改的變量類型創(chuàng)建對應(yīng)的函數(shù)。
但應(yīng)當(dāng)注意FVector、FLinearColor類型的變量是以結(jié)構(gòu)體形式儲存的,沒有直接的F【XXX】Property對應(yīng),需要獲取為FStructProperty然后使用FProperty::ContainerPtrToValuePtr直接返回容器內(nèi)值的指針,參考代碼如下:
運(yùn)行結(jié)果如下

簡化獲取步驟
這個(gè)步驟中對變量進(jìn)行了嚴(yán)格的限定,雖然達(dá)成了目的但是適用度太窄,如果后續(xù)修改其他類型的變量又要?jiǎng)?chuàng)建新的函數(shù),而且其中的重復(fù)步驟太多,可以考慮簡化一下然后變成函數(shù)模板縮減一下工作量。參考了這篇文章(https://forums.unrealengine.com/t/c-introspection-and-containerptrtovalueptr/1208130/3)于是就有了下面的內(nèi)容:
由于返回值類型不同,轉(zhuǎn)換到輸出中打印。
修改Deug函數(shù)嘗試調(diào)用一下:
點(diǎn)擊運(yùn)行后各變量輸出正常。

寫入DataTable
完成數(shù)據(jù)讀取之后下一步就是要把讀出來的數(shù)據(jù)寫入藍(lán)圖,根據(jù)[中文直播] 第21期 | UE4數(shù)據(jù)驅(qū)動(dòng)開發(fā) | Epic 大釗中大釗老師講的內(nèi)容,使用DataTable或DataAsset均可,兩者在編輯上也類似,差異在于DataTable前置條件是結(jié)構(gòu)體,而DataAsset前置條件是類。在實(shí)際應(yīng)用的配置方面,DataTable只需要指定一個(gè)變量,DataAsset需要?jiǎng)?chuàng)建一個(gè)Map,作為純數(shù)據(jù)存儲兩者差異也不大。最終考慮到csv導(dǎo)入導(dǎo)出還是選擇了DataTable,但DataTable不支持藍(lán)圖寫入,所以需要把DataTable寫入邏輯寫在C++。
首先創(chuàng)建結(jié)構(gòu)體,由于之前美術(shù)同時(shí)已經(jīng)把變量名字抄到了Excel中,那么就需要批量創(chuàng)建對應(yīng)的結(jié)構(gòu)體變量,這方面使用Excel公式拼接配合Word查找替換就可以很輕松的完成。在示例(即上邊的截圖,文末附鏈接)中我創(chuàng)建了如下變量:
bool bShowWidget;FVector WidgetOffset;FText WidgetText;FLinearColor TextColor;
/*FTexture2D WidgetTexture*/;double MoveAmplitude;int32 Index;
對應(yīng)的創(chuàng)建結(jié)構(gòu)體,繼承自FTableRowBase,在替換時(shí)特別注意Category的引號容易被替換為全角引號,可以指替換前不加換行,到IDE中使用列編輯模式統(tǒng)一修改。

創(chuàng)建DataTable后在C++中添加DataTable對象指針,然后使用函數(shù)設(shè)置DataTable值,(參考文章:https://www.cnblogs.com/shiroe/p/14745783.html)
在嘗試過程中發(fā)現(xiàn),用戶定義結(jié)構(gòu)體UScriptStruct在繼承關(guān)系上比較復(fù)雜,無法直接將上面的UObject替換為UScriptStruct,使用上邊創(chuàng)建的結(jié)構(gòu)體類型進(jìn)行處理:
這邊對應(yīng)的使用Target->StaticStruct()作為FProperty作為查找參數(shù),直接使用找到的指針改變參數(shù)值。
同樣創(chuàng)建EditorCallable函數(shù)在編輯器中調(diào)用:
指定后點(diǎn)擊編輯器中生成的Button將藍(lán)圖實(shí)例的變量以指定的RowName寫入DataTable寫入結(jié)果:

如果當(dāng)前DataTable處于打開狀態(tài),需要關(guān)閉并重新打開查看寫入結(jié)果。
寫入完成后就可以給不同子關(guān)卡中的目標(biāo)對象分別掛載Component,指定不同的RowName將內(nèi)容寫到DataTable進(jìn)行數(shù)據(jù)保存,需求完成了一半,剩下的就是把DataTable里的值在些回到藍(lán)圖對象中。
從DataTable讀取到藍(lán)圖
從DataTable中讀取數(shù)據(jù)和讀取藍(lán)圖中變量的邏輯基本一致,結(jié)合上面對結(jié)構(gòu)體的修改內(nèi)容如下:
設(shè)置藍(lán)圖變量:
創(chuàng)建編輯器可調(diào)用函數(shù)并進(jìn)行測試
至此實(shí)現(xiàn)了將對象A的參數(shù)寫入DataTable然后再將DataTable中保存的變量寫入對象B。

其他易用性優(yōu)化
對于需求中需要覆蓋double(float)、bool、LinearColor類型的多個(gè)變量,可以將同種變量進(jìn)行歸類,使用特殊分割符號(比如【&】),然后使用FString::的ParseIntoArray轉(zhuǎn)換成字符串?dāng)?shù)組,然后For循環(huán)依次設(shè)置同類型的值。FName構(gòu)造可以直接接收FString類型,直接輸入即可,但是注意最好不要帶空格,否則在解析的時(shí)候可能會(huì)報(bào)錯(cuò)(因此我把插件里的變量都去了空格)。
三種預(yù)設(shè)中可以創(chuàng)建枚舉量,然后覆寫PostEditChangeProperty函數(shù)(參考文章https://zhuanlan.zhihu.com/p/63195899),當(dāng)枚舉量修改時(shí)調(diào)用從DataTable中讀取變量的邏輯,實(shí)現(xiàn)直觀預(yù)覽和效果確認(rèn)。但是DynamiSky的需要調(diào)用構(gòu)造函數(shù)才能預(yù)覽變化,在嘗試中反射沒能成功調(diào)用藍(lán)圖的構(gòu)造腳本,因此還是需要一步手動(dòng)操作。這個(gè)函數(shù)在使用中還需要注意在聲明和實(shí)現(xiàn)中該函數(shù)均需要使用#if WITH_EDITOR #endif包裹,否則打包時(shí)會(huì)報(bào)錯(cuò)。
增刪變量需要修改內(nèi)容,在本文中變量查找使用名稱驅(qū)動(dòng),因此在少用一個(gè)變量時(shí)只需要在輸入變量群字符串中去除該變量即可,讀取時(shí)會(huì)跳過該變量;對增加的內(nèi)容一方面需要在輸入變量群字符串中加入該變量,另一方面需要在結(jié)構(gòu)體中對應(yīng)添加、更新DataTable內(nèi)容。

后續(xù)潛在的改進(jìn)方向
Texture2D讀取與寫入,在上邊的內(nèi)容中Texture2D數(shù)據(jù)的讀取和寫入都被注釋掉了,目前還沒有想到讀取和寫入的方法,待后續(xù)嘗試中進(jìn)一步完善。
DataTable寫入過程通用性增強(qiáng),上文在讀寫DataTable時(shí)傳入的時(shí)特定的結(jié)構(gòu)體,兩個(gè)文件形成了強(qiáng)綁定關(guān)系,在對接其他需要修改的對象的時(shí)候需要重復(fù)工作。目前已經(jīng)有文章描述自動(dòng)創(chuàng)建Struct的方法(參考文章https://blog.csdn.net/zhangxiaofan666/article/details/112879891),后續(xù)會(huì)進(jìn)一步擴(kuò)展。
讀取寫入方法函數(shù)模板統(tǒng)一,這個(gè)項(xiàng)目是我第一次使用反射,UE內(nèi)各種繼承關(guān)系還沒有完全搞懂,很多地方有重復(fù)的代碼,比如在BP和DataTable讀寫兩者差別很少但沒能統(tǒng)一,不同類型的變量需要分別調(diào)用。
性能優(yōu)化,在調(diào)用FindPropertyByName的過程中進(jìn)行了很多次域的查找,根據(jù)網(wǎng)上大佬的做法似乎可以對這方面調(diào)用更加內(nèi)部的代碼,實(shí)現(xiàn)僅查找一次完成所有變量的讀寫操作,提升性能。
希望對初學(xué)者朋友們有一點(diǎn)點(diǎn)幫助,也歡迎大家提建議,希望和大家共同進(jìn)步。
示例工程:https://github.com/jiadevr/UECppLearning