Skip to content

Unified Navigation System

NavigationSystem is a unified high-level navigation system that integrates multiple navigation modules:

Path Planning → Flow Control → Local Avoidance → Collision Resolution
↓ ↓ ↓ ↓
A*/NavMesh Queueing ORCA Wall Penetration Fix

Key Features:

  • Pluggable algorithm architecture (path planning, avoidance, collision resolution)
  • Separate handling of static/dynamic obstacles
  • Time-sliced pathfinding (for large-scale agent scenarios)
  • Flow control and queue management

Core Architecture: Clear Primary/Secondary Roles

Section titled “Core Architecture: Clear Primary/Secondary Roles”

NavigationSystem adopts a clear primary/secondary architecture design:

ModuleRoleResponsibility
Path Planner (A*/NavMesh)PrimaryCalculate global paths around static obstacles
Local Avoidance (ORCA)SecondaryHandle dynamic obstacles and agent-to-agent avoidance
Collision ResolverFallbackPrevent wall penetration, handle all obstacles

This design ensures:

  • A*/NavMesh is responsible for global path planning around walls, buildings, and other static obstacles
  • ORCA only handles moving dynamic obstacles (like other NPCs, players), without interfering with the main path
  • Collision resolver serves as the last line of defense, ensuring agents don’t penetrate any obstacles
import {
NavigationSystem,
NavigationAgentComponent,
createNavMeshPathPlanner,
createAStarPlanner,
createORCAAvoidance,
createFlowController,
createDefaultCollisionResolver
} from '@esengine/pathfinding/ecs';
// Create navigation system
const navSystem = new NavigationSystem({
enablePathPlanning: true,
enableLocalAvoidance: true,
enableFlowControl: false,
enableCollisionResolution: true
});
// Set path planner (choose one)
// Option 1: NavMesh (for complex polygon terrain)
navSystem.setPathPlanner(createNavMeshPathPlanner(navMesh));
// Option 2: A* (for grid maps)
navSystem.setPathPlanner(createAStarPlanner(gridMap, undefined, { cellSize: 20 }));
// Set local avoidance
navSystem.setLocalAvoidance(createORCAAvoidance({
defaultTimeHorizon: 2.0,
defaultTimeHorizonObst: 1.0 // Time horizon for dynamic obstacles
}));
// Set collision resolver
navSystem.setCollisionResolver(createDefaultCollisionResolver());
// Add to scene
scene.addSystem(navSystem);

NavigationSystem classifies obstacles into two types:

TypeExamplesHandled ByAPI
Static ObstaclesWalls, buildings, terrainPath planner routes aroundaddStaticObstacle()
Dynamic ObstaclesMoving platforms, destructiblesORCA real-time avoidanceaddDynamicObstacle()
// Add static obstacle (wall) - path planner will route around it
navSystem.addStaticObstacle({
vertices: [
{ x: 100, y: 50 },
{ x: 200, y: 50 },
{ x: 200, y: 70 },
{ x: 100, y: 70 }
]
});
// Add dynamic obstacle (moving platform) - ORCA will avoid in real-time
navSystem.addDynamicObstacle({
vertices: [
{ x: 300, y: 100 },
{ x: 350, y: 100 },
{ x: 350, y: 150 },
{ x: 300, y: 150 }
]
});
// Get obstacle lists
const staticObs = navSystem.getStaticObstacles();
const dynamicObs = navSystem.getDynamicObstacles();
// Clear obstacles
navSystem.clearStaticObstacles();
navSystem.clearDynamicObstacles();
Agent sets destination
┌─────────────────────────────────────────────────────────┐
│ Path Planner (A*/NavMesh) │
│ Input: start, end, staticObstacles │
│ Output: global waypoint list │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Flow Controller (optional) │
│ Detect congestion, manage queuing │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Local Avoidance (ORCA) │
│ Input: preferred velocity, neighbor agents, │
│ dynamicObstacles │
│ Output: safe new velocity │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Collision Resolver │
│ Input: new position, staticObstacles + dynamicObstacles│
│ Output: corrected final position (prevent penetration) │
└─────────────────────────────────────────────────────────┘
Agent moves to new position

Each entity that needs navigation should have NavigationAgentComponent:

import { ORCAConfigComponent } from '@esengine/pathfinding/ecs';
const entity = scene.createEntity('Agent');
const nav = entity.addComponent(new NavigationAgentComponent());
// Configure agent parameters
nav.radius = 0.5; // Collision radius
nav.maxSpeed = 5.0; // Maximum speed
nav.waypointThreshold = 0.5; // Distance threshold for reaching waypoint
// Optional: Add ORCA config for custom avoidance parameters
const orcaConfig = entity.addComponent(new ORCAConfigComponent());
orcaConfig.neighborDist = 15.0; // Neighbor search distance
orcaConfig.maxNeighbors = 10; // Maximum neighbors
orcaConfig.timeHorizon = 2.0; // Agent time horizon
orcaConfig.timeHorizonObst = 1.0; // Obstacle time horizon
// Set destination
nav.setDestination(targetX, targetY);
// Check status
if (nav.hasArrived()) {
console.log('Reached destination!');
}
// Stop navigation
nav.stop();

