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);
| Method | Description |
|---|---|
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.