This document is about: QUANTUM 3
SWITCH TO

Custom Navmesh Generation

在運行時更改和構建 Quantum 導航網格具有挑戰性。未來版本的 Quantum 導航將著重於動態靈活性。目前有一些更具動態性的尋路解決方案,它們與程序式運行時關卡生成更兼容,例如基於瓦片的尋路器、流場等。

對於程序生成的導航網格,有兩種可行的方法:

  • 在一個客戶端上生成導航網格,然後共享給其他客戶端
  • 確定性地生成BakeData,並在每個客戶端本地烘焙

生成和共享導航網格

一個客戶端使用 Unity 工具烘焙導航網格,然後共享給其他每個客戶端。由於烘焙過程耗時且影響較大,最好在任何客戶端啟動 Quantum 模擬之前同步完成。

  • 烘焙 Unity 導航網格
  • MapNavMesh.ImportFromUnity()
  • MapNavMeshBaker.BakeNavMesh()
  • 上傳到文件服務器
  • 從文件服務器下載
  • 替換導航網格
  • 啟動Quantum

可以通過使用admin Photon 客戶端或利用自定義後端來選擇生成導航網格的客戶端。

我們之前推薦使用 Quantum 資產注入和 DynamicAssets 作為自訂和共享導航網格的工具,這仍然是可行的,但它只在導航網格資產較小(例如小於 1MB)時效果良好,否則在啟動期間發送大量數據會給即時協議帶來很大的網絡負擔。

替換導航網格二進制數據

C#

// Make sure to dispose the UnityDB after each game
UnityDB.Dispose();

// Load the pre-existing navmesh data asset
var navmeshData = UnityDB.FindAsset<BinaryDataAsset>(4350045557267041182).Settings;

// This would need to be replaced by a downloaded file from a server
var newNavmeshData = UnityEngine.Resources.Load<BinaryDataAsset>("NewNavmeshData");

// Replace the navmesh data that is loaded with the pre-existing navmesh asset
// Grid and world sizes etc must match, regions will be added to the map correctly
navmeshData.Data = newNavmeshData.Settings.Data;

// QuantumRunner.StartGame()

確定性生成導航網格 BakeData

這種方法不需要 Unity 導航網格烘焙。相反,它通過程序生成導航網格的部分內容,然後烘焙成 Quantum 導航網格。因為輸入數據(BakeData)本身已是導航網格,而非 Unity 導航網格生成所用的碰撞體集合,所以當導航網格可以由預先生成的部分拼接而成,或者三角形可以按簡單模式生成時,這種解決方案效果很好。有關 BakeData 對象的內部結構,請參見下面的部分。

要求:

  • MapNavMesh.BakeData的創建需要具有確定性

每個客戶端以確定性的方式單獨生成 BakeData,這消除了與其他客戶端共享它的必要性,但後來加入的客戶端也必須經歷這個過程。

生成的 BakeData 將用於生成 Quantum 導航網格。

C#

// Bake navmesh asset
var navmesh = NavmeshBaker.BakeNavMesh(f.Map, bakeData);

結果也可以像前面的部分那樣替換一個虛擬導航網格數據資產,或者用運行時導航網格替換現有的導航網格。

在啟動前替換運行時導航網格

以這種方式替換資產必須在通過UnityDB加載導航網格之前以及模擬開始之前完成。加載 Unity 導航網格資產是為了替換其中的 Quantum 資產(.Settings),並復制GuidPath值。使.DataAsset無效將防止 Quantum 最終加載二進制導航網格資產(_data資產)時對其進行反序列化。

C#

// BakeData needs to be procedurally generated
var bakeData = default(NavmeshBakeData);
var newNavmesh = NavmeshBaker.BakeNavMesh(map, bakeData);

// Load navmesh asset to replace
var navmeshAsset = UnityEngine.Resources.Load<NavMeshAsset>("DB/TestNavMeshAgents/NavMeshToReplace");

