Web Engine Docs
Preparing documentation
Documentation0%
Use the search bar to quickly find any topic
Content
Search
Examples
Ready
Web Engine v0.1.0-alpha
Edit this page on GitHub
Last updated: 1/15/2026
Was this page helpful?
Preparing documentation
Use the search bar to quickly find any topic
Find optimal paths through NavMesh with A* pathfinding, path smoothing algorithms, and path following behaviors.
Web Engine's pathfinding system provides:
import { 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 arrayconst 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);}
For accurate pathfinding, snap start/end positions to the NavMesh:
// 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);
The funnel algorithm creates the shortest path that stays within NavMesh bounds:
import { 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`);
Simplify paths by removing waypoints with direct line-of-sight:
import { 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);
Remove waypoints that are too close together:
import { 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 movement
import { 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 path
import { 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;
import { 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);
import { 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 movementconst result = SteeringBehaviors.arrive(agent, target, slowingRadius: 2.0);if (result.active) {// Apply steering forceVelocity.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 waypointconst agentPos = new THREE.Vector3().fromArray(agent.position);const waypointPos = new THREE.Vector3().fromArray(target.position);const distance = agentPos.distanceTo(waypointPos);if (distance < waypointRadius) {currentWaypointIndex++;}}
import { 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 behaviorconst { result, nextIndex } = SteeringBehaviors.pathFollowing(agent,steeringPath,currentIndex);currentIndex = nextIndex;if (result.active) {// Apply forceVelocity.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 completeif (currentIndex >= steeringPath.points.length - 1 && !steeringPath.looped) {onPathComplete(eid);}}
import { 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 targetconst 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 steeringconst 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 reachedconst 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;});
import { 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 pathconst 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 blackboardctx.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,]);
Recalculate paths when obstacles change or target moves:
// 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; // secondsfunction shouldReplan(currentTarget: THREE.Vector3,time: number): boolean {if (!lastTargetPosition) return true;// Check time thresholdif (time - lastPathTime < minReplanInterval) {return false;}// Check distance thresholdconst 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;}}
import { 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 obstaclesconst avoidance = SteeringBehaviors.obstacleAvoidance(agent,obstacles,lookAhead: 5.0);combiner.add('avoidance', SteeringPriority.Critical, 1.0, avoidance.force, avoidance.active);// Normal priority: follow pathconst 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 forceVelocity.linear[eid][0] += finalForce.x * delta;Velocity.linear[eid][1] += finalForce.y * delta;Velocity.linear[eid][2] += finalForce.z * delta;combiner.clear();}
// 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();}
Always use async pathfinding to avoid blocking the main thread:
// Queue pathfinding requestsconst pathRequests = new Map<number, Promise<PathResult>>();async function requestPath(eid: number,start: THREE.Vector3,end: THREE.Vector3): Promise<void> {// Avoid duplicate requestsif (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 entityconst waypoints = result.path.map(p =>new THREE.Vector3(p.x, p.y, p.z));setBlackboardValue(AIAgent.blackboardId[eid],'path',waypoints);}}
import {navMeshBridge,smoothPathRaycast,simplifyPath,validatePath,SteeringBehaviors,} from '@web-engine-dev/core/engine/ai';async function setupPathfinding(eid: number,targetPosition: THREE.Vector3) {// 1. Get agent positionconst agentPos = new THREE.Vector3().fromArray(Transform.position[eid]);// 2. Find pathconst result = await navMeshBridge.findPath(agentPos, targetPosition);if (!result.success || !result.path) {console.error('Pathfinding failed');return;}// 3. Convert to Vector3 arraylet waypoints = result.path.map(p =>new THREE.Vector3(p.x, p.y, p.z));// 4. Smooth pathwaypoints = smoothPathRaycast(waypoints, raycastFunction, maxIterations: 2);// 5. Simplify pathwaypoints = simplifyPath(waypoints, minDistance: 0.5);// 6. Validate pathif (!validatePath(waypoints)) {console.error('Invalid path generated');return;}// 7. Store in blackboardconst 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 reachedconst 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);}}