ECS

Queries

How to iterate over entities that match a component tuple using traverseEntities and traverseComponents, and what the performance characteristics of each are.

EntityComponentDataset provides three traversal methods for querying entities by their components. All three avoid heap allocation on the hot path by reusing scratch arrays internally.

traverseEntities — match a component tuple

traverseEntities visits every entity that carries all of the specified component types. The visitor receives one instance per listed type, followed by the entity ID as the final argument:

ecd.traverseEntities(
    [Transform, Velocity],
    (transform, velocity, entity) => {
        transform.position.x += velocity.x * dt;
        transform.position.y += velocity.y * dt;
        transform.position.z += velocity.z * dt;
    }
);

Return false from the visitor to stop iteration early. The optional third argument is a thisArg for the callback:

ecd.traverseEntities([Health], function(health, entity) {
    if (health.value <= 0) this.scheduleRemoval(entity);
    // return false to stop after the first match
}, this);

If any of the listed component types is not registered in the dataset, traverseEntities returns immediately with no visits — it cannot match any entity if a required type is absent.

The inner loop is a tight bit-test over a packed occupancy table. For N entities and K component types, the occupancy check is O(K) per entity; there are no per-frame allocations.

traverseComponents — iterate one component type

When you only need instances of a single component type, traverseComponents is more direct:

ecd.traverseComponents(Velocity, (velocity, entity) => {
    velocity.x *= 0.98;  // dampen all velocities
});

Return false to stop early. Because it iterates the backing store for one component index directly, it pays no bit-mask overhead — it just walks the sparse array.

traverseEntitiesExact — match a tuple with no extras

traverseEntitiesExact visits only entities that have exactly the listed component types and nothing else. It is slower than traverseEntities (it counts all components on each entity and rejects any that have more), so reserve it for cases where the extra-component check matters.

ecd.traverseEntitiesExact([Transform, Tag], (transform, tag, entity) => {
    // only entities with exactly Transform and Tag — not Transform + Tag + Collider
});

traverseEntityIndices — raw entity IDs

When you only need the entity IDs, not their components:

ecd.traverseEntityIndices((entity) => {
    console.log(entity);
});

Iterating all entities with for…of

EntityComponentDataset is Iterable<number>, so you can also use a for...of loop:

for (const entity of ecd) {
    const t = ecd.getComponent(entity, Transform);
    if (t !== undefined) { /* ... */ }
}

Singleton components — getAnyComponent

Some components are expected to have exactly one instance in the dataset (camera, audio listener, settings). getAnyComponent returns the first one it finds without allocating:

const { entity, component } = ecd.getAnyComponent(Camera);
if (component !== null) {
    // component is the Camera instance; entity is its owner ID
}

Observer-based queries

For reactive queries that must fire when the matching set changes, use EntityObserver (see Entities & components). EntityObserver is O(1) per change event — it fires the callback at the moment the tuple completes or breaks, not by scanning the whole dataset each frame.

Systems use observers internally to drive link and unlink. Prefer observers for setup/teardown logic and traverseEntities for per-frame iteration over the matched set.

Performance notes

  • traverseEntities and traverseComponents reuse a shared scratch array between calls. They are safe to call from within a traversal of a different component set, but not to nest on the same scratch buffer. In practice, calling traverseEntities inside a visitor passed to traverseEntities can corrupt arguments. Use traverseComponents or a manual index loop for nested iteration.
  • getComponent(id, klass) is a hash-map lookup (Map.get) and is O(1).
  • hasComponent(id, klass) delegates to getComponent and is also O(1).

Where to go next