// Replace the navmesh content
newNavmesh.Guid = navmeshAsset.Settings.Guid;
newNavmesh.Path = navmeshAsset.Settings.Path;
navmeshAsset.Settings = newNavmesh;

// Invalidate the navmesh data (because it has been generated during runtime it's already loaded)
navmeshAsset.Settings.DataAsset.Id = AssetGuid.Invalid;
navmeshAsset.Settings.Name = "MyNavmesh";

// QuantumRunner.StartGame()

在啟動期間替換運行時導航網格

在這個代碼示例中,在OnGameStartOnGameResync回調期間,已加載的虛擬導航網格被運行時生成的導航網格替換。

通過這種方式也可以在遊戲期間替換導航網格:

  • 只能在經過驗證的幀期間
  • 所有烘焙導航網格的信息和參數必須在該幀上可用(例如單例組件或全局變量),以便後來加入的玩家在重新同步時能夠生成正確的導航網格。

C#

namespace Quantum {
  using Quantum.Experimental;

  public class RuntimeNavmeshBaking : QuantumCallbacks {
    public override void OnGameStart(QuantumGame game) {
      // In this sample the bake data has already been created, but it should be assembled during runtime
      var bakeData = UnityEngine.Resources.Load<BakeDataSO>("BakeData");
      ReplaceNavmesh(game.Frames.Verified.Map, 1356438205741681193, bakeData.BakeData);
    }

    public override void OnGameResync(QuantumGame game) {
      var bakeData = UnityEngine.Resources.Load<BakeDataSO>("BakeData");
      ReplaceNavmesh(game.Frames.Verified.Map, 1356438205741681193, bakeData.BakeData);
    }

    private static void ReplaceNavmesh(Map map, AssetGuid navmeshGuid, NavmeshBakeData bakeData) {
      var newNavmesh = NavmeshBaker.BakeNavMesh(map, bakeData);
      var navmeshAsset = UnityDB.FindAsset<NavMeshAsset>(navmeshGuid);
      // cannot change name or regions without manipulating the map too
      navmeshAsset.Settings.GridSizeX = newNavmesh.GridSizeX;
      navmeshAsset.Settings.GridSizeY = newNavmesh.GridSizeY;
      navmeshAsset.Settings.GridNodeSize = newNavmesh.GridNodeSize;
      navmeshAsset.Settings.WorldOffset = newNavmesh.WorldOffset;
      navmeshAsset.Settings.MinAgentRadius = newNavmesh.MinAgentRadius;
      navmeshAsset.Settings.Triangles = newNavmesh.Triangles;
      navmeshAsset.Settings.TrianglesGrid = newNavmesh.TrianglesGrid;
      navmeshAsset.Settings.Vertices = newNavmesh.Vertices;
      navmeshAsset.Settings.BorderGrid = newNavmesh.BorderGrid;
      navmeshAsset.Settings.TrianglesCenterGrid = newNavmesh.TrianglesCenterGrid;
      navmeshAsset.Settings.Borders = newNavmesh.Borders;
      navmeshAsset.Settings.Links = newNavmesh.Links;
    }
  }
}

namespace Quantum {
  using Quantum.Experimental;
  using UnityEngine;

  public class BakeDataSO : ScriptableObject {
    public NavmeshBakeData BakeData;
  }
}

MapNavMesh.BakeData

中間導航網格格式MapNavMesh.BakeData是一個確定性的網格,構建為帶有附加數據的三角形條帶,在運行時可以通過MapNavMeshBaker.BakeNavMesh(MapData data, MapNavMesh.BakeData navmeshBakeData)轉換為 Quantum 導航網格資產。

MapNavMesh.BakeData類

