Recursive AI

onepath documentation

All onepath docs

The documents here are normative: where a paper leaves a choice open (tie-breaks, open-list order, rounding, byte layouts, budget units, preset values) the choice is written down so that a port to another language can reproduce the same bits from the text alone. API reference is in the XML documentation that ships with the package; the README has the quickstart and the speed table.

The library has three tiers, each built on the one below:

  1. Happy path. Grid.FromText(...) or Grid.From(...) then .FindPath(a, b), HexGrid, VoxelGrid, Graph, PointGraph, NavMesh.Build(...), NavMesh.Build2D(...), NavMesh.BuildGltf(...), mesh.FindPath(a, b), mesh.CreateCrowd(), crowd.Add(...), agent.SetDestination(...), crowd.Step(dt). Everything is an options object with presets (GridOptions, NavSettings.Humanoid, Agent.Humanoid) or a plain delegate. Namespace RecursiveAI.OnePath. You never write a struct.
  2. Middle. Each build stage as a visible, replaceable static method; one NavMeshQuery with filters, area costs and sync, span and sliced forms; grid and graph searches with struct-generic cost and heuristic hooks; corridors and path followers; tile cache; tile streaming. Namespaces .Grids, .Graphs, .Mesh.*, .Crowds.Corridors.
  3. Raw. Voxel spans, regions, contours, polygons, tile data, the open and closed sets, the search loop, the ORCA solver (.Mesh.Build3D, .Mesh.Build2D, .Mesh.Format, .Grids engines, .Crowds.Orca).

Where to start

You want toRead
Move from Recast, Detour, DotRecast or Unity's navigationmigrating.md
Know what a path result contains and how statuses map across grids, graphs and meshespath-results.md
Search a tile map, hex map or voxel worldgrids.md
Route over a weighted or point graph, with landmarksgraphs.md
Build a navmesh from a tile map or outlinesbuild2d-tiles.md
Build a navmesh from level geometrybuild3d-polys.md
Query a mesh (path, raycast, nearest, random point)navmesh-queries.md
Run a crowdcrowds.md, then orca.md for the solver
Run inside a lockstep or rollback hostHost hooks below

By area

Grids and graphs (tier 1 and 2)

DocumentCovers
grids.mdSquare (4- and 8-connected), hex and 3D voxel grids; A*, JPS, JPS+, Theta*, Lazy Theta*, HPA*; LPA* and D* Lite replanners; flow fields; area costs; agent clearance; tie-breaking and open-list order; incremental tile updates; cost and heuristic hooks
graphs.mdWeighted and point graphs, A*, bidirectional search, ALT landmarks and their serialization
grids-state.mdByte-exact save and restore of grid, graph, replanner, flow-field and sliced-search state
DocumentCovers
navmesh-format.mdThe tiled mesh (format version 2, frozen): tile layout, polygons, detail heights, areas and flags, off-mesh links, several agent sizes, streaming, serialization, build fingerprint
navmesh-queries.mdNavMeshQuery: path (polygon and crossing node models), string pulling, raycast, nearest polygon and point, random point, distance to wall, shape queries, sliced queries
navmesh-snapshot.mdByte-exact snapshot and restore of a dynamic mesh (rebuilt tiles, obstacles)
DocumentCovers
build2d-geometry.mdThe exact 2D builder's geometry: snap grid, regions, boolean operations, offsets and shrink, constrained Delaunay, convex merge, exact predicates
build2d-tiles.mdTile slicing, plane-to-mesh mapping and rounding, NavMesh.Build2D
build3d-spans.md3D builder, front half: voxelization of triangle soup and voxel chunks, span filters, compaction, erosion
build3d-regions.mdRegions (watershed, monotone, layers) and contours
build3d-polys.mdPolygons, per-polygon height detail, tiles, NavMesh.Build
build3d-gltf.mdThe glTF 2.0 reader and NavMesh.BuildGltf
build3d-runtime.mdRun-time tile cache: tile rebuild, temporary obstacles, area volumes

Crowds

DocumentCovers
crowds.mdPath corridors, path following, off-mesh links, Crowd and Agent, separation, priorities, avoidance quality, crowd snapshot bytes
orca.mdThe ORCA solver: constraints, linear program, neighbour order, where the paper is silent

