Recursive AI

Migrating to onepath

All onepath docs

For code that uses Recast and Detour (C++), DotRecast (C#) or Unity's built-in navigation. It maps concepts and types, and calls out the places where a number means something different. The mapping comes from the three projects' published API documentation; where you depend on an exact behaviour of the old library, check it against that library's own documentation.

Three differences run through everything:

  • Units are world units. Recast's build settings count in voxels (cells); onepath's NavSettings are metres-style world units, and the library does the conversion. Pass AgentRadius = 0.6, not walkableRadius = 2.
  • Coordinates are double at the public boundary (Vector3D from RecursiveAI.OneMath), exact within ±2^21, with Y up. Positions beyond that bound throw ArgumentOutOfRangeException. Convert engine vectors with Vector3D.FromSystemNumerics(v) and vector3D.ToSystemNumerics(); NavMesh.Build(ReadOnlySpan<float> vertices, ReadOnlySpan<int> indices, settings) takes the float arrays engines hold (the float to double step is exact).
  • Mesh bytes are not interchangeable. A Detour or DotRecast navigation-mesh file is not read by onepath (it fails with a typed NavMeshFormatException, a kind of OnePathFormatException: bad magic), and onepath's files are not read by them. Re-bake from your geometry with NavMesh.Build(...); there is no converter, because the polygon sets differ (see navmesh-format.md).

Building a mesh

One call replaces the Recast pipeline (heightfield, compact heightfield, regions, contours, polygon mesh, detail mesh, mesh data, nav mesh):

C#
NavMesh mesh = NavMesh.Build(vertices, indices, NavSettings.Humanoid with { AgentRadius = 0.4, CellSize = 0.25 });

Each stage is also callable on its own in tier 2 (Mesh3DPipeline, build3d-spans.md onward).

Recast / DotRecast build settingUnit thereonepath (NavSettings)Unit here
cs (cell size)world unitsCellSize (default 0.3)world units
ch (cell height)world unitsCellHeight (0.2)world units
walkableSlopeAngledegreesAgentMaxSlopeDegrees (45)degrees, 0 to 90
walkableHeightvoxelsAgentHeight (2)world units
walkableClimbvoxelsWalkableClimb (0.9)world units
walkableRadiusvoxelsAgentRadius (0.6)world units
maxEdgeLenvoxelsMaxEdgeLength (3.6)world units; 0 disables
maxSimplificationErrorvoxelsContourMaxError (1.3)cells (same meaning)
minRegionAreacells; the samples pass a side length and square itMinRegionArea (64)cells, an area: pass 8 * 8, not 8
mergeRegionAreacells; same conventionMergeRegionArea (400)cells, an area: pass 20 * 20
maxVertsPerPolyverticesMaxVertsPerPoly (6)vertices, 3 to 16
detailSampleDistthe samples multiply by csDetailSampleDistance (1.8)world units; 0 turns height detail off
detailSampleMaxErrorthe samples multiply by chDetailMaxError (0.2)world units
tileSize, borderSizevoxelsTileSize (64)world units; the border is internal
partition (watershed, monotone, layers)enumPartitionPartitionType
bmin, bmaxworldOriginX, OriginZtile grid origin; the heightfield bounds come from the input

Flags and areas: Detour polygons carry a 16-bit flags value and a 6-bit area id; so do onepath's (NavSettings.Flags, Area for a 2D build; areas from SpanSettings and volumes in 3D; 64 areas). Area costs are multipliers in a QueryFilter (WithAreaCost(area, cost)), not a per-area array on the query. Off-mesh connections are NavSettings.OffMeshLinks (OffMeshLinkDefinition).

ConceptRecast / Detour / DotRecastonepath
Triangle inputrcRasterizeTriangles over float[]NavMesh.Build(float or double vertices, indices), Triangle3D, NavMesh.BuildGltf
Voxel inputheight field built by handNavMesh.Build(VoxelChunk), NavMesh.Build(SolidHeightfield)
Tile map / 2D outlinesnot supportedNavMesh.Build2D(...) (exact, no voxels)
Tiled meshdtNavMesh plus dtNavMeshCreateParams per tileNavMesh is tiled by NavSettings.TileSize; TileStreamer and NavWorld stream and mix agent sizes
Several agent sizesone mesh per size, by handNavWorld (one mesh per agent class)
Run-time obstacles and area volumestile cacheTileCache
Save and loaddtNavMesh data blobsNavMeshSerializer, snapshots in navmesh-snapshot.md

Queries

Detour / DotRecastonepathNotes
findNearestPoly(center, halfExtents, filter)NavMeshQuery.FindNearestPoly(center, halfExtents, filter)Returns NearestPoly (poly reference and point)
findPath then findStraightPathNavMesh.FindPath(start, end, options)One call, returns PathResult with Points, Status, IsPartial; options carry the filter and search extents (default half-extents 2, 4, 2). Tier 2: BeginPath/UpdatePath/FinishPath and FindStraightPath
sliced path query (initSlicedFindPath, updateSlicedFindPath, finalizeSlicedFindPath)BeginPath, UpdatePath(maxIterations), FinishPathThe budget counts iterations, never time; slicing gives the same result as one call
raycastNavMesh.Raycast(from, to, out NavMeshHit, filter); NavMeshQuery.RaycastNavMeshHit has position, normal and distance
moveAlongSurfaceNavMeshQuery.MoveAlongSurface
findRandomPoint, findRandomPointAroundCircleNavMesh.TryGetRandomPoint(ref SeededRandom, ...), NavMeshQuery.RandomPoint/RandomPointAroundThe generator is passed in and seeded: there is no hidden random state
findDistanceToWall, findPolysAroundCircle, findLocalNeighbourhoodDistanceToWall, FindPolysInCircle, FindPolysInShape, FindLocalNeighbourhood
closestPointOnPolyClosestPointOnPoly
dtQueryFilter (include/exclude flags, area costs)QueryFilterImmutable; new QueryFilter { ExcludeFlags = 2 }.WithAreaCost(3, 5). Struct form for the zero-allocation span queries: DefaultQueryFilter, or your own IQueryFilter
dtStatus bit flagsQueryStatus, PathStatusEnums, not bit masks; see path-results.md
Poly reference dtPolyRefPolyRef64-bit (24 bits salt, 20 slot, 20 polygon)

A path that cannot reach the goal returns the corridor to the nearest reachable polygon with IsPartial = true, as Detour does.

Crowds

dtCrowd/DtCrowd ≈ Crowd (mesh.CreateCrowd()); dtCrowdAgentParams ≈ AgentOptions (Agent.Humanoid, Small, Large).

Detour crowd parameteronepathNotes
radiusAgentOptions.RadiusBody radius between agents; the mesh is already shrunk by its build radius
heightNavSettings.AgentHeightBuild time, not per agent
maxSpeed, maxAccelerationMaxSpeed, MaxAccelerationunits per second, per second squared
collisionQueryRangeCrowdOptions.MaxWallsPerAgent, WallRefreshDistance; neighbours by Quality
pathOptimizationRangeFollowerOptions.OptimizeRange (through AgentOptions.Path)
separationWeightSeparationDistance and SeparationWeightNot the same scale. Detour's weight is a non-negative number (the samples use values such as 2); onepath's SeparationWeight is 0 to 1, the fraction of MaxSpeed a touching neighbour adds to the wanted velocity (default 0.3), and separation is off until SeparationDistance is above 0. Values above 1 throw. Start with SeparationDistance = 2 * Radius and tune
obstacleAvoidanceTypeQuality: Off, Low, Medium, HighPresets for neighbour count and time horizon
queryFilterTypeCrowdOptions.Filter (one filter per crowd)
updateFlags (anticipate turns, optimize visibility, optimize topology, obstacle avoidance, separation)FollowerOptions intervals and budgets; Quality; SeparationDistanceA feature is off when its interval, distance or quality is 0
requestMoveTargetAgent.SetDestination(position) → PathStatusReturns GoalInvalid if the goal is not near the mesh
update(dt)Crowd.Step(dt)Fixed dt is the supported use; the crowd has no clock
getActiveAgentsforeach (Agent a in crowd), CopyAgents(span)Slot order
addAgent returning an indexCrowd.Add(position, options) returns an AgentThrows if the position is off the mesh or the crowd is full

The velocity solver is ORCA (van den Berg et al.), made aware of mesh edges; it is not the sampled velocity-obstacle search Detour uses, so agent paths differ in detail even with matching numbers. Details in crowds.md and orca.md.

From Unity's navigation

Unityonepath
NavMeshSurface / NavMeshBuilder.BuildNavMeshDataNavMesh.Build(vertices, indices, settings), or NavMesh.BuildGltf; Unity's float[] and Vector3[] meshes convert with Vector3D.FromSystemNumerics
Agent Type (radius, height, step height, max slope)NavSettings (AgentRadius, AgentHeight, WalkableClimb, AgentMaxSlopeDegrees); one mesh per size through NavWorld
NavMesh.SamplePosition(point, out hit, maxDistance, areaMask)NavMesh.TrySamplePosition(position, maxDistance, out point, filter)
NavMesh.CalculatePath(a, b, areaMask, path)NavMesh.FindPath(a, b, options); the path is PathResult.Points and Status tells complete from partial
NavMesh.RaycastNavMesh.Raycast(from, to, out NavMeshHit, filter) (returns true when the ray is blocked, like Unity)
Area costs and areaMaskQueryFilter.WithAreaCost and IncludeFlags/ExcludeFlags
NavMeshAgent.speed, acceleration, radius, avoidancePriorityAgentOptions.MaxSpeed, MaxAcceleration, Radius, Priority (greater priority gives way less, the opposite direction to Unity's lower-number-wins avoidancePriority)
NavMeshAgent.stoppingDistanceAgentOptions.ArrivalRadius
NavMeshAgent.SetDestination, remainingDistance, velocity, hasPathAgent.SetDestination, RemainingDistance, Velocity, HasPath
NavMeshAgent.WarpAgent.Teleport(position) (returns false when off the mesh)
NavMeshAgent moving the transformYou move your own objects: read Agent.Position after crowd.Step(dt)
NavMeshObstacle (carving)TileCache obstacles
OffMeshLinkNavSettings.OffMeshLinks; with OffMeshMode.Host the host drives the crossing and calls Agent.CompleteOffMeshLink()
Per-frame Update with Time.deltaTimecrowd.Step(fixedDt): use a fixed step, not the frame time, if you need identical results on every machine

Unity's navigation is not deterministic across platforms; onepath's is. If your game relied on float quirks of the old library (a path that happened to cut a corner), expect small differences and re-tune against the settings above.