類型字段描述
StringName導航網格的名稱,可在模擬中通過f.Map.NavMeshes[name]存取
Vector3Position導航網格的位置。最終的導航網格頂點以全局空間存儲,在烘焙期間它們的位置會由此位置進行平移。
FPAgentRadius導航網格所針對的最大代理的半徑。舊版本的Quantum允許不同的代理半徑,但現在已取消。現在,代理可以一直走到它們的樞軸點位於導航網格邊緣的位置。這樣,代理應該遠離牆壁的邊距就被烘焙到三角形中。這個值只用於渲染調試圖形。
List<string>Regions此導航網格中使用的所有區域ID。在烘焙期間,區域ID將添加到地圖資產的區域列表中,它們的索引被烘焙到導航網格三角形的區域掩碼(NavMeshTriangle.Regions)中。這些區域在地圖上匯總,因為一個地圖可以有多個共享區域ID的導航網格。
MapNavMeshVertex[]Vertices導航網格的頂點。
MapNavMeshTriangle[]Triangles導航網格的三角形。這是一種常規的網格數據結構,其中三角形和頂點分別存儲在兩個數組中,三角形指向頂點數組以標記其3個頂點。
MapNavMeshLink[]Links同一導航網格上不同位置之間的鏈接。
enumClosestTriangleCalculationQuantum導航網格使用網格進行空間分區。每個網格單元會分配一個備用三角形。默認的搜索相當慢(BruteForce),而SpiralOut更高效,但可能會導致備用三角形為空。
intClosestTriangleCalculationDepth搜索要擴展的網格單元數量。
boolEnableQuantum_XY啟用時,導航網格烘焙將翻轉頂點位置的Y和Z分量,以支持在XY平面中生成的導航網格。
boolLinkErrorCorrection在烘焙期間自動將導航網格鏈接位置校正到最近的三角形。

MapNavMeshTriangle類

三角形預期為順時針繞組順序。
並非所有字段都需要填寫。其中一些只用於舊式導航網格繪製工具。

類型字段描述
StringId非必需
String[]VertexIds長度必須為3。以ID形式引用的頂點。SDK 2.1或更早版本需要。
Int32[]VertexIds2長度必須為3。以頂點數組中的索引形式引用的頂點。SDK 2.2需要。
Int32Area非必需
StringRegionId此三角形所屬的區域。默認為null
FPCost三角形的成本。默認值應為FP._1

MapNavMeshVertex類

在 SDK 2.2 中,Position的類型已替換為FPVector3.

類型字段描述
StringIdSDK 2.1或更早版本需要
Vector3Position頂點的位置
List<Int32>Neighbors非必需
List<Int32>Triangles非必需

在 SDK 2.2 中,Start, EndCostOveride的類型分別替換為FPVector3FP

類型字段描述
Vector3Start鏈接的起始位置。必須位於同一導航網格上。
Vector3End鏈接的結束位置。必須位於同一導航網格上。
boolBidirectional鏈接是否可以從兩個方向穿越。
floatCostOverride連接的成本。
StringRegionId鏈所屬的區域ID。默認為null
StringName鏈接的名稱。可以通過navmesh.Links[NavMeshPathfinder.CurrentLink()].Name查詢。

代碼片段

C#

// Generate simple navmesh BakeData
var bakeData = new MapNavMesh.BakeData() {
  AgentRadius = FP._0_20,
  ClosestTriangleCalculation = MapNavMesh.FindClosestTriangleCalculation.SpiralOut,
  ClosestTriangleCalculationDepth = 1,
  Name = "DynamicNavmesh",
  PositionFP = FPVector3.Zero,
  Regions = new System.Collections.Generic.List<string>(),
  Vertices = new MapNavMeshVertexFP[] {
    new MapNavMeshVertexFP { Position = FPVector3.Forward },
    new MapNavMeshVertexFP { Position = FPVector3.Right },
    new MapNavMeshVertexFP { Position = -FPVector3.Forward},
    new MapNavMeshVertexFP { Position = -FPVector3.Right},
  },
  Triangles = new MapNavMeshTriangle[] {
    new MapNavMeshTriangle { VertexIds2 = new int[] { 0, 1, 2}, Cost = FP._1 },
    new MapNavMeshTriangle { VertexIds2 = new int[] { 0, 2, 3}, Cost = FP._1 }
  }
};
Back to top