[UnrealEngine] 淺談GameFeature Plugin

posted in: UnrealEngine, 開發日誌 | 0

GameFeaturePlugin,這個功能是5.0最大的變革之一,對程式相關職能的人而言,我覺得重要性甚至更勝於nanite跟lumen,因為相關機制會從根本性上的改變了我們程式碼的架構方式以及思考模式。 不知道這東西的人可以先看一下官方介紹:https://docs.unrealengine.com/……/game-features……/ 以下就先來粗略的分享一些我目前對這功能的理解吧: 就他的設計理念看來,他是屬於DLC概念的實現,跟Patch不同,Patch需要蓋掉原本的內容,而DLC必須要做到「熱插拔」的機制。 就傳統UE4 project,我們要達到DLC的這項要求非常麻煩,因為我們只能對「Content-Only」plugin包出dlc包,而哪些dlc需要開啟或關閉則需要你在BaseGame中做出一些可以管理的設計。 在GameFeature Plugin機制出現後,我們總算不需要做這些勞力活了。 注:所謂的BaseGame,指的是${PROJECT_ROOT}/Source下面所有的module以及${PROJECT_ROOT}/Content所有的Asset。 有些人會困惑於我們建出的GameFeature Plugin沒辦法跨專案共用,但這個理解並沒有錯,基本上GameFeature Plugin需要依賴於BaseGame而存在,因此我們也可以理解成他是專屬於project的Plugin。若你的功能想要跨專案共用,你要寫的是一般的Plugin而不是GameFeature。 GameFeaturePlugin之間可以有相依關係,但一個大原則是不能做出循環參照(circular reference)的設計。 就實現面來看,我們做出來的GameFeature Plugin依賴的是BaseGame,而BaseGame則是依賴於Plugins\Experimental\ModularGameplay。 它本身是利用訂閱者模式,讓發佈者(GameFeature)主動的將設計的內容推送到目標Actor身上,並動態掛上該Actor需要的Component。 Lyra Game 中有一個ModularGameplayActors Plugin,裡面就是幫我們做出了各種跟 GameFrameworkComponentManager 訂閱的Actor。 BaseGame本身是不認得我們做的那堆GameFeature Plugin的,他只負責做訂閱的動作。因此當我們在設計GameFeature Plugin的時候,要特別注意這些Plugin要能夠自給自足。可以想像一下,我們總不能做出一個dlc然後把dlc拔掉後原本的遊戲就不能動吧?

[UnrealEngine][開發日誌] 5.1升級到5.2時遇到的有關於AutomationTest的坑

posted in: UnrealEngine, 開發日誌 | 0

最近5.2 preview 1發布了,想說可以開始來升級我的plugin。由於我的plugin有寫一些automation test來幫忙做unit test跟functional test,這樣我就可以省下一些QA的時間。只是在把Plugin升上5.2之後,突然發現之前寫的Automation Test都不會跑了。 在花了一些時間除錯之後,總算發現原因在哪了。結論是:Unreal在5.2之後有做以下幾個更改會影響到這個功能。 第一點:用來過濾來跑哪些Test的FilterPrefix原本是「Filter:」,在5.2的時候改成了「StartsWith:」,因此我們的的AutomationTest要調整成如下的型式: 第二點:有用ScreenShot功能的人會發現不會去比較之前留下來的ground truth image而是要求重新加入,以為整組功能壞光光,這是因為從5.2開始Default RHI變成了D3D12,我們可以在Log中發現下面的訊息: 由於系統會根據目前的FunctionalTest找下面的路徑出來比較: 因此我們只要把所有的Functional Test中的D3D11_SM5改成D3D12_SM5就能抓到需要比較的ground truth image,如下圖: 不過可以想見的是,如果我們把SM5調成SM6的話,那麼相對路徑也會變化,這邊在使用上就要特別注意。 升級完Plugin後,看到Automation Test全通過,嗯,感覺真好。

[開發日誌][UnrealEngine] PainCausingVolume如何結合GAS中的GameplayEffect使用

