看板 GameDesign 關於我們 聯絡資訊
網頁版 https://yekdniwue.blogspot.com/2020/07/AutoBuildNavMesh.html Build Static Navigation Mesh in World Composition 簡介 先前有提到因為地圖被細切成很多張的關係,想要使用static nav mesh, 有幾個困難處要解決: 1. 地圖範圍很大的時候,是沒辦法一次載入所有地圖, 只按一次build path就完成的。 不僅會執行很久,也會遇到build path失敗的情況。 失敗會有警告訊息並且有部分nav mesh不完整。 2. 如果想要每張地圖各別計算,要在編輯器內重複的 讀取子地圖 build path 子地圖存檔 卸載子地圖 這樣的流程其實更適合用自動化來做。 名詞與縮寫說明 在開始之前,先介紹一些本篇文章會用到的名詞或是縮寫。 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的部分就一律交給機器更新。 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的產生與影響。 都是還沒了解的部份,有實際需求的話是要優先研究的。 -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 59.120.146.90 (臺灣) ※ 文章網址: https://www.ptt.cc/bbs/GameDesign/M.1595387022.A.63E.html
metallican: 每次都很期待大大的文章 感謝 受教了 07/22 11:38
damody: 讚!感覺下一步要做自動生地形了 07/22 21:55
coolrobin: 只能推了 07/23 21:53