waypointThreshold controls when an agent advances to the next waypoint:

// Recommended: 2x agent radius, minimum 15
nav.waypointThreshold = Math.max(nav.radius * 2, 15);

Problems with too large value:

  • Agent switches to next waypoint too early
  • At corners, agent may aim at waypoints behind walls, conflicting with ORCA avoidance
  • Results in agent repeatedly turning around at corners

Problems with too small value:

  • Agent movement not smooth enough
  • May pause at waypoints

For complex polygon terrain:

import { createNavMeshPathPlanner } from '@esengine/pathfinding/ecs';
const planner = createNavMeshPathPlanner(navMesh, {
agentRadius: 10 // Agent radius for path smoothing
});
navSystem.setPathPlanner(planner);

For grid-based maps, supports A*, JPS, HPA*:

import {
createAStarPlanner,
createJPSPlanner,
createHPAPlanner
} from '@esengine/pathfinding/ecs';
// A* pathfinder
const astarPlanner = createAStarPlanner(gridMap, undefined, {
cellSize: 20 // Grid cell size (pixels)
});
// JPS pathfinder (uniform-cost grids, 10-100x faster than A*)
const jpsPlanner = createJPSPlanner(gridMap, undefined, {
cellSize: 20
});
// HPA* pathfinder (very large maps 1000x1000+)
const hpaPlanner = createHPAPlanner(gridMap, { clusterSize: 16 }, undefined, {
cellSize: 20
});

When your game uses pixel coordinates while the grid uses cell coordinates, the cellSize parameter handles the conversion automatically:

// Assume grid is 30x20 cells, each cell 20x20 pixels
// Game world size is 600x400 pixels
const gridMap = createGridMap(30, 20);
const planner = createAStarPlanner(gridMap, undefined, { cellSize: 20 });
// Now you can use pixel coordinates directly
// Internally converts: (480, 300) → grid(24, 15) → pixels(490, 310)
nav.setDestination(480, 300);

Conversion rules:

  • Pixel → Grid: Math.floor(pixel / cellSize)
  • Grid → Pixel:
    • When cellSize > 1: grid * cellSize + cellSize * 0.5 (returns cell center)
    • When cellSize = 1: grid (returns grid coordinate directly)

alignToCenter option:

You can explicitly control whether to return cell center using alignToCenter:

// Default behavior: align to center when cellSize > 1, no alignment when cellSize = 1
const planner1 = createAStarPlanner(gridMap, undefined, { cellSize: 20 }); // alignToCenter = true
// Explicitly disable center alignment (returns cell top-left corner)
const planner2 = createAStarPlanner(gridMap, undefined, {
cellSize: 20,
alignToCenter: false
});
// Explicitly enable center alignment (returns 0.5, 1.5, 2.5... even when cellSize = 1)
const planner3 = createAStarPlanner(gridMap, undefined, {
cellSize: 1,
alignToCenter: true
});

Time-Sliced Pathfinding (Large-Scale Agents)

Section titled “Time-Sliced Pathfinding (Large-Scale Agents)”

For large-scale agent scenarios (100+), you can use incremental pathfinding to spread computation across multiple frames:

import {
NavigationSystem,
createIncrementalAStarPlanner
} from '@esengine/pathfinding/ecs';
import { createGridMap } from '@esengine/pathfinding';
const gridMap = createGridMap(200, 200);
// Create incremental pathfinder
const planner = createIncrementalAStarPlanner(gridMap, undefined, {
cellSize: 20
});
// Enable time slicing
const navSystem = new NavigationSystem({
enableTimeSlicing: true, // Enable time slicing
iterationsBudget: 1000, // Total iterations per frame
maxAgentsPerFrame: 10, // Max agents to process per frame
maxIterationsPerAgent: 200 // Max iterations per agent per frame
});
navSystem.setPathPlanner(planner);

How it works:

  • System auto-detects IIncrementalPathPlanner and enables incremental mode
  • Iteration budget is allocated to agents by priority each frame
  • Pathfinding computation is spread across frames to avoid stuttering
  • Agents can set priority via priority property (lower number = higher priority)

Agent Priority:

const nav = entity.addComponent(new NavigationAgentComponent());
nav.priority = 10; // High priority, gets iteration budget first
// Check pathfinding status
if (nav.isComputingPath) {
console.log(`Pathfinding progress: ${(nav.pathProgress * 100).toFixed(0)}%`);
}
import { Scene } from '@esengine/ecs-framework';
import {
NavigationSystem,
NavigationAgentComponent,
createAStarPlanner,
createORCAAvoidance,
createDefaultCollisionResolver
} from '@esengine/pathfinding/ecs';
import { createGridMap } from '@esengine/pathfinding';
// Create scene
const scene = new Scene();
// Create grid map
const gridMap = createGridMap(30, 20);
gridMap.setRectWalkable(10, 5, 5, 10, false); // Add obstacle area
// Create navigation system
const navSystem = new NavigationSystem({
enablePathPlanning: true,
enableLocalAvoidance: true,
enableCollisionResolution: true
});
// Configure modules
navSystem.setPathPlanner(createAStarPlanner(gridMap, undefined, { cellSize: 20 }));
navSystem.setLocalAvoidance(createORCAAvoidance());
navSystem.setCollisionResolver(createDefaultCollisionResolver());
// Add static obstacle (wall)
navSystem.addStaticObstacle({
vertices: [
{ x: 200, y: 100 },
{ x: 300, y: 100 },
{ x: 300, y: 300 },
{ x: 200, y: 300 }
]
});
scene.addSystem(navSystem);
// Create agents
for (let i = 0; i < 10; i++) {
const entity = scene.createEntity(`Agent-${i}`);
const nav = entity.addComponent(new NavigationAgentComponent());
// Set initial position
nav.setPosition(50 + Math.random() * 100, 150 + Math.random() * 100);
nav.radius = 0.5;
nav.maxSpeed = 5.0;
nav.waypointThreshold = 0.5;
// Set destination (other side)
nav.setDestination(500, 200);
}
OptionTypeDefaultDescription
enablePathPlanningbooleantrueEnable path planning
enableLocalAvoidancebooleantrueEnable local avoidance
enableFlowControlbooleantrueEnable flow control
enableCollisionResolutionbooleantrueEnable collision resolution
enableTimeSlicingbooleanfalseEnable time-sliced pathfinding
iterationsBudgetnumber1000Total iterations per frame
maxAgentsPerFramenumber10Max agents to process per frame
maxIterationsPerAgentnumber200Max iterations per agent per frame
MethodDescription
setPathPlanner(planner)Set path planner
getPathPlanner()Get current path planner
setLocalAvoidance(avoidance)Set local avoidance module
getLocalAvoidance()Get current local avoidance module
setFlowController(controller)Set flow controller
getFlowController()Get current flow controller
setCollisionResolver(resolver)Set collision resolver
getCollisionResolver()Get current collision resolver
addStaticObstacle(obstacle)Add static obstacle
addDynamicObstacle(obstacle)Add dynamic obstacle
clearStaticObstacles()Clear all static obstacles
clearDynamicObstacles()Clear all dynamic obstacles
clearObstacles()Clear all obstacles (static and dynamic)
getStaticObstacles()Get static obstacle list
getDynamicObstacles()Get dynamic obstacle list
PropertyTypeDefaultDescription
positionIVector2{x:0,y:0}Current position
velocityIVector2{x:0,y:0}Current velocity
radiusnumber0.5Collision radius
maxSpeednumber5.0Maximum speed
accelerationnumber10.0Acceleration
waypointThresholdnumber0.5Waypoint arrival threshold
arrivalThresholdnumber0.3Destination arrival threshold
repathIntervalnumber0.5Path recalculation interval (seconds)
enabledbooleantrueWhether navigation is enabled
autoRepathbooleantrueWhether to auto repath when blocked
smoothSteeringbooleantrueWhether to enable smooth steering
prioritynumber50Priority (lower = higher priority)
isComputingPathbooleanfalseWhether computing path
pathProgressnumber0Pathfinding progress (0-1)
MethodDescription
setPosition(x, y)Set current position
setDestination(x, y)Set target position
stop()Stop navigation
hasArrived()Check if arrived at destination
isBlocked()Check if path is blocked
isUnreachable()Check if destination is unreachable
getCurrentWaypoint()Get current waypoint
getDistanceToDestination()Get distance to destination
getCurrentSpeed()Get current speed magnitude

Use ORCAConfigComponent to customize ORCA parameters per agent:

PropertyTypeDefaultDescription
neighborDistnumber15.0Neighbor detection distance
maxNeighborsnumber10Maximum neighbors
timeHorizonnumber2.0Agent time horizon
timeHorizonObstnumber1.0Obstacle time horizon
// Draw path in render loop
if (nav.path.length > 0) {
ctx.beginPath();
ctx.moveTo(nav.path[0].x, nav.path[0].y);
for (let i = 1; i < nav.path.length; i++) {
ctx.lineTo(nav.path[i].x, nav.path[i].y);
}
ctx.strokeStyle = 'blue';
ctx.stroke();
}

Issue: Agent circles around at corners

  • Check if waypointThreshold is too large
  • Recommended: Math.max(radius * 2, 15)

Issue: Agent cannot reach destination

  • Check if destination is in walkable area
  • Check if static obstacles are set correctly
  • Use planner.isWalkable(x, y) to verify

Issue: Agent penetrates obstacles

  • Ensure enableCollisionResolution: true
  • Check obstacle vertex order (should be CCW)
  • Use Polygon.ensureCCW() for auto-correction

Check out the Navigation System Interactive Demo to experience the full functionality.