posted in: 開發日誌 | 0

最近在思考怎麼做毒沼澤的功能,發現引擎中內建PainCausingVolume還蠻符合自己需求的,我們可以在這個Volume上設定傷害數值,並且設定每隔幾秒才觸發事件。 不過由於我的測試專案高度集成GameplayAbilitySystem(GAS),因此會希望所有的角色的數值變化操作都透過系統中的GameplayEffect來執行。由於PainCausingVolume跟GAS系統並沒有相依關係,為了這個機制要另外刻一個Volume又感覺有點多餘。不過好加在GAS系統本身製作的彈性還蠻大的,因此是有方法可以把這種外部的傷害數值注入,後續的傷害計算跟數值的加減就可以延著原本系統設計的邏輯進行,不需要再製作另一套系統維護。 其實方法還蠻簡單的,主要就是要在我們的給PainCausingVolume用的GE中的Magnitude Calculation Type使用Set by Caller,並設定一個GamplayTag讓系統知道之後要拿傷害的時候就去找這個Tag拿就行了。下圖我的這個Tag是設定成:GameEffect.SetByCaller.IncomingDirectDamage。 然後在角色的AnyDamage Callback中,我們就可以做出這個GameplayEffect,使用AssignTagSetByCallerMagnitude把實際的數值指定給這個Tag,然後就可以把這個效果套用給角色產生傷害,如下圖:

[開發日誌][UnrealEngine] 如何將Mixamo的動畫Retarget給Unreal Character

posted in: 未分類, 開發日誌 | 0

這篇文章主要是要記錄自己怎麼把Mixamo中的動畫導出來給Unreal中的Mannequin用,以及除何修正中途遇到的一些坑。 使用版本:5.1 使用的是Third Person Template 第一步當然是先從Mixamo下載看中想使用的動畫。 由於Mixamo的骨架跟Unreal不相容,主要原因Mixamo是少了一個root,這會造成root motion沒辦法正常運作。我們可以下載 mixamo_convert這個Blender Addons來幫我們加上一個root bone。 如下圖,右邊加上root的是我們希望的狀態: 這邊要注意的是,Blender Addons的安裝要使用zip才認的到。 https://github.com/enziop/mixamo_converter 下載完zip之後,從preference選Add-ons安裝,然後把功能打開 接著從右上方打開選單 可以看到Mixamo Rootbaker 可以選擇把bone轉成UnrealEngine公版的bone Name,但我這邊不勾,之後自己再使用UE內建的IK Rig以及IK Retargeter來做map,詳細步驟後面會記錄。另外他的OutputPath目錄不會自己建,所以要自己先開: 之後我們就可以先匯入轉換過後的Mixamo的模型(X Bot.fbx) 接著匯入其他的動畫,記得Import Mesh勾掉,Skeleton選擇剛剛匯入的。 然後就可以開始建立IK Rig,這個檔案的目的是建出組Bone Chain,讓我們可以可以在IK Retargeter中指定Source IK Rig跟Target IK Rig做map。 Source就是剛剛匯進來的Mixamo,我們必須要手動建一個出來(右鍵Animation -> IK Rig 下可以找到),Target則是Unreal內建的Mannequin,路徑是/Game/Characters/Mannequins/Rigs/IK_Mannequin。 … Continued

[開發日誌][UnrealEngine] 如何自動排序原始碼中的include

posted in: UnrealEngine, 未分類, 開發日誌 | 0

