Entities & components
How entities are identified, how components are defined and registered, and how to build and query entities using the high-level Entity builder and the low-level EntityComponentDataset.
An entity is an integer ID plus a generation counter — nothing more. The EntityComponentDataset recycles IDs when entities are removed; the generation counter makes recycled IDs distinguishable from stale references without any extra tracking.
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 {
constructor() {
this.x = 0;
this.y = 0;
this.z = 0;
}
}
Velocity.typeName = "Velocity";
Components have no built-in lifecycle hooks and no methods that reference the entity they belong to. Logic lives in systems.
One component per type per entity
A dataset enforces the rule: each entity can hold at most one instance of any given component type. Attempting to add 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.
Building an entity — the high-level API
Entity is a chainable builder that accumulates components and then inserts everything into a dataset in one call:
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { SerializationMetadata } from "@woosh/meep-engine/src/engine/ecs/components/SerializationMetadata.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
const transform = new Transform();
transform.position.set(10, 0, 20);
const entityId = new Entity()
.add(transform)
.add(SerializationMetadata.Transient)
.build(ecd); // returns the numeric entity ID
add() returns this for chaining. If the entity is already built when you call add(), the component is forwarded directly to the dataset. entity.destroy() removes the entity from the dataset and clears the builder so it can be rebuilt later.
Entity also exposes:
| Method | Description |
|---|---|
hasComponent(klass) | Returns true if a component of that type is present |
getComponent(klass) | Returns the instance, or null if absent |
getComponentSafe(klass) | Same, but throws if absent |
removeComponent(klass) | Removes and returns the instance (also removes from dataset if built) |
isBuilt | true after build(), false after destroy() |
Reading an existing entity into a builder
If you have a numeric entity ID and a dataset, Entity.readFromDataset(id, ecd) reconstructs an Entity builder from the live components, so you can inspect or modify them using the same API.
The low-level API — EntityComponentDataset
EntityComponentDataset is the actual storage. The Entity builder calls into it; you can also use it directly when you need fine-grained control.
import { EntityComponentDataset } from "@woosh/meep-engine/src/engine/ecs/EntityComponentDataset.js";
const ecd = new EntityComponentDataset();
// Register the component types the dataset will hold
ecd.registerComponentType(Transform);
ecd.registerComponentType(Velocity);
// Create an entity
const id = ecd.createEntity();
// Add components to it
ecd.addComponentToEntity(id, new Transform());
ecd.addComponentToEntity(id, new Velocity());
// Read a component back
const t = ecd.getComponent(id, Transform); // returns the instance or undefined
// Remove a component
ecd.removeComponentFromEntity(id, Velocity);
// Destroy the entity entirely
ecd.removeEntity(id);
EntityManager calls registerManyComponentTypes automatically from the component lists declared on each system’s dependencies and components_used, so you rarely need to register types by hand.
Key dataset methods:
| Method | Description |
|---|---|
createEntity() | Allocates a new ID (reuses freed slots) |
entityExists(id) | Tests whether an ID is live |
getEntityGeneration(id) | Returns the generation counter for a live entity |
addComponentToEntity(id, instance) | Attaches a component; type must be registered |
getComponent(id, klass) | Returns the instance or undefined |
getComponentSafe(id, klass) | Same, throws on missing |
hasComponent(id, klass) | Boolean presence check |
removeComponentFromEntity(id, klass) | Detaches a component |
removeEntity(id) | Removes all components, fires lifecycle events, frees the ID |
getEntityCount() | Current live entity count |
Component observers and lifecycle signals
An EntityObserver fires a callback when an entity first acquires a complete set of specified components, and again when that set breaks. Systems use this internally; you can also use it directly:
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 events, the dataset also dispatches string-named events on individual entities. The built-in event names are in EventType:
import { EventType } from "@woosh/meep-engine/src/engine/ecs/EventType.js";
// Listen for a component being added to a specific entity
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
- ECS overview — the conceptual model.
- Systems & scheduling — how systems read and write components each frame.
- Queries —
traverseEntities,traverseComponents, and cached iteration.