motion
Import from @pierre/ecs/modules/motion.
2D velocity integrator: each tick, pos += vel · dt. Optional boundary
handling (wrap / clamp) and an onMove hook so games that keep a
separate spatial index or dirty-flag queue can stay in sync without the
motion module owning either.
Not a full physics module — for gravity + collision + grounded, see
@pierre/ecs/modules/kinematics.
interface Bounds { width: number; height: number }
type VelocityIntegrationBoundary = | { mode: 'wrap'; bounds: Bounds } // toroidal topology, range [0, width) × [0, height) | { mode: 'clamp'; bounds: Bounds }; // pinned, range [0, width] × [0, height]
interface VelocityIntegrationTickCtx { dtMs: number; world: EcsWorld;}
interface VelocityIntegrationOptions<TCtx extends VelocityIntegrationTickCtx> { name?: string; runAfter?: string[]; boundary?: VelocityIntegrationBoundary; tag?: TagDef; // integrate only entities carrying this tag onMove?: ( ctx: TCtx, id: EntityId, prev: Readonly<Position>, next: Readonly<Position>, ) => void;}
function makeVelocityIntegrationSystem<TCtx extends VelocityIntegrationTickCtx>( options?: VelocityIntegrationOptions<TCtx>,): SchedulableSystem<TCtx>;Iterates every entity that has both VelocityDef and PositionDef.
Skips entities whose (vx, vy) is (0, 0) and entities whose final
position equals their prior position (after boundary handling) to avoid
waking downstream observers for no-op updates.
import { makeVelocityIntegrationSystem } from '@pierre/ecs/modules/motion';import { PositionDef, VelocityDef } from '@pierre/ecs/modules/transform';
const motion = makeVelocityIntegrationSystem<GameTickCtx>({ boundary: { mode: 'wrap', bounds: { width: 800, height: 600 } }, onMove(ctx, id, prev, next) { // Keep a HashGrid2D in sync with the integrated position. const p = cellOfPoint(prev.x, prev.y, CELL_SIZE); const n = cellOfPoint(next.x, next.y, CELL_SIZE); if (p.x !== n.x || p.y !== n.y) ctx.grid.move(id, p, n); },});
scheduler.add(motion);modules/spatial packages this projection as
makeGridSyncOnMove({ grid, cellSize }), which returns an onMove callback.
Vector helpers
Section titled “Vector helpers”Vector helpers do not live here. @pierre/ecs/modules/math owns the Vec2,
Vec3 and Quat blocks (vec2Normalize, vec2ScaleToLength,
vec2MoveToward, vec3*, quat*) — this module is the integrator only.
- Depends on
@pierre/ecs/modules/transform(PositionDef,VelocityDef). bounds.widthandbounds.heightmust be positive (> 0). Zero or negative bounds produceNaNpositions underwrapand are not validated at runtime — caller responsibility.- 2D only — angle (
RotationDef) is not integrated here. Angular-velocity integration is a one-liner rather than a standard component shape; a rotation-rate component lives in the game until a second consumer converges on one. - No acceleration term, gravity, or collision — those belong in
modules/kinematics. - Vector maths used to integrate (
vec2ScaleToLength,vec2MoveToward) comes from@pierre/ecs/modules/math. onMoveis the only extension point. Games that need per-entity enable/disable either remove theVelocityDefcomponent or set(vx, vy)to(0, 0).