之前在寫code的時候,發現某個cpp檔中的include了一大堆其他module中的檔案,而且其中一大堆重覆include,越看越覺得亂,然後心中的潔癖突然發作想要好好的整頓一番。第一個想法是,如果我們的include能夠自動依字母排序,這樣就能夠很清楚的看出有哪些include重覆了,若我們能好好的依不同的module進行分類,還能夠清楚的知道我們用到了各別module中的哪些功能。 查詢了一下,其實VisualStudio中還真的有相關的功能,他的快速鍵是Shift+Alt+L, Shift+Alt+S: 由於我有買Visual Assist X,因此另外也試了一下它的版本,並把快速鍵設成了F9。最後發現這個版本比內建的順手多了: https://docs.wholetomato.com/default.asp?W175 另外也發現了Visual Studio Code也有SortLine的功能: 做完快速鍵的設置後,我們就可以使用該功能來幫我們做排序,我自己是喜歡依據module的不同來區分,如下圖: 除了include之外,我也喜歡對build.cs中的DependencyModuleNames做排序: 嗯,看起來清爽多了。

[UnrealEngine] 如何使用DataFragment設計進行資料欄位擴充

posted in: UnrealEngine, 未分類, 開發日誌 | 0

本文章內容基於UnrealEngine5.1版本 最近在看5.0新導入的MassFramework,看到裡面出現了FInstanceStruct這個詞引起了我的興趣。基本上這東西可以讓我們動態選擇struct,而在Unreal中是把這種組裝用的property稱為Fragment。這個概念我第一次看到是在Unreal官方釋出的Lyra範例專案中看到,只是當時實驗了一下,由於他是使用UObject搭上reflection macro來實現,我沒辦法把一個UObject直接放進DataTable的Row中做編輯。這帶來的結果,就是當我在製作Plugin的時候,若需要用到DataTable時,必須要預先預想好所有使用者可能會想要有的欄位:哪些要直接放到TableRow中、哪些要指向外部的asset讓使用者自己在自己定義的BP或DataAsset中擴充。然而,可以想像的到,多了一個uasset就代表編輯的時候要另外開檔案,這對企畫人員非常的不友善,而且檔案一多,在維護上也會有不小壓力。這次看到了FInstanceStruct,馬上直覺想到這東西或許可以解決這個困擾我很久的問題。稍微實驗了一下,馬上驗證了上面提到的問題可以靠FInstanceStruct的機制來解決,因此決定花一點時間整理二種方法的優缺點。 接著,讓我們來看看這二種方法個別怎麼實現吧。 使用InstanceStruct製作Fragment的例子 InstanceStruct是定義在StructUtils這個Plugin中,記得要先去開起來。雖然在5.1還是Experimental,不過它是MassEntity的相依功能,而這個Plugin已經進Beta,因此看起來是不太需要擔心,目前我用起來感覺還蠻穩定的。 在把Plugin開起來之後,首先我們需要要定義一個BaseFragment: 接著就可以在TableRow中宣告這個property,而這個property需要的meta如下: BaseStruct用來限定Editor中可以顯示哪些Struct出來,而ExcludeBaseStruct可以把這個BaseFragment從選項中排除。 然後就可以根據需求繼承這個BaseFragment並加入任何需要的資料。 實際在DataTable中使用起來的體驗如下圖,我們可以自由選到所有繼承自BaseFragment的struct,裡面對應可編輯的欄位也會動態反射顯示出來: 接著可以用以下方法拿出我們所定義的Fragment: 不過這方法的缺點是,相關的擴充功能只能在C++中進行,因為我們無法用BP去剛剛宣告出來的BaseFragment。 使用UObject製作Fragment的例子 基本概念跟上面的例子差不多,只是我們需要換成使用UObject宣告BaseFragment,另外要記得UCLASS裡要加上DefaultToInstanced跟EditInlineNew。 宣告使用,可以在BP或DataAsset中宣告,這邊宣告在DataAsset中: 接著就可以繼承下來做資料的擴充 當我們把他宣告在DataAsset中時,就可以根據狀況選擇需要的Fragment,另外除了C++外,也可以純使用BP下來做欄位的擴充,裡面的可編輯欄位也能動態的反射顯示出來。 使用法方就是把DataAsset讀進來後,直接對Fragment做Cast就行: 結論 二種製作Fragment的方法各有優缺點,就資料擴充性而言由於InstanceStruct只能在C++中進行擴充,而UObject則可以同時在C++/BP中做,這讓UObject版本帶來某方面的優勢,然而,這方法的缺點是需要遊戲設計者另外製做出BP或DataAsset才能夠做資料的編輯,也就是說會多一個uasset,這在某些需要定義大量道具、武器裝備數據的應用是無法接受的;相反的InstanceStruct則可以很自然的作為TableRow的擴充嵌入DataTable。關於這點,我們可以看下圖,CustomDataFragment使用的是InstanceStruct方法,而CustomDataAsset則是使用UObject的方法,這個DataAsset中包含了Fragment的定義。 因此哪種方法比較好,可能要看遊戲的設計而決定。例如UE5導入的MassFramework,裡面的FMassFragment是基於InstanceStruct的設計;而在Unreal官方釋出的Lyra範例專案中,他的裝備系統是基於UObject Fragment方法的設計。另外若是多個TableRow需要「共用」參數的話,或許UObject Fragment也是個不錯的解決方案。我自己是在資料表中同時提供二種方式,讓使用者自行決定要使用哪種方式做擴充。

