ECS

Components

How components are defined as plain classes, registered with a dataset, attached to and read from entities, and observed as they are added and removed.

Components are the data half of the ECS: plain classes that hold state and nothing else. An entity is just an ID; everything you can know about it is the set of components attached to it. Logic that acts on that data lives in systems.

Defining a component

A component is a plain JavaScript class. The only requirement is a static typeName string, which the dataset uses for name-based lookup and serialisation:

export class Velocity {
    static typeName = "Velocity"

    x = 0
    y = 0
    z = 0
}

Components have no built-in lifecycle hooks and no methods that reference the entity they belong to. Keep behaviour out of them — it belongs in systems.

One component per type per entity

A dataset enforces one rule: each entity can hold at most one instance of any given component type. Adding a second instance of the same type to the same entity throws. This constraint is intentional — it makes component lookup O(1) and keeps the data layout flat.

Registering component types

A dataset only accepts components whose type it knows about. Register types before adding instances:

import { EntityComponentDataset } from "@woosh/meep-engine/src/engine/ecs/EntityComponentDataset.js";

const ecd = new EntityComponentDataset();

ecd.registerComponentType(Transform);
ecd.registerComponentType(Velocity);

In practice you rarely register by hand: EntityManager calls registerManyComponentTypes automatically from the component lists each system declares in its dependencies and components_used.

Attaching and reading components

Once the type is registered, attach, read, and remove components through the dataset:

const id = ecd.createEntity();

ecd.addComponentToEntity(id, new Transform());
ecd.addComponentToEntity(id, new Velocity());

const t = ecd.getComponent(id, Transform);   // the instance, or undefined

ecd.removeComponentFromEntity(id, Velocity);
MethodDescription
addComponentToEntity(id, instance)Attaches a component; its type must be registered
getComponent(id, klass)Returns the instance, or undefined
getComponentSafe(id, klass)Same, but throws on missing
hasComponent(id, klass)Boolean presence check
removeComponentFromEntity(id, klass)Detaches a component

The Entity builder wraps these same calls with a chainable API when you’re building an entity up rather than poking at a live one.

Observing components

An EntityObserver fires a callback when an entity first acquires a complete tuple of specified components, and again when that tuple breaks. Systems use this internally; you can use it directly too:

import { EntityObserver } from "@woosh/meep-engine/src/engine/ecs/EntityObserver.js";

const observer = new EntityObserver(
    [Transform, Velocity],                          // watch for this tuple
    (transform, velocity, entity) => { /* linked */ },
    (transform, velocity, entity) => { /* unlinked */ }
);

ecd.addObserver(observer, true);     // true = fire immediately for existing matches
// ...
ecd.removeObserver(observer, true);  // true = fire broken-callbacks on removal

For per-entity signals, the dataset also dispatches string-named events on individual entities. The built-in names are in EventType:

import { EventType } from "@woosh/meep-engine/src/engine/ecs/EventType.js";

ecd.addEntityEventListener(entityId, EventType.ComponentAdded, (event) => {
    console.log("added", event.klass.typeName, event.instance);
});

EventType.ComponentAdded, EventType.ComponentRemoved, and EventType.EntityRemoved are the three built-in events.

Where to go next

  • Entities — building and destroying the entities components attach to.
  • Queries — iterating entities by their component makeup.
  • Saving & loading — per-component serialization adapters and transient marking.