Query

Queries retrieve entities based on their component composition.

Overview

Queries are the primary way to find entities in bitECS. You specify which components an entity must have (or not have), and get back a list of matching entity IDs.

Pattern Matching

Define which components entities must/must not have.

Cached Results

Queries are cached and updated incrementally for speed.

Composable

Combine And, Or, Not operators for complex patterns.

Basic Queries

The simplest query finds all entities with a set of components:

basic.ts
import { query } from 'bitecs'

// Find all entities with Position
const positioned = query(world, [Position])

// Find all entities with Position AND Velocity
const moving = query(world, [Position, Velocity])

// Iterate over results
for (const entity of moving) {
  Position.x[entity] += Velocity.x[entity]
  Position.y[entity] += Velocity.y[entity]
}
// Queries return arrays of entity IDs
const e1 = addEntity(world)  // 1
const e2 = addEntity(world)  // 2
const e3 = addEntity(world)  // 3

addComponent(world, e1, Position)
addComponent(world, e2, Position)
addComponent(world, e2, Velocity)
addComponent(world, e3, Velocity)

query(world, [Position])            // [1, 2]
query(world, [Velocity])            // [2, 3]
query(world, [Position, Velocity])  // [2]

Query Operators

Operators let you create more complex matching patterns:

OperatorAliasDescription
And(A, B)All()Entity must have ALL components (default)
Or(A, B)Any()Entity must have ANY of the components
Not(A)None()Entity must NOT have the component

And / All

Matches entities that have all specified components. This is the default behavior when you list components in an array:

and.ts
import { query, And, All } from 'bitecs'

// These are equivalent:
query(world, [Position, Velocity])           // Implicit And
query(world, [And(Position, Velocity)])      // Explicit And
query(world, [All(Position, Velocity)])      // Alias

Or / Any

Matches entities that have at least one of the specified components:

or.ts
import { query, Or, Any } from 'bitecs'

// Match entities with Position OR Velocity (or both)
query(world, [Or(Position, Velocity)])
query(world, [Any(Position, Velocity)])

// Practical use: entities that can take damage
query(world, [Or(Health, Shield, Armor)])

Not / None

Excludes entities that have the specified components:

not.ts
import { query, Not, None } from 'bitecs'

// Entities with Position but NOT Velocity (stationary)
query(world, [Position, Not(Velocity)])

// Exclude multiple components
query(world, [Enemy, None(Dead, Stunned, Frozen)])

Complex Queries

Combine operators for sophisticated matching:

complex.ts
// Entities with Position, either Health OR Shield, but not Stunned
query(world, [
  Position,
  Or(Health, Shield),
  Not(Stunned)
])

// Living enemies that can move
query(world, [
  Enemy,
  Position,
  Velocity,
  Not(Dead),
  Not(Frozen)
])

// AI-controlled entities (not players) that need pathfinding
query(world, [
  AI,
  Position,
  PathfindingTarget,
  Not(Player),
  Not(Disabled)
])

Query Options

Queries accept options or modifiers for different behaviors:

Option/ModifierDescription
buffered: true / asBufferReturn Uint32Array instead of readonly number[]
commit: false / isNestedSkip committing pending entity removals
options.ts
import { query, asBuffer, isNested } from 'bitecs'

// Options object style
query(world, [Position], { commit: false })      // Skip removal commit
query(world, [Position], { buffered: true })     // Return Uint32Array

// Modifier style (more concise)
query(world, [Position], isNested)               // Safe for nested iteration
query(world, [Position], asBuffer)               // Return Uint32Array
query(world, [Position], asBuffer, isNested)     // Combined

// asBuffer is useful for passing to web workers (transferable)
const buffer = query(world, [Position], asBuffer)
worker.postMessage({ entities: buffer }, [buffer.buffer])

Nested Queries

By default, entity removals are deferred until a query is called. When nesting queries, use isNested to prevent removals during outer iteration:

nested.ts
// WRONG: Inner query may commit removals during outer iteration
for (const entity of query(world, [Position])) {
  for (const other of query(world, [Target])) { // ❌ May break outer loop!
    // ...
  }
}

// CORRECT: Use isNested for inner queries
for (const entity of query(world, [Position])) {
  for (const other of query(world, [Target], isNested)) {  // ✓ Safe
    // No removals committed during iteration
  }
}

// Or use options object
for (const entity of query(world, [Position])) {
  for (const other of query(world, [Target], { commit: false })) {
    // Also safe
  }
}

// Commit removals manually when done
import { commitRemovals } from 'bitecs'
commitRemovals(world)
!
Always use isNested for queries inside loops to avoid iterator invalidation bugs where entities disappear mid-iteration.

Relation Queries

Query entities by their relationships using relation components:

relation-queries.ts
import { query, Wildcard } from 'bitecs'

const ChildOf = createRelation()

// Find all children of a specific parent
query(world, [ChildOf(parentEntity)])

// Find all entities that are a child of ANYTHING
query(world, [ChildOf(Wildcard)])
query(world, [ChildOf('*')])  // Shorthand

// Find all entities at hierarchy depth 2
import { Hierarchy } from 'bitecs'
query(world, [Transform, Hierarchy(ChildOf, 2)])

// Process entities in hierarchy order (parents before children)
for (const eid of query(world, [Transform, Hierarchy(ChildOf)])) {
  // Parent's worldTransform is guaranteed to be calculated first
  updateWorldTransform(eid)
}

Performance Tips

Cache Query Results

If running the same query every frame, results are cached internally. Just call query() directly - no need to store references.
performance.ts
// Queries are cached - this is efficient
const movementSystem = (world) => {
  // bitECS caches this query result and updates incrementally
  for (const eid of query(world, [Position, Velocity])) {
    Position.x[eid] += Velocity.x[eid]
    Position.y[eid] += Velocity.y[eid]
  }
}

// For many entities, consider TypedArray iteration
const entities = query(world, [Position, Velocity], asBuffer)
for (let i = 0; i < entities.length; i++) {
  const eid = entities[i]
  Position.x[eid] += Velocity.x[eid]
  // Array index iteration can be faster than for-of for large counts
}

Next Steps

Continue Learning