[UnrealEngine][開發日誌] 關於GAS中FAttributeSetInitterDiscreteLevels的神坑

posted in: UnrealEngine, 開發日誌 | 33

最近發現GAS的Attribute初始化時那張CurveTable,在打包版本時會把多餘的資料移除,造成FAttributeSetInitterDiscreteLevels在進行初始化時會失敗,猜測這個行為是在4.23以後(2020年3月的commit)的修改。 由於CurveTable本身設計是被用來當成連續型資料使用,因此進行重覆點的移除是可以理解的,因為可以減少個存的資料量進而增進效能,只要我們可以在2點之間利用內插法取得數值就行。 但是用在GAS的Attribute初始化上就出問題了,因為在GAS中他並不被當成連續型資料使用;而是轉成離散型資料,並以CurveTable上面的點進行屬性的分群。 移除重覆點,代表我們會遺失某個等級的資訊,例如當我們某個GAS Attribute在不同的等級上的數值都沒有變化,例如1~3級的移動速度都是200,他就只會保留等級1跟等級3,中間的等級2被當成重覆資料移除掉了。 當我們對等級2的角色進行屬性初始化時,就會因為找不到該等級資料而失敗。 這個議題比較噁心的點在於,它只會在打包版本發生,因為在editor中並沒有進行移除重覆資料的動作。 這個問題解決方法有幾種:         1. 使用ConsoleVariable把CurveTable.RemoveRedundantKeys這個預設行為關閉,但會影響所有的CurveTable,造成這項優化失效。     2. 重新設計AttributeSetInitter,不要離散化,而是直接用CurveTable

[開發日記][UnrealEngine] 關於C++中的volatile變數的一些記錄

posted in: UnrealEngine, 開發日誌 | 0

