Pathfinding
Find optimal paths through NavMesh with A* pathfinding, path smoothing algorithms, and path following behaviors.
Overview#
Web Engine's pathfinding system provides:
- A* pathfinding on NavMesh
- Funnel algorithm for path smoothing
- Raycast-based path simplification
- Path validation and length calculation
- Path following with waypoint navigation
- Integration with steering behaviors
Finding Paths#
Basic Pathfinding#
basic-pathfinding.ts
typescriptimport { navMeshBridge } from '@web-engine-dev/core/engine/ai';import * as THREE from 'three'; // Find path between two pointsconst start = new THREE.Vector3(0, 0, 0);const end = new THREE.Vector3(10, 0, 10); const result = await navMeshBridge.findPath(start, end); if (result.success && result.path) { // Convert to Vector3 array const waypoints = result.path.map(p => new THREE.Vector3(p.x, p.y, p.z) ); console.log(`Path found with ${waypoints.length} waypoints`);} else { console.error('No path found:', result.error);}Snap to NavMesh#
For accurate pathfinding, snap start/end positions to the NavMesh:
snap-to-navmesh.ts
typescript// Snap positions to NavMesh before pathfindingconst startOnMesh = navMesh.getClosestPoint(start);const endOnMesh = navMesh.getClosestPoint(end); if (!startOnMesh || !endOnMesh) { console.error('Start or end position not on NavMesh'); return null;} const result = await navMeshBridge.findPath(startOnMesh, endOnMesh);Path Smoothing#
Funnel Algorithm#
The funnel algorithm creates the shortest path that stays within NavMesh bounds:
funnel-smoothing.ts
typescriptimport { smoothPathFunnel } from '@web-engine-dev/core/engine/ai'; // Define portal edges between path segmentsconst portals = [ { left: new THREE.Vector3(-1, 0, 5), right: new THREE.Vector3(1, 0, 5) }, { left: new THREE.Vector3(-1, 0, 10), right: new THREE.Vector3(1, 0, 10) }, { left: new THREE.Vector3(-1, 0, 15), right: new THREE.Vector3(1, 0, 15) },]; // Original path from A*const rawPath = [ new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 5), new THREE.Vector3(0, 0, 10), new THREE.Vector3(0, 0, 15), new THREE.Vector3(0, 0, 20),]; // Apply funnel smoothingconst smoothed = smoothPathFunnel(rawPath, portals); console.log(`Smoothed from ${rawPath.length} to ${smoothed.length} waypoints`);Raycast Smoothing#
Simplify paths by removing waypoints with direct line-of-sight:
raycast-smoothing.ts
typescriptimport { smoothPathRaycast } from '@web-engine-dev/core/engine/ai'; // Define raycast function (checks for obstacles)const raycast = (start: THREE.Vector3, end: THREE.Vector3) => { const direction = new THREE.Vector3().subVectors(end, start); const distance = direction.length(); direction.normalize(); const raycaster = new THREE.Raycaster(start, direction, 0, distance); const intersects = raycaster.intersectObjects(obstacles, true); return { hit: intersects.length > 0, distance: intersects[0]?.distance ?? distance, };}; // Smooth path with raycast validationconst smoothed = smoothPathRaycast( rawPath, raycast, maxIterations: 3 // Number of smoothing passes);Path Utilities#
Simplify Path#
Remove waypoints that are too close together:
simplify-path.ts
typescriptimport { simplifyPath } from '@web-engine-dev/core/engine/ai'; // Remove points closer than 0.5 unitsconst simplified = simplifyPath(path, minDistance: 0.5); // Result: fewer waypoints, smoother movementValidate Path#
validate-path.ts
typescriptimport { validatePath } from '@web-engine-dev/core/engine/ai'; // Check for NaN or infinite valuesif (!validatePath(path)) { console.error('Invalid path detected'); return;} // Safe to use pathCalculate Path Length#
path-length.ts
typescriptimport { calculatePathLength } from '@web-engine-dev/core/engine/ai'; const totalLength = calculatePathLength(path);console.log(`Path is ${totalLength.toFixed(2)} units long`); // Use for travel time estimationconst speed = 5.0; // units per secondconst estimatedTime = totalLength / speed;Get Position Along Path#
position-along-path.ts
typescriptimport { getPositionAlongPath } from '@web-engine-dev/core/engine/ai'; // Get position at distance 10 units along pathconst position = getPositionAlongPath(path, distance: 10.0); if (position) { console.log('Position:', position);} // Use for look-ahead in path followingconst lookAheadDistance = 5.0;const lookAheadPos = getPositionAlongPath(path, currentDistance + lookAheadDistance);Path Following#
Basic Path Following#
basic-following.ts
typescriptimport { SteeringBehaviors } from '@web-engine-dev/core/engine/ai';import { Transform, Velocity } from '@web-engine-dev/core/engine/ecs/components'; // Path following statelet currentWaypointIndex = 0;const waypointRadius = 1.0; function followPath(eid: number, path: THREE.Vector3[], delta: number) { if (currentWaypointIndex >= path.length) { return; // Path complete } const agent = { position: Transform.position[eid], velocity: Velocity.linear[eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0, }; const target = { position: path[currentWaypointIndex], }; // Use arrive behavior for smoother movement const result = SteeringBehaviors.arrive(agent, target, slowingRadius: 2.0); if (result.active) { // Apply steering force Velocity.linear[eid][0] += result.force.x * delta; Velocity.linear[eid][1] += result.force.y * delta; Velocity.linear[eid][2] += result.force.z * delta; } // Check if reached waypoint const agentPos = new THREE.Vector3().fromArray(agent.position); const waypointPos = new THREE.Vector3().fromArray(target.position); const distance = agentPos.distanceTo(waypointPos); if (distance < waypointRadius) { currentWaypointIndex++; }}Advanced Path Following with Steering#
advanced-following.ts
typescriptimport { SteeringBehaviors, SteeringPath } from '@web-engine-dev/core/engine/ai'; // Create steering pathconst steeringPath: SteeringPath = { points: waypoints, looped: false, radius: 1.0,}; let currentIndex = 0; function updatePathFollowing(eid: number, delta: number) { const agent = { position: Transform.position[eid], velocity: Velocity.linear[eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0, }; // Use pathFollowing behavior const { result, nextIndex } = SteeringBehaviors.pathFollowing( agent, steeringPath, currentIndex ); currentIndex = nextIndex; if (result.active) { // Apply force Velocity.linear[eid][0] += result.force.x * delta; Velocity.linear[eid][1] += result.force.y * delta; Velocity.linear[eid][2] += result.force.z * delta; } // Check if path complete if (currentIndex >= steeringPath.points.length - 1 && !steeringPath.looped) { onPathComplete(eid); }}Integration with Behavior Trees#
Path Following Action#
bt-path-following.ts
typescriptimport { BTAction, BTStatus, BTContext } from '@web-engine-dev/core/engine/ai'; const followPathAction = new BTAction('followPath', (ctx: BTContext) => { const path = ctx.blackboard.get<THREE.Vector3[]>('path'); const waypointIndex = ctx.blackboard.get<number>('waypointIndex') ?? 0; if (!path || path.length === 0) { return BTStatus.FAILURE; } if (waypointIndex >= path.length) { ctx.blackboard.delete('path'); ctx.blackboard.delete('waypointIndex'); return BTStatus.SUCCESS; // Path complete } // Get agent and target const agent = { position: Transform.position[ctx.eid], velocity: Velocity.linear[ctx.eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0, }; const target = { position: path[waypointIndex] }; // Apply steering const result = SteeringBehaviors.arrive(agent, target, slowingRadius: 2.0); if (result.active) { Velocity.linear[ctx.eid][0] += result.force.x * ctx.delta; Velocity.linear[ctx.eid][1] += result.force.y * ctx.delta; Velocity.linear[ctx.eid][2] += result.force.z * ctx.delta; } // Check waypoint reached const agentPos = new THREE.Vector3().fromArray(agent.position); const waypointPos = new THREE.Vector3().fromArray(target.position); if (agentPos.distanceTo(waypointPos) < 1.0) { ctx.blackboard.set('waypointIndex', waypointIndex + 1); } return BTStatus.RUNNING;});Pathfinding Action#
bt-pathfinding.ts
typescriptimport { BTAction, BTStatus } from '@web-engine-dev/core/engine/ai'; const findPathAction = new BTAction('findPath', async (ctx: BTContext) => { const targetPos = ctx.blackboard.get<THREE.Vector3>('targetPosition'); if (!targetPos) { return BTStatus.FAILURE; } const agentPos = new THREE.Vector3().fromArray(Transform.position[ctx.eid]); // Find path const result = await navMeshBridge.findPath(agentPos, targetPos); if (result.success && result.path) { const waypoints = result.path.map(p => new THREE.Vector3(p.x, p.y, p.z) ); // Store path in blackboard ctx.blackboard.set('path', waypoints); ctx.blackboard.set('waypointIndex', 0); return BTStatus.SUCCESS; } return BTStatus.FAILURE;}); // Use in behavior treeconst moveToTargetSequence = new BTSequence([ new BTCondition('hasTarget', (ctx) => ctx.blackboard.has('targetPosition')), findPathAction, followPathAction,]);Dynamic Pathfinding#
Replanning#
Recalculate paths when obstacles change or target moves:
replanning.ts
typescript// Replan threshold - distance target must move to trigger replanconst replanThreshold = 5.0; // Track last path calculationlet lastTargetPosition: THREE.Vector3 | null = null;let lastPathTime = 0;const minReplanInterval = 1.0; // seconds function shouldReplan( currentTarget: THREE.Vector3, time: number): boolean { if (!lastTargetPosition) return true; // Check time threshold if (time - lastPathTime < minReplanInterval) { return false; } // Check distance threshold const distance = currentTarget.distanceTo(lastTargetPosition); return distance > replanThreshold;} // In update loopif (shouldReplan(targetPosition, time)) { const result = await navMeshBridge.findPath(agentPos, targetPosition); if (result.success && result.path) { currentPath = result.path; lastTargetPosition = targetPosition.clone(); lastPathTime = time; }}Obstacle Avoidance During Path Following#
path-with-avoidance.ts
typescriptimport { SteeringCombiner, SteeringPriority } from '@web-engine-dev/core/engine/ai'; function followPathWithAvoidance( eid: number, path: THREE.Vector3[], obstacles: SteeringObstacle[]) { const agent = { position: Transform.position[eid], velocity: Velocity.linear[eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0, }; const combiner = new SteeringCombiner(); // High priority: avoid obstacles const avoidance = SteeringBehaviors.obstacleAvoidance( agent, obstacles, lookAhead: 5.0 ); combiner.add('avoidance', SteeringPriority.Critical, 1.0, avoidance.force, avoidance.active); // Normal priority: follow path const pathFollowing = SteeringBehaviors.pathFollowing( agent, { points: path, looped: false, radius: 1.0 }, currentWaypointIndex ); combiner.add('pathFollow', SteeringPriority.Normal, 1.0, pathFollowing.result.force, pathFollowing.result.active); // Calculate combined force (priority-based) const finalForce = combiner.calculatePrioritized(agent.maxForce); // Apply force Velocity.linear[eid][0] += finalForce.x * delta; Velocity.linear[eid][1] += finalForce.y * delta; Velocity.linear[eid][2] += finalForce.z * delta; combiner.clear();}Performance Tips#
Path Caching#
path-caching.ts
typescript// Cache paths for common routesconst pathCache = new Map<string, THREE.Vector3[]>(); function getCachedPath( start: THREE.Vector3, end: THREE.Vector3): THREE.Vector3[] | null { const key = `${start.toArray().join(',')}->${end.toArray().join(',')}`; return pathCache.get(key) ?? null;} function cachePath( start: THREE.Vector3, end: THREE.Vector3, path: THREE.Vector3[]): void { const key = `${start.toArray().join(',')}->${end.toArray().join(',')}`; pathCache.set(key, path);} // Clear cache when NavMesh changesfunction clearPathCache() { pathCache.clear();}Async Pathfinding#
Always use async pathfinding to avoid blocking the main thread:
async-pathfinding.ts
typescript// Queue pathfinding requestsconst pathRequests = new Map<number, Promise<PathResult>>(); async function requestPath( eid: number, start: THREE.Vector3, end: THREE.Vector3): Promise<void> { // Avoid duplicate requests if (pathRequests.has(eid)) { return; } const promise = navMeshBridge.findPath(start, end); pathRequests.set(eid, promise); const result = await promise; pathRequests.delete(eid); if (result.success && result.path) { // Store path for entity const waypoints = result.path.map(p => new THREE.Vector3(p.x, p.y, p.z) ); setBlackboardValue( AIAgent.blackboardId[eid], 'path', waypoints ); }}Example: Complete Pathfinding Setup#
complete-pathfinding.ts
typescriptimport { navMeshBridge, smoothPathRaycast, simplifyPath, validatePath, SteeringBehaviors,} from '@web-engine-dev/core/engine/ai'; async function setupPathfinding( eid: number, targetPosition: THREE.Vector3) { // 1. Get agent position const agentPos = new THREE.Vector3().fromArray(Transform.position[eid]); // 2. Find path const result = await navMeshBridge.findPath(agentPos, targetPosition); if (!result.success || !result.path) { console.error('Pathfinding failed'); return; } // 3. Convert to Vector3 array let waypoints = result.path.map(p => new THREE.Vector3(p.x, p.y, p.z) ); // 4. Smooth path waypoints = smoothPathRaycast(waypoints, raycastFunction, maxIterations: 2); // 5. Simplify path waypoints = simplifyPath(waypoints, minDistance: 0.5); // 6. Validate path if (!validatePath(waypoints)) { console.error('Invalid path generated'); return; } // 7. Store in blackboard const blackboardId = AIAgent.blackboardId[eid]; setBlackboardValue(blackboardId, 'path', waypoints); setBlackboardValue(blackboardId, 'waypointIndex', 0); console.log(`Path generated with ${waypoints.length} waypoints`);} // In game loopfunction updatePathFollowing(eid: number, delta: number) { const blackboardId = AIAgent.blackboardId[eid]; const path = getBlackboardValue<THREE.Vector3[]>(blackboardId, 'path'); const waypointIndex = getBlackboardValue<number>(blackboardId, 'waypointIndex') ?? 0; if (!path || waypointIndex >= path.length) { return; // No path or complete } const agent = { position: Transform.position[eid], velocity: Velocity.linear[eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0, }; const target = { position: path[waypointIndex] }; const result = SteeringBehaviors.arrive(agent, target, slowingRadius: 2.0); if (result.active) { Velocity.linear[eid][0] += result.force.x * delta; Velocity.linear[eid][1] += result.force.y * delta; Velocity.linear[eid][2] += result.force.z * delta; } // Check waypoint reached const agentPos = new THREE.Vector3().fromArray(agent.position); const waypointPos = new THREE.Vector3().fromArray(target.position); if (agentPos.distanceTo(waypointPos) < 1.0) { setBlackboardValue(blackboardId, 'waypointIndex', waypointIndex + 1); }}