Physics

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

MethodDescription
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 lengthTotal 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
ValueBehaviour
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
FlagValueMeaning
Active1System advances this follower. Cleared automatically on arrival when not looping.
Locked2Reserved — no system behaviour currently.
Loop4Wrap the path position back to 0 instead of stopping at the end.
Finished8Set by the system when the follower reaches the end of a non-looping path.
WritePositionX16System writes the X coordinate to Transform.position.
WritePositionY32System writes the Y coordinate.
WritePositionZ64System writes the Z coordinate.
WriteRotationX128System aligns rotation around X toward the movement direction.
WriteRotationY256System aligns rotation around Y (yaw toward movement).
WriteRotationZ512System 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:

  1. Skips entities whose PathFollower does not have the Active flag or whose Path is empty.
  2. Advances pathFollower.position by speed × timeDelta metres (capped at maxMoveDistance per sub-step to prevent tunnelling at very high speeds or large time steps).
  3. Calls path.sample(...) with the new offset to get the next world position.
  4. Writes the result back to Transform.position on the axes enabled by the WritePosition* flags.
  5. When rotation alignment flags are set, rotates Transform.rotation toward the movement direction at up to rotationSpeed × timeDelta radians.
  6. When a non-looping follower reaches the end, sets Finished, clears Active, 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).