大家應該或多或少在engine中都會看到volatile這個關鍵字,通常這個詞都是跟某個系統上的cache機制有緊密的關係,被宣告成volatile,代表的是他是隨時會被各種不受控制的外部系統改變,因此系統不會對該資料進行cache的動作。 在C++中,對某個變數i宣告volatile這個關鍵字又代表著什麼意思呢?對cpu而言,他對於變數的cache機制不難想像,就是他身上的暫存器 (Register)。 基本上不管編譯時期的compiler或運行時期的cpu,他們會盡一切手段去優化執行的效率,例如把變數變成常數,reorder指令的順序或者是把某個值i從記憶體中讀進cpu的register中,然後接下來的指令就直接從該register位置中拿值而不再去從記憶體中要。雖然這些優化手段在使用上都會是保守的手段,理論上會被保存在register中的數值,對於接下來運行的指令而言應該是可信的,例如: int i=10; int a = i; int b = i;  i=10在這三條指令的範圍內應該要是是可信的。 但還是有些例外,例如該值i是被其他外部程序直接更改記憶體中的值的時候呢(*1)?比如a先assign了10之後,i指馬上被外部程式改成了11,這時候由於cpu register沒機會去刷新,因此b還會維持10的結果,但理論上應該要是11才對。這時候volatile這個關鍵字就派上用場了,前面也提到,被宣告成volatile,我們的系統就不會去cache他,因此我們每次都重新去從記憶體中拿值而不是從register中。 *1:這邊通常是硬體搭配Memory mapping I/O使用。利用系統保留的記憶體區間對應一個裝置上的幾個register數值,在程式中操作記憶體等同於操作該裝置。 不過在multithreading的使用情境中,volatile並不等同於thread-safe。雖然compile time時的優化被volatile指令阻止了,包括禁止cache跟reorder指令。然而,雖然你的指令順序有乖乖照你希望的被編譯出來,但cpu可沒那麼聽話,他run time的reorder還是會照常運行。 因此要thread-safe我們還是要搭配有同步(synchronization)機制的正規手段才行,例如atomic、mutex或memory fence,換句話說,在multi-thread的環境下,指令的執行順序很重要,因此上面這幾個工具就是設計來確保程式能夠叫compiler跟cpu都能夠乖乖照我的邏輯執行,不要亂做奇怪的優化並籍此防止race condition。 另外volatile的實際行為,要不要reorder以及怎麼reorder也硬體自身的實作有關,因此太依賴這個東西的話可能會在跨平台的應用上踩到某些雷。 不過在unreal的搜原始碼中,我們會看到有一些multi-thread用的變數除了有用Atomic相關工具之外,還是被加上了volatile,就我的認知中,他最好只在使用MMIO跟硬體存取搭配時使用。關於這點我思考了很久,會不會是因為歷史因素照成的?或是某些平台,單純使用上面提到的幾個同步工具時不可靠?這點我還沒有答案,也有可能是我的認知還不夠完善,有人知道原因的話也歡迎提出來討論。

[開發日記][UnrealEngine] MemoryMapped File

posted in: UnrealEngine, 開發日誌 | 0

前幾天在看資料看到MemoryMapped File這個東西,好奇unreal目前對於MemoryMapped File支援度如何,因此花了一些時間看了一下並做一些實驗。由於iOS上可用的記憶體實在太少,想說如果沒開的話可以試著把他開起來。 簡單來說,這東西能夠讓我們直接把硬碟上的檔案直接map到我們的virtual memory中,直接用記憶體相關操作做檔案IO,這不僅可以少掉記憶體壓力,也能夠少掉把檔案讀寫進記憶體時間。由於他佔用的是virtual memory的空間,因此在64位元的系統,他的上限可以對應到2^64次方,就是8EB;相對於32位元的系統只有4GB。雖然4.27在Android上還支援使用32位元的armv7,但UE5就只剩下arm64了。 MemoryMapped File有console command可以開關:mmio.enable。 mmio這個名詞用在這裡其實還蠻讓人困惑的,他跟MemoryMapped File的概念不太一樣。 更進一步的把mmio這個關鍵字餵進去搜尋,會找到2個地方有出現這個名詞: MMIO:Memory mapping I/O,主要是用在跟外部硬體互動的機制,很久以前會需要發特殊的硬體指令,但現在(不確定這個現代是從什麼時候)喜歡透過正規的記憶體操作來跟硬體互動,現在的硬體大多支援這個機制。作業系統會保留一塊區間給外部硬體,一個記憶體位置對應一個硬體裝置,在程式中操作記憶體等同於操作裝置。細節我也沒玩過,google到的使用方法是做一個struct,然後針對某個係保留的記憶體位置強制做轉型,之後就可以在程式中對該物件做操作。1. data-in register2. data-out register3. status register4. control register例如手把controller的區間是200–20F(512-527) 結論來說,目前引擎只有iOS有開啟支援,且4.27限定在CompressedAnimation跟UnrealAudio這二個asset上、5.0則只支援UnrealAudio。看來UnrealEngine對於iOS這種記憶體有限的裝置,還是有想一些應對的方法來減緩記憶體的壓力。沒有全部asset都開的原因,可能是因為flash memory有寫入次數的問題。雖然目前我還不確定為什麼5.0要把CompressedAnimation關掉,可能跟上面的原因一樣。 有試著開其他type的asset,結果馬上就遇到了檔案alignment的check error;測試開了windows,也會同樣的error也會發生。註解掉error是能夠繼續運行,但就沒深入去研究有什麼副作用(side effect)就是。 各別Asset的開關需要去調整FGenericPlatformProperties中的SupportsMemoryMappedFiles、SupportsMemoryMappedAudio以及SupportsMemoryMappedAnimation。要針對各平台的話,例如IOS,就是在FIOSPlatformProperties中實作這三個function並回傳true。 要注意的是,在引擎中的用法是先檢查SupportsMemoryMappedFiles是不是true,再根據各別的Asset去看要不要開起來,也就是說二個條件都要符合,例如Animation的檢查如下: 註:想強制把功能開起來,需調整以下東西1. FBulkDataBase::Serialize把bAttemptFileMapping強制設成true,2. DefaultEngine.ini中的[MemoryMappedFiles]MasterEnable=trueAlignment=16384

