This document is about: QUANTUM 3
SWITCH TO

Custom Navmesh Generation

Changing and constructing the Quantum navmesh during run-time is challenging. Other pathfinding solutions are more compatible with procedural run-time level generation, for example tile based pathfinding or Flow Fields.

For procedurally generated navmeshes two approaches are feasible:

  • Generate the navmesh on one client and share it with the others
  • Deterministically generate NavMeshBakeData and bake locally on each client

Generating and Sharing A Navmesh

One client bakes the navmesh using the Unity tools and then shares it with each other client. Because of its lengthy and impactful baking process it is best to synchronize this before any client started the Quantum simulation.

  • Bake the Unity navmesh
  • Import it into NavMeshBakeData with QuantumNavMesh.ImportFromUnity()
  • Bake the Quantum navmesh with NavMeshBaker.BakeNavMesh()
  • Serialize the navmesh and upload it to a file server
  • Download the navmesh data on the other clients
  • Replace the navmesh data
  • Start Quantum

Selecting the client to generate the navmesh can be done by using the admin Photon client or utilizing a custom backend.

Quantum Asset Injection and DynamicAssets can also be used to customize and share navmeshes, but this only works well when the navmesh asset is small (e.g. less than 1MB), otherwise sending so much data during startup causes a lot of network friction with the Realtime protocol.

Serialize The Navmesh

A baked navmesh is serialized into the same format that is stored in the navmesh data asset (the _data asset next to the navmesh asset).

C#

var navmesh = NavMeshBaker.BakeNavMesh(map, bakeData);

var stream = new ByteStream(new byte[1024 * 1024 * 10]);
navmesh.Serialize(stream, true);
var navmeshBytes = stream.ToArray();
// upload navmeshBytes

Navmeshes with the NavMesh Serialize Type BakeDataOnly only store the bake data and bake the navmesh when it is loaded, which reduces the data to share, see Importing A Unity Navmesh.

Replace The Navmesh Data

The navmesh deserializes its data asset when it is loaded and unloads the data asset afterwards. Replace the data before the navmesh is loaded, which is before the simulation starts.

C#

// Load the pre-existing navmesh data asset
var navmeshData = QuantumUnityDB.GetGlobalAsset(new AssetRef<BinaryData>(navmeshDataGuid));

// Replace it with the downloaded data
// Grid and world sizes etc must match, regions will be added to the map correctly
navmeshData.SetData(downloadedNavmeshBytes, compress: false);

// await SessionRunner.StartAsync(arguments)

Deterministically Generate Navmesh BakeData

This approach does not require the Unity navmesh baking. Instead, it works with parts of the navmesh being procedurally generated and then baked into a Quantum navmesh. Because the input data (NavMeshBakeData) is already a navmesh and not a collection of colliders (as used by the Unity navmesh generation) this solution works well when it can be stitched together from pre-generated navmesh parts or triangles can be generated with a simple pattern. See the section below about the internals of the NavMeshBakeData class.

Requirements:

  • NavMeshBakeData creation needs to be deterministic

The bake data is generated by each client individually in a deterministic way which removes the necessity to share it with others but late-joiners also have to go through this process.

The completed bake data is used to generate a Quantum navmesh, either as a new navmesh asset or baked into an existing one.

C#

// Bake a new navmesh asset, adds missing regions to the map
var navmesh = NavMeshBaker.BakeNavMesh(map, bakeData);

// Or bake into an existing (dummy) navmesh asset that is already referenced by the map
NavMeshBaker.BakeIntoExistingNavMesh(bakeData, existingNavmesh);

Replace The Navmesh During Start

In this code sample, an already loaded dummy navmesh is replaced with a runtime generated navmesh when the game starts or resyncs.

Replacing a navmesh during the game is also possible this way:

  • Only during a verified frame
  • All information and parameters to bake the navmesh have to be available on the frame (e.g. singleton component or globals) for late-joiners to be able to generate the correct navmesh when resyncing.

C#

namespace Quantum {
  using UnityEngine;

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

      // The dummy navmesh is referenced by the map and loaded with it
      var navmesh = game.Frames.Verified.Map.NavMeshes["DummyNavmesh"];

