Recursive AI

Navigation mesh queries

All onepath recipes

Build a navigation mesh from a tile map with OnePath, then find a path around an obstacle, cast a ray along the mesh, snap a point to the nearest walkable spot, and save the mesh to bytes to load at run time.

C#
using RecursiveAI.OneMath;
using RecursiveAI.OnePath;
 
// A 30 x 20 tile map, one unit per cell, with a block in the middle.
var map = Grid.FromText("""
    ..............................
    ..............................
    ..............................
    ..............................
    ..............................
    ..............................
    ...........########...........
    ...........########...........
    ...........########...........
    ...........########...........
    ...........########...........
    ...........########...........
    ..............................
    ..............................
    ..............................
    ..............................
    ..............................
    ..............................
    ..............................
    ..............................
    """);
NavMesh mesh = NavMesh.Build2D(map);
 
PathResult route = mesh.FindPath(new Vector3D(2, 0, 9), new Vector3D(28, 0, 9));
Console.WriteLine($"path: length {route.Length:F4}, {route.Points.Count} points");
foreach (var p in route.Points) Console.WriteLine($"  {p.Position}");
 
// Line of sight along the mesh: does a straight walk hit the block?
bool hit = mesh.Raycast(new Vector3D(2, 0, 9), new Vector3D(28, 0, 9), out NavMeshHit info);
Console.WriteLine($"raycast hit: {hit}, at {info.Position}");
 
// Snap a click to the nearest walkable point.
if (mesh.TrySamplePosition(new Vector3D(15, 0, 9), 5, out Vector3D snapped))
    Console.WriteLine($"nearest walkable to (15, 0, 9): {snapped}");
 
// Bake once, ship the bytes, load at run time (1.1.0).
byte[] baked = mesh.Save();
NavMesh loaded = NavMesh.Load(baked);
PathResult again = loaded.FindPath(new Vector3D(2, 0, 9), new Vector3D(28, 0, 9));
Console.WriteLine($"saved {baked.Length} bytes; loaded mesh gives the same length: {again.Length == route.Length}");
Text
path: length 27.4165, 4 points
  (2, 0, 9)
  (10.79296875, 0, 12.599609375)
  (19.20703125, 0, 12.599609375)
  (28, 0, 9)
raycast hit: True, at (10.400390625, 0, 9)
nearest walkable to (15, 0, 9): (15, 0, 5.400390625)
saved 793 bytes; loaded mesh gives the same length: True

What happened

  • NavMesh.Build2D(grid) treats every non-wall cell as walkable and uses Grid.Space for cell size and origin (one unit per cell here). The 2D builder is exact: it shrinks the walkable area by the agent radius from NavSettings (0.6 for the default Humanoid), which is why the path keeps clear of the block’s corners.
  • FindPath returns the string-pulled path: the start, one point past each corner it has to round, and the goal.
  • Raycast walks the mesh surface in a straight line and reports where it leaves the walkable area, here at the eroded edge of the block.
  • TrySamplePosition returns the nearest walkable point within the distance you give; a click inside the block snaps to its edge.
  • Save and Load (new in 1.1.0) bake a mesh once and ship the bytes. The loaded mesh gives the same bits from every query, and every later 1.x release reads them.

The full query layer, with filters, area costs, random points, distance to walls and sliced queries, is NavMeshQuery: see navmesh queries. For level geometry, NavMesh.Build (triangles) and NavMesh.BuildGltf run the 3D builder into the same format.