Build Static Navigation Mesh with Commandlet in World Composition
簡介
先前有提到因為地圖被細切成很多張的關係,想要使用static nav mesh有幾個困難處要解決:
1. 地圖範圍很大的時候,是沒辦法一次載入所有地圖,只按一次build path就完成的。
不僅會執行很久,也會遇到build path失敗的情況。失敗會有警告訊息並且有部分nav mesh不完整。
2. 如果想要每張地圖各別計算,要在編輯器內重複的讀取子地圖,build path,子地圖存檔,卸載子地圖。
這樣的流程其實更適合用自動化來做。
這是本系列的最後一篇
目前一共有以下幾篇為系列的文章
1. Import RealWorld Landscape to UE4
2. 了解Landscape的組成元件
3. Generate Navigation Mesh with World Composition
4. Build Static Navigation Mesh in World Composition
名詞與縮寫說明
在開始之前,先介紹一些本篇文章會用到的名詞或是縮寫。
NBV: Navigation Bounds Volume,用來定義navigation mesh的範圍。
P-Level: Persistent Level。在本篇指的是在World Composition模式下的主地圖。
Sublevel: 在本篇指的是在World Composition模式下的各個子地圖,可能是透過tiled height map匯入進來的。
前置準備
要能夠執行期間讀取/卸載存在子地圖的靜態nav mesh資料,需要以下步驟。
我試過很多方法,下面的步驟缺一不可。這些步驟都是在開啟P-Level的模式下運作。
1. 放置一個NBV在P-Level中,可以不需要跟任何東西交集。
2. 選擇P-Level內自動產生的RecastNavMesh Actor。
3. Runtime Generation 設為Static。
4. Fixed Tile Pool size設為true。
5. Tile Pool size有可能需要隨著地形大小調大。
6. 在每個子地圖放置需要的NBV。
以上的步驟完成之後,就可以手動對一個子地圖build path再執行,確認是不是能夠真的動態載入。
如果能夠動態載入/卸載,就代表成功了。
要嚴謹一點的話,最好存檔後重開編輯器再測試,最嚴謹的話,甚至要package測試。
基本上我在實驗的過程中各種情況都遇到了,例如只有編輯器預覽正常,或是編輯器重啟後不正常,或是package出來才不正常...
但是沒有關係,只要照著上面的步驟作,應該是不會有問題的。
注意事項
World Composition模式下的Static Nav Mesh似乎只能在主地圖開啟的模式下build path。
如果你直接打開子地圖Build path再存檔的話,這個子地圖的nav mesh反而不會被即時載入。
因為nav mesh直接被存進子地圖的RecastNavMesh這個actor內了。
但是在world composition模式主要的RecastNavMesh是放在P-Level內。
UE4可能不支援多個RecastNavMesh actor,所以會有無法載入的問題。
這個細節不一定每個團隊人員都會知道,所以會造成開發上的麻煩。
例如可能某個人只單獨開啟子地圖編輯場景,修改場景內容的同時也按了build path並存檔。
(又或是他的editor設定為自動更新navigation)
這樣一作下去這塊地圖的nav mesh就壞了,如圖所示。
所以引入Commandlet自動化的話,比較不會有這樣的疑慮。NavMesh的部分就一律交給機器人更新。
Do not build path in sublevel directly.
Commandlet Construction
Commandlet可以開啟無使用者介面的UE4 editor,並執行Commandlet內撰寫的流程。
這次的目標是開啟P-Level,依序載入子地圖,計算路徑後對子地圖存檔。
所以我們需要繼承已經有讀檔/存檔能力的UResavePackagesCommandlet。
不過因為Commandlet是Editor用,直接創在專案內會影響打包流程。
可能會有編譯錯誤的問題,所以製作成Plugin會比較好管理。
創造新的Commandlet流程
1. 在Editor創一個Blank Plugin。(不用是Editor Plugin)
2. 修改.uplugin檔Modules內的參數
"Type": "Editor"以及"LoadingPhase": "Default"
3. 修改Build.cs檔PrivateDependencyModuleNames內的參數
新增"UnrealEd"來開啟相關的功能
4. 在Plugin內新增C++ class 並繼承UResavePackagesCommandlet
5. Override PerformAdditionalOperations,需要實作的行為寫在這裡
Modify .uplugin.
Add UnrealEd into build.cs in plugin.
ResavePackagesCommandlet介紹
在這次的環境,我們需要ResavePackagesCommandlet內建的幾個重要功能,
包含
1. InitializeResaveParameters
2. LoadAndSaveOnePackage
3. PerformAdditionalOperations
4. CheckoutFile
InitializeResaveParameters
我們需要InitializeResaveParameters來解析輸入參數,主要是要擷取Map參數,讓後續的LoadAndSaveOnePackage使用。
LoadAndSaveOnePackage
LoadAndSaveOnePackage算是ResavePackagesCommandlet的主函式。主要內容就是讀檔,作事情(存檔,算Lighting資訊等等)與上傳(負責與source control溝通)。
PerformAdditionalOperations
因為LoadAndSaveOnePackage實作了太多事情,我找到PerformAdditionalOperations有提供virtual可以實作,除了會把讀地圖檔建立好的World傳進來。
函式本身的程式碼也非常具有參考價值,尤其是Setup the World的部分,充分說明了在Commandlet裡面要如何讀一個地圖檔並且建立出正確的World,如同Editor中一樣。
CheckoutFile
因為我們是使用Perforce作版本控管,所以還多需要CheckoutFile的功能。
Commandlet Implementation
這邊還可以分為幾個部分:
Build Navigation必要的程式碼
正確的讀取子地圖必要的程式碼
如果是要使用Commandlet計算靜態場景(非World Composition)的路徑,就不用了解後者。
只需要參考Build Navigation必要的程式碼就好。
Build Navigation必要的程式碼
需要Build Navigation總共需要呼叫兩個函式:
FNavigationSystem::AddNavigationSystemToWorld
NavigationSystem::Build
呼叫AddNavigationSystemToWorld的原因,是因為我們需要把 MainNavData 建立出來。call stack 大概是
AddNavigationSystemToWorld
InitializeForworld
ProcessRegisterationCandidates
如果沒有呼叫這行,後續要執行NavigationSystem::Build的時候會因為無資料而直接結束Build流程。
NavigationSystem::Build。呼叫Navigation build nav mesh的最主要函式,所有的資料都要準備齊全才會有正確的結果。
我有追查到在編輯器點選Build Path其實是會呼叫FEditorBuildUtils::EditorBuild。
不過在Commandlet使用會因為GUnrealEd是null而當機,所以不能直接呼叫。
正確的讀取子地圖必要的程式碼
Load Sublevel
Initialize Sublevel
Save Sublevel
Unload Sublevel
CollectGarbage
Load Sublevel
首先我們有的是P-Level的UWorld*,從World->WorldComposition->TilesStreaming可以拿到子地圖的資訊。
利用GetWorldAssetPackageName可以查到子地圖的完整路徑,再透過LoadPackage讀入記憶體。
Initialize Sublevel
此時P-Level跟讀入的子地圖還是沒有關連的
所以要透過AddStreamingLevel建立P-Level跟子地圖的關係。
然後子地圖要呼叫一連串的函式,設定狀態,再透過FlushLevelStreaming更新。
但是只有這樣還不夠,我發現子地圖其實是以原點為中心儲存的,
Engine會讀取Tile資訊的Absolute position,然後呼叫ApplyWorldOffset將地圖內所有actor的座標更新。
Navigation mesh才會在計算到對的位置。
Tile的Absolute position的抓取可看下面的程式碼
TArray<FWorldCompositionTile>& tileList = worldComposition->GetTilesList();
TArray<ULevelStreaming*> tilesStreaming = worldComposition->TilesStreaming;
for (int32 index = 0; index < tilesStreaming.Num(); ++index)
{
auto perLevelStreaming = tilesStreaming[index];
auto tileInfo = tileList[index].Info;
FIntVector levelOffset = tileInfo.AbsolutePosition;
}
在設置的最後一個步驟記得要呼叫EditorLevelUtils::SetLevelVisibility,宣告Sublevel的顯示狀態visible。
這樣navigation build的時候才抓的到SubLevel的NBV。
詳細的程式碼可看圖。
Initialize sublevel.
Save Sublevel
在儲存Sublevel之前,要先還原剛剛為了正確計算nav mesh的位移操作。
所以要呼叫一次ApplyWorldOffset,但是這次要乘上-1。
儲存相關的程式碼請參考圖。
Save sublevel.
Unload Sublevel
Unload 相對簡單,子地圖設MarkPendingKill,P-Level移除StreamingLevel與RemoveFromWorld就可以了。如圖所示。
Unload sublevel.
CollectGarbage
為了避免持續讀取地圖造成記憶體不足,每處理完一個子地圖就呼叫
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS)
可以釋放記憶體。
整合以上重點項目後的最後流程
1. AddNavigationSystemToWorld
2. For each Sublevel
Load Sublevel
Initialize Sublevel
NavigationSystem::Build
Save Sublevel
Unload Sublevel
CollectGarbage
結論與未來工作
結論
藉由這次的項目,我學到很多新的觀念,包含了解navigation mesh,commandlet,package操作,world composition等系統。
navigation mesh相關的部份,我得知了navigation的build到底是如何運作的;需要哪些資訊才能正確build path;
程式碼在哪邊;nav mesh的儲存規則;子地圖的offset機制。
commandlet則是學到如何在commandlet模式讀取一個地圖檔;根據地圖檔產生UWorld;了解ResavePackage執行的項目;
為了能存檔讀檔,UPackage、ULevel、ULevelStreaming等資料型態也概略的看過,稍微分辨得出差異。
World Composition系統雖然很大,從子地圖資料的抽取;子地圖載入/卸載;如何獲得Tile資訊,也都包含在這次的研究範圍。
未來工作
以目前的版本來說,已經達到我原先想作的目標了,不過我依然有注意到一些項目是可以再進一步改進的。
首先就是這個Commandlet儲存出來的地圖,在World Composition預覽會變成預設圖。
而原來在編輯器操作並儲存的版本預覽圖會是正確的。
日後如果真的要使用這套流程的話應該要修正。
commandlet如果遇到checkout失敗要怎麼辦,也是可以再延伸的課題。
另外一個重要的項目就是,大型場景不會那麼乾淨,只有一張地形檔,一次只需要計算一個Sublevel。
實際上可能會再細分企劃場景(內含有碰撞會影響nav mesh的Actor),建築物,零碎物件等等。
但是在計算nav mesh的時候要將這些地圖一起納入再算,才會是正確的結果。
所以除了開發之前要制定良好地圖資料夾規範以外,commandlet也要隨著這個規範稍作修改。
要能支援多階層的地圖結構,並且某一個階層下的地圖會把所有subLevel讀取進來,統一build。
舉例來說,地圖結構可能如圖所描述:
這時候navigation mesh應該是存於X0_Y0以及X0_Y1內,計算兩次就好。
最後,其實這次對World Composition的了解還不夠深入;包含調整地圖讀取的優先順序;
如何確保地形載入後再spawn玩家;Sublevel LOD的產生與影響。
都是還沒了解的部份,有實際需求的話是要優先研究的。
留言
張貼留言