Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
Master ECS queries: the mechanism for finding and filtering entities. Learn about defineQuery, enterQuery, exitQuery, query caching, and complex query patterns.
Queries are the primary way to find entities with specific components. Web Engine uses bitECS queries, which are automatically cached for O(1) performance after the first execution.
A query is defined once and reused throughout the system lifecycle. Calling a query returns an array of entity IDs matching the component set.
import { defineQuery } from 'bitecs';import { Transform, RigidBody } from '@web-engine/core';// Define a query for entities with Transform AND RigidBodyconst physicsQuery = defineQuery([Transform, RigidBody]);// Execute the query (returns entity ID array)export function MySystem(world: IWorld) {const entities = physicsQuery(world);console.log(`Found ${entities.length} physics entities`);// Iterate over resultsfor (let i = 0; i < entities.length; i++) {const eid = entities[i];// Access componentsconst x = Transform.position.x[eid];const mass = RigidBody.mass[eid];}return world;}
Query Caching
bitECS automatically caches query results. After the first call, subsequent calls to the same query are O(1) — just a pointer lookup. The cache is invalidated only when entities are added/removed or components are added/removed.
bitECS provides enterQuery and exitQuery to detect when entities enter or exit a query. This is useful for initialization and cleanup logic.
import { defineQuery, enterQuery } from 'bitecs';import { Mesh, Material } from '@web-engine/core';// Define base queryconst meshQuery = defineQuery([Mesh, Material]);// Get entities that just got Mesh + Materialconst meshEnterQuery = enterQuery(meshQuery);export function MeshInitSystem(world: IWorld) {// Only entities that JUST got both components this frameconst newMeshes = meshEnterQuery(world);for (let i = 0; i < newMeshes.length; i++) {const eid = newMeshes[i];console.log(`New mesh entity: ${eid}`);// Initialize Three.js mesh, upload to GPU, etc.initializeMesh(eid);}return world;}
import { defineQuery, exitQuery } from 'bitecs';import { AudioSource } from '@web-engine/core';const audioQuery = defineQuery([AudioSource]);const audioExitQuery = exitQuery(audioQuery);export function AudioCleanupSystem(world: IWorld) {// Entities that just lost AudioSource componentconst removedAudio = audioExitQuery(world);for (let i = 0; i < removedAudio.length; i++) {const eid = removedAudio[i];console.log(`Cleaning up audio for entity ${eid}`);// Stop audio, release resourcesstopAndReleaseAudio(eid);}return world;}
When Enter/Exit Triggers
enterQuery triggers when an entity gains the LAST component needed to match the query.
exitQuery triggers when an entity loses ANY component in the query.
bitECS supports query modifiers to create more complex filters.
import { defineQuery, Not } from 'bitecs';import { Transform, RigidBody, Static } from '@web-engine/core';// Find entities with Transform and RigidBody, but WITHOUT Staticconst dynamicPhysicsQuery = defineQuery([Transform,RigidBody,Not(Static), // Exclude static objects]);export function DynamicPhysicsSystem(world: IWorld) {const entities = dynamicPhysicsQuery(world);// Only dynamic (non-static) physics entitiesfor (let i = 0; i < entities.length; i++) {const eid = entities[i];// Process dynamic objects only}return world;}
Common Use Cases for Not()
Not() is useful for excluding disabled entities, filtering out specific states, or processing only active objects. For example: Not(Disabled), Not(Hidden), Not(Dead).
Web Engine provides typed query helpers that preserve component type information for better TypeScript support.
import { createTypedQuery, createTypedEnterQuery, createTypedExitQuery } from '@web-engine/core';import { Transform, Velocity, Health } from '@web-engine/core';// Create typed queryconst movingQuery = createTypedQuery([Transform, Velocity]);// Create typed enter queryconst healthEnter = createTypedEnterQuery(createTypedQuery([Health]));// Create typed exit queryconst healthExit = createTypedExitQuery(createTypedQuery([Health]));// TypeScript now knows which components are availableexport function MovementSystem(world: IWorld) {const entities = movingQuery(world);for (let i = 0; i < entities.length; i++) {const eid = entities[i];// TypeScript knows Transform and Velocity are availableTransform.position.x[eid] += Velocity.linear.x[eid];}return world;}
import { defineQuery, Not } from 'bitecs';import { Transform, Player, Enemy, Health, Dead } from '@web-engine/core';// Separate queries for different entity typesconst playerQuery = defineQuery([Transform, Player, Health, Not(Dead)]);const enemyQuery = defineQuery([Transform, Enemy, Health, Not(Dead)]);export function CombatSystem(world: IWorld) {const players = playerQuery(world);const enemies = enemyQuery(world);// Process playersfor (let i = 0; i < players.length; i++) {const playerEid = players[i];// Player logic}// Process enemiesfor (let j = 0; j < enemies.length; j++) {const enemyEid = enemies[j];// Enemy logic}return world;}
import { defineQuery } from 'bitecs';import { Transform, Parent, Mesh } from '@web-engine/core';// Query for all entities with parents (children)const childQuery = defineQuery([Transform, Parent]);// Query for all root entities (no parent)const rootQuery = defineQuery([Transform, Not(Parent)]);export function HierarchySystem(world: IWorld) {// Process root entities firstconst roots = rootQuery(world);for (let i = 0; i < roots.length; i++) {const eid = roots[i];// Process root}// Then process childrenconst children = childQuery(world);for (let i = 0; i < children.length; i++) {const eid = children[i];const parentEid = Parent.eid[eid];// Process child, using parent's transform}return world;}
import { defineQuery } from 'bitecs';import { Character, Idle, Walking, Running, Jumping } from '@web-engine/core';// Define queries for each stateconst idleQuery = defineQuery([Character, Idle]);const walkingQuery = defineQuery([Character, Walking]);const runningQuery = defineQuery([Character, Running]);const jumpingQuery = defineQuery([Character, Jumping]);export function CharacterStateSystem(world: IWorld) {// Process each state separatelyconst idleChars = idleQuery(world);for (let i = 0; i < idleChars.length; i++) {// Idle behavior}const walkingChars = walkingQuery(world);for (let i = 0; i < walkingChars.length; i++) {// Walking behavior}// ... and so onreturn world;}
bitECS queries are highly optimized and use multiple strategies to maximize performance:
Query results are cached after first execution. Subsequent calls are O(1) pointer lookups.
Queries use bitwise operations on dense bitsets for ultra-fast component matching.
Query results are only computed when needed and invalidated only on structural changes.
Query arrays are reused across frames. No garbage collection from query execution.
if (entities.length === 0) to skip empty iterationsskipWhenEmpty: true for conditional systemsReal-world query performance in Web Engine (measured on M1 Pro):
Query Performance (1 million entities, 100,000 matches):First call (uncached): ~2.5msSubsequent calls (cached): ~0.001ms (1 microsecond!)enterQuery (100 new): ~0.05msexitQuery (100 removed): ~0.05msWith Not() modifier: ~3ms (first), ~0.001ms (cached)3-component query: ~3.5ms (first), ~0.001ms (cached)5-component query: ~5ms (first), ~0.001ms (cached)Iteration over 100k matches: ~0.5ms (simple operations)Conclusion: Query overhead is negligible after first call.Focus optimization efforts on iteration logic, not queries.
// Query for entities that need physics bodies createdconst physicsQuery = createTypedQuery([Transform,RigidBody,Collider,Not(PhysicsBodyAdded), // Only entities without bodies yet]);// Query for entities with physics bodiesconst activePhysicsQuery = createTypedQuery([Transform,RigidBody,PhysicsBodyAdded,]);// Query for entities to remove from physicsconst physicsExitQuery = createTypedExitQuery(physicsQuery);
// Query for visible meshes (not culled)const visibleMeshQuery = createTypedQuery([Transform,Mesh,Not(Culled),Not(Hidden),]);// Query for instanced meshesconst instancedMeshQuery = createTypedQuery([Transform,InstancedMesh,Not(Culled),]);// Query for transparent meshes (for separate render pass)const transparentQuery = createTypedQuery([Transform,Mesh,TransparentMaterial,]);
visibleMeshQuery vs q1