Entities
Deep dive into entities: the fundamental building blocks of the ECS architecture. Learn about entity lifecycle, recycling, hierarchies, and metadata management.
In Web Engine's ECS architecture, entities are the simplest concept: they're just unsigned 32-bit integers that serve as unique identifiers. An entity has no data and no behavior on its own — it's merely an ID that ties components together.
What is an Entity?#
Think of an entity as a database primary key. It's a number that identifies a game object, but all the actual data lives in component arrays indexed by that number.
import { addEntity, addComponent } from 'bitecs';import { Transform, RigidBody, Mesh } from '@web-engine/core'; // Create a new entityconst eid = addEntity(world); // eid is just a numberconsole.log(eid); // 42 // Add components to give it data and behavioraddComponent(world, Transform, eid);addComponent(world, RigidBody, eid);addComponent(world, Mesh, eid); // Now set component dataTransform.position.x[eid] = 10.0;RigidBody.mass[eid] = 50.0;Entity IDs are Sequential
bitECS assigns entity IDs sequentially starting from 0. The first entity is 0, second is 1, and so on. When entities are removed, their IDs are recycled to keep the arrays compact.
Entity Lifecycle#
Entities go through a well-defined lifecycle from creation to destruction:
Creation#
import { addEntity, addComponent } from 'bitecs';import { Transform, setEntityDisplayName } from '@web-engine/core'; // Create a new entityconst eid = addEntity(world); // Always add Transform (required for all game objects)addComponent(world, Transform, eid); // Set default transform valuesTransform.position.x[eid] = 0;Transform.position.y[eid] = 0;Transform.position.z[eid] = 0;Transform.scale.x[eid] = 1;Transform.scale.y[eid] = 1;Transform.scale.z[eid] = 1; // Set a display name (stored in EntityMetadataStore)setEntityDisplayName(eid, 'Player');Destruction#
When removing an entity, Web Engine ensures all components and metadata are cleaned up properly. The EntityCleanupSystem handles this at the end of each frame.
import { removeEntity } from 'bitecs';import { clearEntityMetadata } from '@web-engine/core'; // Remove an entityremoveEntity(world, eid); // Clean up metadata (done automatically by EntityCleanupSystem)clearEntityMetadata(eid); // The entity ID is now recycled and can be reusedDeferred Cleanup
Web Engine uses deferred entity cleanup to avoid issues with systems still processing entities. When you call removeEntity, the entity is marked for removal but not immediately destroyed. The EntityCleanupSystem runs at the end of the frame to finalize cleanup.
Entity Recycling#
bitECS automatically recycles entity IDs to prevent unbounded growth of internal arrays. When an entity is removed, its ID goes into a free list and will be reused for the next entity creation.
// Create first entityconst eid1 = addEntity(world); // ID: 0 // Create second entityconst eid2 = addEntity(world); // ID: 1 // Remove first entityremoveEntity(world, eid1); // Create third entity - reuses ID 0const eid3 = addEntity(world); // ID: 0 (recycled!) console.log(eid3 === eid1); // trueBenefits of Recycling#
- Compact arrays — Component arrays stay dense, improving cache efficiency
- Bounded memory — Maximum entity count is fixed (default: 1 million)
- No fragmentation — Gaps are immediately filled by new entities
- Predictable performance — No allocation spikes from array growth
Stale Entity References
Because entity IDs are recycled, storing entity IDs long-term can be dangerous. If you hold a reference to entity 42, destroy it, then create a new entity, ID 42 might refer to a completely different object. Always validate entity references before use.
Finding Entities with Queries#
Queries let you find all entities with a specific set of components. Web Engine uses bitECS queries, which are automatically cached for O(1) performance after the first call.
import { defineQuery } from 'bitecs';import { Transform, RigidBody, Mesh } from '@web-engine/core'; // Define a query for all entities with Transform and RigidBodyconst physicsQuery = defineQuery([Transform, RigidBody]); // Execute the query (cached after first call)const entities = physicsQuery(world); // Iterate over resultsfor (let i = 0; i < entities.length; i++) { const eid = entities[i]; // Access components const mass = RigidBody.mass[eid]; const x = Transform.position.x[eid]; console.log(`Entity ${eid} at (${x}, ...) with mass ${mass}`);}For more details on queries, see the Queries documentation.
Entity Hierarchies#
Web Engine supports parent-child relationships between entities using the Parent and LocalTransform components. This enables scene graphs where child entities inherit their parent's transform.
import { addComponent } from 'bitecs';import { Transform, LocalTransform, Parent } from '@web-engine/core'; // Create parent entityconst parentEid = addEntity(world);addComponent(world, Transform, parentEid);Transform.position.x[parentEid] = 10.0; // Create child entityconst childEid = addEntity(world);addComponent(world, Transform, childEid);addComponent(world, LocalTransform, childEid);addComponent(world, Parent, childEid); // Set parent referenceParent.eid[childEid] = parentEid; // Set local transform (relative to parent)LocalTransform.position.x[childEid] = 5.0; // Child's world position will be 15.0 (parent 10.0 + local 5.0)Hierarchy Components#
Transform— World-space transform (final calculated position)LocalTransform— Local-space transform (relative to parent)Parent— Stores parent entity IDSiblingIndex— Order among siblings (for editor)
Transform Propagation
Web Engine uses WASM-accelerated transform propagation to update world transforms from local transforms efficiently. The WasmTransformSystem processes entire hierarchies in parallel.
Entity Metadata#
Not all entity data fits in TypedArrays. For strings, objects, and complex data, Web Engine uses the EntityMetadataStore — a unified Map that stores non-primitive data.
import { setEntityDisplayName, getEntityDisplayName, setMetadata, getMetadata,} from '@web-engine/core'; // Set display namesetEntityDisplayName(eid, 'Player 1'); // Get display nameconst name = getEntityDisplayName(eid); // "Player 1" // Store custom metadatasetMetadata(eid, 'playerInfo', { name: 'Alice', avatarId: 'avatar_001',}); // Retrieve custom metadataconst info = getMetadata(eid, 'playerInfo');console.log(info.name); // "Alice"Common Metadata Types#
name— Display name for editor and debuggingbillboard— Billboard widget metadatainteractable— Interaction prompt and actionplayerInfo— Player display infoinventory— Inventory items arrayanimationRuntime— Animation graph statelogicRuntime— Logic graph execution state
Memory Efficiency
EntityMetadataStore only allocates memory for entities that actually have metadata. Empty metadata objects are automatically cleaned up to prevent memory leaks.
Prefabs and Templates#
Web Engine supports entity prefabs — reusable templates that can be instantiated multiple times with different configurations.
import { spawnPrefab, PrefabRegistry } from '@web-engine/core'; // Define a prefab templatePrefabRegistry.register({ name: 'Crate', components: [ { type: 'Transform', data: { scale: [1, 1, 1] } }, { type: 'RigidBody', data: { mass: 10.0, friction: 0.5 } }, { type: 'Collider', data: { type: 0, size: [0.5, 0.5, 0.5] } }, { type: 'Mesh', data: { geometryType: 'box' } }, ],}); // Spawn instances of the prefabconst crate1 = spawnPrefab(world, 'Crate', { position: [0, 1, 0] });const crate2 = spawnPrefab(world, 'Crate', { position: [5, 1, 0] });const crate3 = spawnPrefab(world, 'Crate', { position: [10, 1, 0] });Best Practices#
- Always add Transform — Every game object should have Transform as a base
- Use display names — Set meaningful names for debugging and editor display
- Clean up metadata — Always call clearEntityMetadata when destroying entities
- Validate references — Check if an entity still has required components before use
- Use prefabs — Define common entity types as prefabs for consistency
- Batch creation — Create multiple entities in one frame for better performance