ECS v2
Entity-Component-System architecture. Entities are IDs, components are data, systems are functions. No inheritance, no scene graph — just fast, flat, data-oriented design.
Quick Example
import { createWorld, createEntity, stepWorld } from './engine/ecs/world/World.js';
import { registerSystem } from './engine/ecs/systems/SystemRegistry.js';
import { setComponent, getComponent } from './engine/ecs/storage/ArchetypeStorage.js';
// 1. Create a world
const world = createWorld({ name: 'MyGame' });
// 2. Create entities (just numeric IDs)
const player = createEntity(world);
const enemy = createEntity(world);
// 3. Attach components (plain data)
setComponent(world, player, 'Transform', {
position: [0, 1, 0],
rotation: [0, 0, 0, 1],
scale: [1, 1, 1]
});
// 4. Register systems (functions that run each tick)
registerSystem(world, {
name: 'GravitySystem',
phase: 'physics',
update(world, dt) {
// Query and update entities with PhysicsBody components
}
});
// 5. Step the simulation
stepWorld(world, 1/60);
Core Concepts
Worlds
A World is a container for entities, components, and systems. Most apps use a single world, but you can create multiple for isolation (e.g., a UI world separate from gameplay).
const world = createWorld({
name: 'GameWorld',
fixedDelta: 1/60,
phases: ['prePhysics', 'physics', 'postPhysics', 'render', 'lateUpdate']
});
The world tracks:
world.time— Current tick count, elapsed time, fixed deltaworld.systems— Registered system listworld.metrics— Per-system timing for profilingworld.config.phases— Ordered execution phases
Entities
An entity is a generational ID — a 32-bit integer encoding an index and a generation counter. This prevents dangling references: if entity slot 5 is destroyed and reused, the old ID (generation 1) won't match the new occupant (generation 2).
const id = createEntity(world); // → 1048577 (index=1, gen=1)
destroyEntity(world, id); // Frees the slot
isEntityAlive(world, id); // → false
const newId = createEntity(world); // Reuses slot 1, but gen=2
isEntityAlive(world, id); // → false (old gen doesn't match)
isEntityAlive(world, newId); // → true
ID encoding: Lower 20 bits = entity index (max ~1M entities). Upper bits = generation counter. Decoded via decodeEntityId(id) → { index, generation }.
Components
Components are plain data objects attached to entities by name. The engine defines a standard schema in EntitySchema.js with normalization and validation:
| Component | Key Fields | Purpose |
|---|---|---|
Transform | position, rotation, scale | Spatial placement |
PhysicsBody | velocity, mass, type | Rigid body dynamics |
Collider | shape, size, offset | Collision shapes |
Renderable | meshId, materialId, visible | Visual representation |
Light | type, color, intensity, range | Light sources |
Camera | fov, near, far, projection | View configuration |
ParticleEmitter | preset, rate, lifetime | Particle spawning |
NavAgent | speed, radius, destination | AI pathfinding |
NetReplicated | ownerId, priority | Network sync |
// Set a component
setComponent(world, entityId, 'Light', {
type: 'point',
color: [1, 0.9, 0.7],
intensity: 2.5,
range: 15
});
// Get a component
const transform = getComponent(world, entityId, 'Transform');
console.log(transform.position); // → [0, 1, 0]
// Remove a component
removeComponent(world, entityId, 'Light');
Systems
Systems are registered functions that execute each frame in a defined phase order. They process entities by querying for required components.
registerSystem(world, {
name: 'MovementSystem',
phase: 'physics', // Which phase to run in
order: 10, // Priority within phase (lower = earlier)
updateKind: 'tick', // 'tick' (fixed), 'frame' (variable), or 'both'
after: ['InputSystem'], // Dependency ordering
before: ['CollisionSystem'], // Must run before these
init(world, system) {
// Called once when registered
system.state = { moveSpeed: 5.0 };
},
update(world, dt, system) {
// Called every tick — do your work here
// Query entities, read/write components
},
teardown(world, system) {
// Called when system is unregistered
}
});
Execution Phases
Systems are grouped into phases that execute in order. The default phases are:
| Phase | Purpose | Typical Systems |
|---|---|---|
prePhysics | Input processing, AI decisions | InputSystem, AISystem |
physics | Physics simulation, movement | PhysicsSystem, MovementSystem |
postPhysics | Collision response, constraints | CollisionSystem, ConstraintSystem |
render | Prepare render data | CameraSystem, LightSystem |
lateUpdate | Cleanup, UI sync | AnimationSystem, UISync |
Two step functions exist:
stepWorld(world, dt)— Runs alltick-kind systems (fixed timestep)stepWorldFrame(world, dt)— Runs allframe-kind systems (variable timestep)
Archetype Storage
Components are stored in archetype-based storage (ArchetypeStorage.js). Entities with the same set of components are grouped together for cache-friendly iteration. The storage handles:
- Component add/remove — Moves entities between archetypes
- Query matching — Finds all entities with a given component set
- Sparse-set indexing — O(1) component access by entity ID
Component Healing
ComponentHealer.js validates and repairs component data using the schemas in EntitySchema.js. It normalizes vectors, clamps values, and fills missing fields with defaults. This makes save/load robust — corrupted or outdated save data is automatically healed.
Entity Lifecycle
// 1. Create
const id = createEntity(world);
// 2. Add components
setComponent(world, id, 'Transform', { position: [0,0,0] });
setComponent(world, id, 'Renderable', { meshId: 'cube' });
// 3. Systems process it each frame automatically
// 4. Delete with full cleanup (GPU buffers, selections, etc.)
deleteEntity({
ecsWorld: world,
entityId: id,
spawnedEntities,
uniformBuffers,
bindGroups
});
Prefabs
The PrefabRegistry.js defines reusable entity templates (spawnables) with pre-configured components and SDF collision shapes:
// Spawning a prefab
const entity = spawnPrefab(world, 'torch', {
position: [5, 0, 3],
scale: [0.5, 0.5, 0.5]
});
// Automatically gets: Transform, Renderable, Light, ParticleEmitter, Collider
// Plus SDF collision shape (sphere/box/cylinder) for particle interaction
World Snapshots
Capture and restore world state for save/load, rewind, or debugging:
// Capture
const snapshot = captureWorldSnapshot(world, { maxEntities: 1000 });
// Restore
restoreWorldFromSnapshot(world, snapshot);
Key Files
| File | Purpose |
|---|---|
ecs/world/World.js | createWorld, createEntity, stepWorld, stepWorldFrame |
ecs/systems/SystemRegistry.js | registerSystem, phase scheduling, dependency ordering |
ecs/storage/ArchetypeStorage.js | Component storage, queries, sparse-set indexing |
ecs/EntitySchema.js | Component schemas with types and normalization |
ecs/ComponentHealer.js | Auto-repair invalid component data |
ecs/EntityManager.js | High-level entity deletion with GPU cleanup |
ecs/prefabs/PrefabRegistry.js | Spawnable entity templates with SDF shapes |