Navigation agents
Move an entity along a path using the Path, PathFollower, and PathFollowingSystem components.
Once you have a path from a navigation mesh, three ECS pieces advance an entity along it every frame: the Path component holds the waypoints, PathFollower controls movement parameters, and PathFollowingSystem drives the Transform each tick.
Path component
Path stores a sequence of 3-D waypoints and provides sampling methods used by PathFollowingSystem.
import Path from "@woosh/meep-engine/src/engine/navigation/ecs/components/Path.js";
const path = new Path();
// Populate from a navmesh query
const output = new Float32Array(1024 * 3);
const count = navmesh.find_path(output, sx, sy, sz, gx, gy, gz);
path.setPointCount(count);
for (let i = 0; i < count; i++) {
path.setPosition(i, output[i * 3], output[i * 3 + 1], output[i * 3 + 2]);
}
Key methods
| Method | Description |
|---|---|
setPointCount(n) | Resize the waypoint list. Truncates or extends. |
setPosition(index, x, y, z) | Write a single waypoint. |
getPosition(index, result: Vector3) | Read a waypoint into a Vector3. |
getPointCount() | Current number of waypoints. Also available as .count. |
isEmpty() | true when there are no waypoints. |
clear() | Remove all waypoints. |
sample(result: Vector3, offset, interpolation?) | Sample a world-space position at offset metres along the path. |
get length | Total arc length, computed lazily and cached until waypoints change. |
reverse() | Flip the waypoint order in-place. |
Interpolation
path.interpolation controls how positions between waypoints are computed. Values come from InterpolationType:
import { InterpolationType } from "@woosh/meep-engine/src/engine/navigation/ecs/components/InterpolationType.js";
path.interpolation = InterpolationType.Linear; // straight segments (default)
path.interpolation = InterpolationType.CatmullRom; // smooth non-uniform spline
| Value | Behaviour |
|---|---|
Linear (1) | Straight-line interpolation between consecutive waypoints. |
CatmullRom (3) | Non-uniform Catmull-Rom spline — smooth curves through the waypoints. |
PathFollower component
PathFollower sits next to Path on the same entity and describes how the entity moves:
import PathFollower from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollower.js";
import { PathFollowerFlags } from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollowerFlags.js";
const follower = new PathFollower();
follower.speed.set(3); // world units per second (Vector1, default 1)
follower.rotationSpeed.set(4); // rad/s; Infinity = instant (default Infinity)
follower.position = 0; // current offset along path in metres
Flags
PathFollower.flags is a bitmask. Use setFlag, clearFlag, and getFlag to manipulate it:
follower.setFlag(PathFollowerFlags.Loop); // wrap back to start when the end is reached
| Flag | Value | Meaning |
|---|---|---|
Active | 1 | System advances this follower. Cleared automatically on arrival when not looping. |
Locked | 2 | Reserved — no system behaviour currently. |
Loop | 4 | Wrap the path position back to 0 instead of stopping at the end. |
Finished | 8 | Set by the system when the follower reaches the end of a non-looping path. |
WritePositionX | 16 | System writes the X coordinate to Transform.position. |
WritePositionY | 32 | System writes the Y coordinate. |
WritePositionZ | 64 | System writes the Z coordinate. |
WriteRotationX | 128 | System aligns rotation around X toward the movement direction. |
WriteRotationY | 256 | System aligns rotation around Y (yaw toward movement). |
WriteRotationZ | 512 | System aligns rotation around Z. |
The default flags enable Active and all six write flags, so an entity moves and faces its direction of travel out of the box.
PathFollowingSystem
PathFollowingSystem iterates every entity that carries PathFollower + Transform + Path and calls performStep for each one. Register it alongside the rest of your ECS systems:
import PathFollowingSystem from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollowingSystem.js";
em.addSystem(new PathFollowingSystem());
Each tick it:
- Skips entities whose
PathFollowerdoes not have theActiveflag or whosePathis empty. - Advances
pathFollower.positionbyspeed × timeDeltametres (capped atmaxMoveDistanceper sub-step to prevent tunnelling at very high speeds or large time steps). - Calls
path.sample(...)with the new offset to get the next world position. - Writes the result back to
Transform.positionon the axes enabled by theWritePosition*flags. - When rotation alignment flags are set, rotates
Transform.rotationtoward the movement direction at up torotationSpeed × timeDeltaradians. - When a non-looping follower reaches the end, sets
Finished, clearsActive, and fires a"path-end-reached"event on the entity.
Arrival event
Listen for the arrival event with the entity manager’s event API:
import { PathFollowerEventType } from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollowerEventType.js";
em.dataset.addEntityEventListener(entity, PathFollowerEventType.EndReached, () => {
// entity reached the end of its path
});
// PathFollowerEventType.EndReached === "path-end-reached"
Putting it together
import Path from "@woosh/meep-engine/src/engine/navigation/ecs/components/Path.js";
import PathFollower from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollower.js";
import PathFollowingSystem from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollowingSystem.js";
import { PathFollowerFlags } from "@woosh/meep-engine/src/engine/navigation/ecs/path_following/PathFollowerFlags.js";
// 1. Register the system once
em.addSystem(new PathFollowingSystem());
// 2. Query a path from the navmesh
const output = new Float32Array(1024 * 3);
const count = navmesh.find_path(output, sx, sy, sz, gx, gy, gz);
// 3. Build the Path component
const path = new Path();
path.setPointCount(count);
for (let i = 0; i < count; i++) {
path.setPosition(i, output[i * 3], output[i * 3 + 1], output[i * 3 + 2]);
}
// 4. Configure the follower
const follower = new PathFollower();
follower.speed.set(3);
// 5. Attach to an entity
new Entity()
.add(new Transform())
.add(path)
.add(follower)
.build(em.dataset);
The system takes it from there — PathFollower.position advances each frame until "path-end-reached" fires.
Where to go next
- Navigation meshes — building the navmesh and querying raw paths.
- Rigid bodies — combining physics with navigation (kinematic bodies driven by path followers).