      // The name and the regions cannot be changed without changing the map too
      NavMeshBaker.BakeIntoExistingNavMesh(bakeData, navmesh);
    }
  }

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

The intermediate navmesh format NavMeshBakeData is a deterministic mesh with additional data that is converted to a Quantum navmesh asset by NavMeshBaker.BakeNavMesh(Map map, NavMeshBakeData bakeData) during run-time.

Type Field Description
string Name The name of the navmesh accessible inside the simulation by f.Map.NavMeshes[name].
FPVector3 Position The position of the navmesh. Final navmesh vertices are stored in global space and their positions are translated by this during baking.
FP AgentRadius The radius of the largest agents that the navmesh is created for. Agents can walk up until their pivot is on the edge of the navmesh, the margin agents should keep away from walls is baked into the triangles. This value is only used to render debug graphics.
string[] Regions All region ids that are used in this navmesh. During baking the region ids will be added to the region list of the map asset and their index is baked into the navmesh triangles region mask (NavMeshTriangle.Regions). The regions are aggregated on the map because a map can have multiple navmeshes that share the region ids.
NavMeshBakeDataVertex[] Vertices The vertices of the navmesh.
NavMeshBakeDataTriangle[] Triangles The triangles of the navmesh. The triangles and vertices are kept in two separate arrays and each triangle points into the vertex array to mark its 3 vertices.
NavMeshBakeDataLink[] Links Links between positions on the same navmesh.
NavMeshBakeDataFindClosestTriangle ClosestTriangleCalculation The Quantum navmesh uses a grid for spatial partitioning. Each grid cell will have a fallback triangle assigned. The default search is quite slow (BruteForce) while SpiralOut is more efficient, but it could result in empty fallback triangles.
int ClosestTriangleCalculationDepth The number of grid cells to expand the SpiralOut search.
bool EnableQuantum_XY When enabled the navmesh baking will flip Y and Z components of the vertex positions to support navmeshes generated in the XY plane.

Triangles are expected to have clock-wise winding order.

Type Field Description
int[] VertexIds Must have length of 3. The referenced vertices as indices into the vertex array.
string RegionId The region that this triangle belongs to. Default is null.
FP Cost The cost of the triangle. Default should be FP._1.
int Area The Unity navmesh area, not required.
Type Field Description
FPVector3 Position The position of the vertex.
Type Field Description
FPVector3 Start Start position of the link. Must be on the same navmesh.
FPVector3 End End position of the link. Must be on the same navmesh.
int StartTriangle The triangle index of the start position.
int EndTriangle The triangle index of the end position.
bool Bidirectional Can the link be traversed from both directions.
FP CostOverride The cost of the connection.
string RegionId The region id that the link belongs to. Default is null.
string Name The name of the link. Can be queried by navmesh.Links[pathfinder.CurrentLink(f)].Name.

Snippet

C#

// Generate simple navmesh bake data
var bakeData = new NavMeshBakeData() {
  AgentRadius = FP._0_20,
  ClosestTriangleCalculation = NavMeshBakeDataFindClosestTriangle.SpiralOut,
  ClosestTriangleCalculationDepth = 1,
  Name = "DynamicNavmesh",
  Position = FPVector3.Zero,
  Regions = new string[0],
  Vertices = new NavMeshBakeDataVertex[] {
    new NavMeshBakeDataVertex { Position = FPVector3.Forward },
    new NavMeshBakeDataVertex { Position = FPVector3.Right },
    new NavMeshBakeDataVertex { Position = -FPVector3.Forward },
    new NavMeshBakeDataVertex { Position = -FPVector3.Right },
  },
  Triangles = new NavMeshBakeDataTriangle[] {
    new NavMeshBakeDataTriangle { VertexIds = new int[] { 0, 1, 2 }, Cost = FP._1 },
    new NavMeshBakeDataTriangle { VertexIds = new int[] { 0, 2, 3 }, Cost = FP._1 }
  },
  Links = new NavMeshBakeDataLink[0]
};

Last updated on

Back to top