Conventions across the tiers

  • Options are sealed records, immutable once constructed (GridOptions, VoxelOptions, PathOptions, NavSettings, CrowdOptions, AgentOptions, GraphOptions, PointGraphOptions, LandmarkOptions, FollowerOptions): properties are init-only, so a shared preset cannot be changed by one caller under another, and with derives a changed copy (GridOptions.FourConnected with { Algorithm = GridAlgorithm.Jps }). There is no Clone(). Two options with the same values are equal (delegates compare by reference). A list or array an option holds (NavSettings.OffMeshLinks, PointGraphOptions.Links, Mesh3DPipeline.Obstacles) is copied when it is set, so editing the caller's array afterwards changes nothing. QueryFilter is immutable too (WithAreaCost copies). Settings classes of the middle and raw tiers (stage settings of the 3D builder, for example) stay mutable builders.
  • One validation point. A value is checked where it is set: new GridOptions { AgentSize = 0 } or Agent.Humanoid with { MaxSpeed = -5 } throws ArgumentOutOfRangeException right there, with ParamName the property and a message that states the allowed range. Rules that tie several values together (a tile size against the cell size, a filter against a mesh) are checked when the options are used and name the properties involved. Validate() on the options types repeats the field checks for values built another way.
  • Cancellation: every tier-1 FindPath takes an optional CancellationToken and throws OperationCanceledException; a token that is never cancelled changes no result.
  • FindPathAsync means one thing everywhere (Grid, NavMesh, NavMeshQuery, NavWorld): the signature is (start, goal, options, cancellationToken) and the search runs cooperatively on the caller's synchronization context in slices of GridOptions.IterationsPerSlice / PathOptions.IterationsPerSlice iterations, yielding between slices. The slices count iterations, never time, and any slicing returns what FindPath returns. Nothing moves to the thread pool, so Blocked, StepCost and filter delegates always run on the thread that awaits. To search on another thread, call FindPath inside Task.Run (and do not edit the world meanwhile). Hex and voxel grids have no async form for that reason. Grid.FindPathAsync slices A*, JPS and JPS+ (the algorithms GridSearch can resume) and refuses the others and RecordExpansions with an ArgumentException.
  • Errors. Misuse (a bad argument, a call in the wrong order) throws ArgumentException (ArgumentOutOfRangeException, ArgumentNullException) or InvalidOperationException. Bad data (bytes, text, files, geometry the builder cannot use) throws a subclass of OnePathFormatException (itself a FormatException), so a host that loads untrusted saves catches one type: NavMeshFormatException (with NavMeshVersionException and NavMeshFingerprintException) for meshes, tiles and snapshots, GridStateException and GraphStateException for saved grid, search, replanner, flow-field and graph state and landmark tables, CrowdStateException for crowd, corridor and follower state, GridFormatException for map and scenario text, GltfException for glTF input and GeometryException for a 2D builder invariant failure. Each message names its subject, the offset or field, and what was wrong. A data error is never an InvalidOperationException or ArgumentException. Expected outcomes of a query (no path, a start off the mesh) are statuses, not exceptions.
  • Where the types live. Everything the happy path names, including PathResult, PathOptions, QueryFilter, FollowerState, PartitionType, PathNodeModel, StraightPathPoint and the status enums, is in RecursiveAI.OnePath: one using (plus OneMath's vectors) is enough for the quickstart. Tier 2 and 3 types are in the sub-namespaces.
  • Enumerating a crowd: foreach (Agent a in crowd) and crowd.CopyAgents(Span<Agent>) allocate nothing; both go in slot order.
  • Listeners: AddListener(IGridChangeListener) and AddListener(IVoxelChangeListener) on the tier-1 grids are observer registration, not search hooks: a class implements the interface, never a struct, and nothing on the happy path needs one. They are the only library interfaces the tier-1 surface test allows in the root namespace. Replanners and flow fields implement the callbacks explicitly, so OnCellChanged is not a method you can call on them.

Host hooks for deterministic hosts

A lockstep or rollback host needs five things; each has its own tests and is documented where it lives.

HookWhatDocument
Coordinate ruleWorld positions are double, exact within ±2^21 (a Q31.32 fixed-point value converts exactly); beyond that ArgumentOutOfRangeException. Meshes store tile-local float32 relative to a double tile originnavmesh-format.md, grids.md
Snapshot and restoreSame state, same bytes, on every platform: grids, searches, replanners and flow fields; dynamic meshes; crowdsgrids-state.md, navmesh-snapshot.md, crowds.md
Fixed-dt steppingCrowd.Step(dt) with a caller-supplied step; no internal clockcrowds.md
Iteration budgetsSliced grid, graph, mesh and tile-cache work counts iterations, never time; the unit of one iteration per family is stated in each documentgrids.md, grids-state.md, navmesh-queries.md, build3d-runtime.md
Debug geometryPolygons, links, paths, corridors, agent velocities and grid state as plain data (namespace RecursiveAI.OnePath.Debug)navmesh-format.md, crowds.md, grids-state.md

Saved formats and their fixtures

Every 1.x release reads every format written by an earlier 1.x release: navigation meshes and tiles, mesh and tile-cache snapshots, crowd state (with and without the mesh), path corridors and followers, grid, hex and voxel state, search, replanner and flow-field state, graphs and landmark tables. Reading is backward only; a later release may write a newer version marker.

The rule that keeps it true:

  • A change to a format bumps that format's version marker. The reader keeps reading every earlier 1.x version.
  • Each release has a directory tests/OnePath.Tests/Fixtures/<release>/ of small files its own code wrote (made by tools/FormatFixtures from generated scenes, with a fixtures.tsv manifest). A fixture is never regenerated or edited. A release that adds or changes a format adds a directory of its own and appends it to the list in FormatFixtureTests.
  • The expectation recorded beside each fixture is what the loaded state does, not its bytes: queries, searches, steps and edits after the load are run and hashed (SHA-256 of a transcript written only through the public API). Re-writing and comparing bytes would break the first time a release writes a newer marker.
  • FormatFixtureTests loads every fixture of every release with the current readers and compares. It also lists every public method that returns a byte array or fills a Span<byte>, and fails for one that has no fixture, so a new saved format cannot ship without one.

Determinism and measurement

DocumentCovers
hash-matrix.mdThe manifest format and the columns (JIT, scalar, threads, NativeAOT, WASM, ...) that must produce equal hashes
benchmarking.mdHow the speed rows are measured: warm-up, best of N, references and their versions, quality metrics, memory