Steering Behaviors
Reynolds-style steering behaviors for creating realistic autonomous agent movement with seek, flee, wander, obstacle avoidance, and flocking.
Overview#
Steering behaviors create natural-looking movement by calculating steering forces that guide agents toward their goals while avoiding obstacles and other agents.
Available Behaviors#
Basic Movement
Seek, Flee, Arrive, Pursue, and Evade behaviors for fundamental agent motion.
Complex Behaviors
Wander, Obstacle Avoidance, Hide, Path Following, and Containment for advanced navigation.
Flocking (Group AI)
Separation, Alignment, Cohesion, and combined Flocking for realistic group movement.
Utilities
SteeringCombiner for weighted blending and priority-based selection with zero-GC design.
Basic Behaviors#
Seek#
Steer toward a target at maximum speed:
import { SteeringBehaviors, SteeringAgent, SteeringTarget } from '@web-engine-dev/core/engine/ai'; const agent: SteeringAgent = { position: Transform.position[eid], velocity: Velocity.linear[eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0,}; const target: SteeringTarget = { position: targetPosition,}; const result = SteeringBehaviors.seek(agent, target); 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;}Flee#
Steer away from a threat:
const threat: SteeringTarget = { position: enemyPosition,}; // Flee with panic distance (only flee if within 10 units)const result = SteeringBehaviors.flee(agent, threat, panicDistance: 10.0); if (result.active) { // Apply force to move away applyForce(eid, result.force, delta);}Arrive#
Seek with deceleration near the target to avoid overshooting:
const target: SteeringTarget = { position: destinationPosition,}; // Arrive with slowing radius of 2 unitsconst result = SteeringBehaviors.arrive(agent, target, slowingRadius: 2.0); if (result.active) { applyForce(eid, result.force, delta);} // Agent will slow down smoothly as it approaches the targetPursue & Evade#
Intercept moving targets by predicting their future position:
// Pursue a moving targetconst target: SteeringTarget = { position: Transform.position[targetEid], velocity: Velocity.linear[targetEid], // Required for prediction}; const pursueResult = SteeringBehaviors.pursue( agent, target, lookAheadTime: 0 // 0 = auto-calculate based on speeds); // Evade a moving threatconst evadeResult = SteeringBehaviors.evade( agent, threat, lookAheadTime: 1.0, // Look ahead 1 second panicDistance: 15.0 // Only evade within 15 units);Complex Behaviors#
Wander#
Random exploration with smooth, natural-looking movement:
import { WanderConfig } from '@web-engine-dev/core/engine/ai'; const wanderConfig: WanderConfig = { circleDistance: 2.0, // Distance of wander circle from agent circleRadius: 1.0, // Radius of the wander circle angleChange: 0.3, // Maximum angle change per update}; // Wander requires entity ID for per-agent state trackingconst result = SteeringBehaviors.wander(agent, eid, wanderConfig); if (result.active) { applyForce(eid, result.force, delta);} // Clean up wander state when entity is destroyedSteeringBehaviors.clearWanderState(eid);Obstacle Avoidance#
Navigate around obstacles using feeler rays:
import { SteeringObstacle } from '@web-engine-dev/core/engine/ai'; // Define obstaclesconst obstacles: SteeringObstacle[] = [ { position: new THREE.Vector3(5, 0, 5), radius: 1.0 }, { position: new THREE.Vector3(-3, 0, 8), radius: 1.5 },]; // Avoid obstacles with 5 unit look-aheadconst result = SteeringBehaviors.obstacleAvoidance( agent, obstacles, lookAhead: 5.0); if (result.active) { // High priority - apply immediately applyForce(eid, result.force, delta);}Path Following#
Follow a sequence of waypoints:
import { SteeringPath } from '@web-engine-dev/core/engine/ai'; const path: SteeringPath = { points: [ new THREE.Vector3(0, 0, 0), new THREE.Vector3(5, 0, 5), new THREE.Vector3(10, 0, 5), new THREE.Vector3(10, 0, 10), ], looped: false, // Whether to loop back to start radius: 1.0, // Waypoint proximity threshold}; let currentIndex = 0; function updatePathFollowing() { const { result, nextIndex } = SteeringBehaviors.pathFollowing( agent, path, currentIndex ); currentIndex = nextIndex; if (result.active) { applyForce(eid, result.force, delta); } else { // Path complete onPathComplete(); }}Hide#
Find cover behind obstacles from a threat:
const threat: SteeringTarget = { position: enemyPosition,}; const obstacles: SteeringObstacle[] = [ { position: new THREE.Vector3(5, 0, 0), radius: 2.0 }, { position: new THREE.Vector3(-5, 0, 5), radius: 1.5 },]; // Find hiding spot 2 units behind obstaclesconst result = SteeringBehaviors.hide( agent, threat, obstacles, hideDistance: 2.0); if (result.active) { // Moves to hiding spot behind nearest obstacle applyForce(eid, result.force, delta);} // Falls back to flee if no hiding spots foundContainment#
Keep agents within defined boundaries:
const min = new THREE.Vector3(-50, 0, -50);const max = new THREE.Vector3(50, 10, 50); const result = SteeringBehaviors.containment( agent, min, max, margin: 5.0 // Start steering when within 5 units of edge); if (result.active) { // Steer back toward center applyForce(eid, result.force, delta);}Flocking Behaviors#
Separation#
Avoid crowding neighbors:
// Get nearby agentsconst neighbors: SteeringAgent[] = getNearbyAgents(eid, radius: 5.0); // Separate from neighbors within 2 unitsconst result = SteeringBehaviors.separation( agent, neighbors, radius: 2.0);Alignment#
Match velocity direction with neighbors:
// Align with neighbors within 5 unitsconst result = SteeringBehaviors.alignment( agent, neighbors, radius: 5.0);Cohesion#
Move toward the center of the group:
// Move toward group center (neighbors within 5 units)const result = SteeringBehaviors.cohesion( agent, neighbors, radius: 5.0);Combined Flocking#
import { FlockingConfig } from '@web-engine-dev/core/engine/ai'; const config: FlockingConfig = { separationWeight: 1.5, // Avoid crowding alignmentWeight: 1.0, // Match heading cohesionWeight: 1.0, // Move to center separationRadius: 2.0, // Personal space neighborRadius: 5.0, // Group awareness}; const result = SteeringBehaviors.flocking(agent, neighbors, config); if (result.active) { // Apply combined flocking force applyForce(eid, result.force, delta);}Combining Behaviors#
Weighted Blending#
Combine multiple behaviors with weights:
import { SteeringCombiner, SteeringPriority } from '@web-engine-dev/core/engine/ai'; const combiner = new SteeringCombiner(); // Add behaviors with weightsconst seek = SteeringBehaviors.seek(agent, target);combiner.add('seek', SteeringPriority.Normal, 1.0, seek.force, seek.active); const separation = SteeringBehaviors.separation(agent, neighbors, 2.0);combiner.add('separation', SteeringPriority.Normal, 0.5, separation.force, separation.active); const wander = SteeringBehaviors.wander(agent, eid);combiner.add('wander', SteeringPriority.Low, 0.3, wander.force, wander.active); // Calculate weighted blendconst finalForce = combiner.calculateWeightedBlend(agent.maxForce); // Apply forceapplyForce(eid, finalForce, delta); // Clear for next framecombiner.clear();Priority-Based Selection#
Higher priority behaviors override lower ones:
const combiner = new SteeringCombiner(); // Critical priority: obstacle avoidanceconst avoidance = SteeringBehaviors.obstacleAvoidance(agent, obstacles, 5.0);combiner.add('avoidance', SteeringPriority.Critical, 1.0, avoidance.force, avoidance.active); // High priority: flee from threatsconst flee = SteeringBehaviors.flee(agent, threat, 10.0);combiner.add('flee', SteeringPriority.High, 1.0, flee.force, flee.active); // Normal priority: seek targetconst seek = SteeringBehaviors.seek(agent, target);combiner.add('seek', SteeringPriority.Normal, 1.0, seek.force, seek.active); // Calculate prioritized force (higher priority takes precedence)const finalForce = combiner.calculatePrioritized(agent.maxForce); applyForce(eid, finalForce, delta);combiner.clear();Steering System#
Setup Steering Component#
import { Steering, initSteering, setSteeringBehaviors, SteeringFlags } from '@web-engine-dev/core/engine/ai'; // Add steering componentaddComponent(world, Steering, eid); // Initialize with default valuesinitSteering(eid); // Configure steering parametersSteering.maxSpeed[eid] = 5.0;Steering.maxForce[eid] = 10.0;Steering.arriveRadius[eid] = 1.0;Steering.slowingRadius[eid] = 2.0; // Enable behaviorssetSteeringBehaviors(eid, SteeringFlags.Seek | SteeringFlags.Separation);SteeringSystem Integration#
import { SteeringSystem } from '@web-engine-dev/core/engine/ai'; // In your game loopconst steeringSystem = SteeringSystem(world); function gameLoop(delta: number, time: number) { // SteeringSystem automatically applies steering forces steeringSystem(delta, time);} // Set path for path followingimport { setAgentPath } from '@web-engine-dev/core/engine/ai'; setAgentPath(eid, waypoints, looped: false); // Clear path when doneimport { clearAgentPath } from '@web-engine-dev/core/engine/ai';clearAgentPath(eid);Performance & Optimization#
Zero-GC Design#
Steering behaviors use pre-allocated vectors to avoid garbage collection:
- All calculations reuse shared temporary vectors
- Result objects are reused across calls
- No allocations in hot paths (60 FPS safe)
Neighbor Queries#
Use spatial indexing for efficient neighbor lookups:
import { getAISpatialIndex } from '@web-engine-dev/core/engine/ai'; const spatialIndex = getAISpatialIndex(); // Query nearby entities efficientlyconst neighbors = spatialIndex.queryRadius( position, radius: 10.0, maxResults: 32); // Convert to SteeringAgent arrayconst steeringAgents: SteeringAgent[] = neighbors.map(neighbor => ({ position: Transform.position[neighbor.eid], velocity: Velocity.linear[neighbor.eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0,}));Update Throttling#
// Throttle steering updates for distant agentsconst updateInterval = distanceToCamera < 20 ? 0.0 : 0.1;Steering.updateInterval[eid] = updateInterval; // SteeringSystem will automatically throttle updatesExample: Complete Steering Setup#
import { SteeringBehaviors, SteeringCombiner, SteeringPriority, SteeringAgent, getAISpatialIndex,} from '@web-engine-dev/core/engine/ai'; function updateSteering(eid: number, delta: number) { // Build agent const agent: SteeringAgent = { position: Transform.position[eid], velocity: Velocity.linear[eid], maxSpeed: 5.0, maxForce: 10.0, radius: 0.5, mass: 1.0, }; const combiner = new SteeringCombiner(); // Get obstacles const obstacles = getObstaclesNearAgent(eid); // Critical: obstacle avoidance const avoidance = SteeringBehaviors.obstacleAvoidance(agent, obstacles, 5.0); combiner.add('avoidance', SteeringPriority.Critical, 1.0, avoidance.force, avoidance.active); // High: containment const bounds = getBounds(); const containment = SteeringBehaviors.containment(agent, bounds.min, bounds.max, 5.0); combiner.add('containment', SteeringPriority.High, 1.0, containment.force, containment.active); // Normal: seek target or wander const targetEid = getBlackboardValue(AIAgent.blackboardId[eid], 'targetEid'); if (targetEid) { const target = { position: Transform.position[targetEid], velocity: Velocity.linear[targetEid], }; const pursue = SteeringBehaviors.pursue(agent, target); combiner.add('pursue', SteeringPriority.Normal, 1.0, pursue.force, pursue.active); } else { const wander = SteeringBehaviors.wander(agent, eid); combiner.add('wander', SteeringPriority.Normal, 1.0, wander.force, wander.active); } // Calculate final force const force = combiner.calculatePrioritized(agent.maxForce); // Apply force Velocity.linear[eid][0] += force.x * delta; Velocity.linear[eid][1] += force.y * delta; Velocity.linear[eid][2] += force.z * delta; // Clamp velocity to max speed const vx = Velocity.linear[eid][0]; const vy = Velocity.linear[eid][1]; const vz = Velocity.linear[eid][2]; const speed = Math.sqrt(vx * vx + vy * vy + vz * vz); if (speed > agent.maxSpeed) { const scale = agent.maxSpeed / speed; Velocity.linear[eid][0] *= scale; Velocity.linear[eid][1] *= scale; Velocity.linear[eid][2] *= scale; } combiner.clear();}