[演講筆記][UnrealEngine] UE5中的渲染技術 2022台北遊戲開發者論壇

posted in: UnrealEngine, 開發日誌 | 0

花了一些時間把EpicChina在2022台北遊戲開發者論壇的演講看完並做了一些筆記。 題目是關於UE5中的渲染技術。 只是看完影片後覺得資訊量太多,腦袋呈現完全爆炸狀態……然後影片結尾聽到講者說這只是很粗淺的介紹後我就崩潰了。 雖然以下筆記很多內容我都還有點消化不良而且可能也有理解錯誤的地方,但不管怎麼樣先分享出來給需要的人。 想更進一步知道細節的推薦大家去看演講,不能只有我崩潰。 https://www.twitch.tv/videos/1533064361 1:54:37秒 開始 1. 在nanite中大小三角形會分別用軟硬體做,大的用硬體做,小的為了避開overshading的問題,他自己用compute shader寫了rasterization。為什麼呢?在三角型在算pixel的顏色時,由於頂點不會剛好在pixel的中間,因此他必須相鄰的4個pixel拉進來,用一個Quad為單位來考慮。當你的三角型很小而且緊密時,某些pixel會一直反覆被拉進去做計算,這情況稱之為Overshading或者是Quad OverDraw。而Nanite為了解決這個問題,因此大的三角型雖然還是靠硬體做,但小的三角型則是自己用軟體做,在Compute Shader中寫rasterization。小的三角型有一些條件需要達成才會被歸類為小三角型。 2. 整套技術的重點在Cluster生成、Culling跟LOD的選擇,為了高效的達成Culling跟LOD的選擇,引擎從4.22開始就把整個render底層換成retained mode,就是說讓GPU去維護整個場景render object的一些狀態。 這邊是GDC2019演講。 3. LOD的生成機制,其實就是先做graph partition演算法做cluster分組,然後不斷的遞迴減半面數,最終生成一個DAG(Directed Acyclic Graph)。Graph partition的分群的給定條件是共享的邊盡可能的少,面積盡可能的均勻。這出來的結果是每下一級LOD都會是最小的變化,也就是說外觀的變化最小。而衡量外觀變化的標準就是使用QEM(Quadric error metric)。 4. QEM這個衡量標準可以保證我們減面後的error會越來越大,從DAG的Root到Leaf Node,記錄在節點中的error值可以幫助我們做LOD的選擇。 5. 每個Cluster的BoundingBox都會再各別對每級的LOD個別生成BVH(BoudingVolumeHierarchy)後,再掛到一個大的BVH root下面,因為這樣gpu的入口會比較一致。每4個Cluster會組成一個cluster group,最終這個nanite模型會是一個cluster的BVH結構。 6. Culling會利用分群後的資訊進行,經過以下步驟: — a. Instance Culling:用上一個frame的HZB做跟BoundingBox來做剔除,得到一個粗略的可能還可見對象的instance。— b. … Continued