Components
Master ECS components: pure data structures that define entity properties. Learn about component composition, TypedArrays, built-in components, and the data-only design principle.
In Web Engine's ECS architecture, components are pure data containers with no behavior. They store properties in TypedArrays for optimal cache efficiency and performance. Components are composed together on entities to define their characteristics and capabilities.
What is a Component?#
A component is a schema that defines one or more properties stored in parallel TypedArrays. Each property has a specific numeric type (f32, ui32, etc.) that determines its storage format. Components have no methods — they're just data.
import { defineComponent, Types } from 'bitecs'; // Define a physics componentexport const RigidBody = defineComponent({ velocity: [Types.f32, 3], // 3D vector (x, y, z) angularVelocity: [Types.f32, 3], mass: Types.f32, // Single float friction: Types.f32, restitution: Types.f32, type: Types.ui8, // 0-255 integer ccd: Types.ui8, // Boolean flag (0 or 1) lockRotation: [Types.ui8, 3], // Boolean array});Data-Only Design
Components contain zero logic. All behavior lives in Systems. This separation enables better performance, easier testing, and clearer architecture.
Components vs Class-Based Approach#
Traditional OOP uses classes with data and methods tightly coupled. ECS separates data (Components) from logic (Systems) for better performance and flexibility.
Traditional OOP Approach#
// Traditional OOP - data and behavior mixedclass Player { position = { x: 0, y: 0, z: 0 }; health = 100; inventory: Item[] = []; // Behavior mixed with data update(delta: number) { this.applyGravity(delta); this.checkCollisions(); this.updateAnimation(); } takeDamage(amount: number) { this.health -= amount; this.playHurtAnimation(); }} // Scattered in memory, poor cache efficiencyconst players = [new Player(), new Player(), new Player()];ECS Approach#
// ECS - pure data in componentsconst Transform = defineComponent({ position: [Types.f32, 3], rotation: [Types.f32, 3], scale: [Types.f32, 3],}); const Health = defineComponent({ current: Types.f32, max: Types.f32,}); const Inventory = defineComponent({ capacity: Types.ui8, itemCount: Types.ui8,}); // Systems handle behavior separatelyfunction HealthSystem(world: IWorld, delta: number) { const entities = healthQuery(world); for (let i = 0; i < entities.length; i++) { const eid = entities[i]; // Process health logic for all entities at once if (Health.current[eid] <= 0) { handleDeath(eid); } } return world;} // Data packed in TypedArrays - excellent cache efficiencyCache Efficiency
Components store data in contiguous TypedArrays. Systems iterate through hot data in sequence, maximizing CPU cache hits.
Composition Over Inheritance
Build complex entities by combining simple components instead of deep inheritance hierarchies.
Reusable Logic
Systems process any entity with required components. Logic is reusable across entity types.
Zero GC Pressure
TypedArrays are pre-allocated and reused. No garbage collection during gameplay.
Component Property Types#
bitECS provides several TypedArray property types optimized for different data ranges:
| Type | Range | Storage | Use Case |
|---|---|---|---|
| Types.f32 | -3.4e38 to 3.4e38 | Float32Array | Positions, velocities, physics values |
| Types.f64 | -1.8e308 to 1.8e308 | Float64Array | High-precision calculations |
| Types.ui8 | 0 to 255 | Uint8Array | Flags, small counters, enums |
| Types.ui16 | 0 to 65,535 | Uint16Array | Bitmasks, medium counters |
| Types.ui32 | 0 to 4,294,967,295 | Uint32Array | Entity IDs, large counters |
| Types.i8 | -128 to 127 | Int8Array | Signed small values |
| Types.i16 | -32,768 to 32,767 | Int16Array | Signed medium values |
| Types.i32 | -2.1B to 2.1B | Int32Array | Signed large values |
| Types.eid | 0 to 4,294,967,295 | Uint32Array | Entity references |
Choose the Right Type
Use the smallest type that fits your data range. Smaller types improve cache efficiency and reduce memory usage. For example, use ui8 for boolean flags instead of ui32.
Component Composition#
Entities are defined by which components they have. The same components can be combined in different ways to create diverse entity types.
import { addEntity, addComponent } from 'bitecs';import { Transform, RigidBody, Collider, Mesh, Light } from '@web-engine/core'; // Static prop - Transform + Mesh onlyconst prop = addEntity(world);addComponent(world, Transform, prop);addComponent(world, Mesh, prop); // Dynamic physics object - Add RigidBody + Colliderconst crate = addEntity(world);addComponent(world, Transform, crate);addComponent(world, Mesh, crate);addComponent(world, RigidBody, crate);addComponent(world, Collider, crate); // Light source - Transform + Lightconst lamp = addEntity(world);addComponent(world, Transform, lamp);addComponent(world, Light, lamp); // Moving light - Transform + Light + Velocityconst floatingOrb = addEntity(world);addComponent(world, Transform, floatingOrb);addComponent(world, Light, floatingOrb);addComponent(world, Velocity, floatingOrb);This composition-based approach means you can freely add/remove components to change entity behavior at runtime without inheritance constraints.
Built-in Components#
Web Engine provides a comprehensive set of built-in components organized by category:
Transform Components#
// World-space transform (final calculated position)export const Transform = defineComponent({ position: [Types.f32, 3], rotation: [Types.f32, 3], // Euler angles in radians scale: [Types.f32, 3], quaternion: [Types.f32, 4], // Normalized quaternion parent: Types.eid, // Parent entity reference}); // Local-space transform (relative to parent)export const LocalTransform = defineComponent({ position: [Types.f32, 3], rotation: [Types.f32, 3], scale: [Types.f32, 3], quaternion: [Types.f32, 4],}); // Hierarchy relationshipexport const Parent = defineComponent({ eid: Types.ui32, // Parent entity ID}); // Sibling orderingexport const SiblingIndex = defineComponent({ value: Types.ui32,});Physics Components#
// Rigid body physicsexport const RigidBody = defineComponent({ velocity: [Types.f32, 3], angularVelocity: [Types.f32, 3], mass: Types.f32, friction: Types.f32, restitution: Types.f32, type: Types.ui8, // 0=Dynamic, 1=Fixed, 2=Kinematic ccd: Types.ui8, // Continuous collision detection lockRotation: [Types.ui8, 3],}); // Collision shapeexport const Collider = defineComponent({ type: Types.ui8, // 0=Box, 1=Sphere, 2=Capsule, etc. size: [Types.f32, 3], offset: [Types.f32, 3], isSensor: Types.ui8, collisionGroup: Types.ui16, collisionMask: Types.ui16,}); // Simple velocity (no physics simulation)export const Velocity = defineComponent({ linear: [Types.f32, 3], angular: [Types.f32, 3],});Rendering Components#
// Mesh rendererexport const Mesh = defineComponent({ geometryType: Types.ui8, // 0=box, 1=sphere, 2=custom, etc. materialSlot: Types.ui8, castShadow: Types.ui8, receiveShadow: Types.ui8, visible: Types.ui8, renderOrder: Types.i16, frustumCulled: Types.ui8,}); // Light sourceexport const Light = defineComponent({ type: Types.ui8, // 0=Point, 1=Directional, 2=Spot color: [Types.f32, 3], // RGB 0-1 intensity: Types.f32, range: Types.f32, castShadow: Types.ui8, shadowMapSize: Types.ui16,}); // Cameraexport const Camera = defineComponent({ fov: Types.f32, // Field of view in degrees near: Types.f32, // Near clipping plane far: Types.f32, // Far clipping plane orthographic: Types.ui8, // 0=perspective, 1=orthographic active: Types.ui8, // Is this the active camera});Gameplay Components#
// Health systemexport const Health = defineComponent({ current: Types.f32, max: Types.f32, regenRate: Types.f32, // HP per second invulnerable: Types.ui8,}); // Character controllerexport const CharacterController = defineComponent({ speed: Types.f32, jumpForce: Types.f32, isGrounded: Types.ui8, height: Types.f32, radius: Types.f32, slopeLimit: Types.f32,}); // Lifetime timerexport const Lifetime = defineComponent({ duration: Types.f32, // Total lifetime in seconds elapsed: Types.f32, // Time elapsed});Component Lifecycle#
Components follow a simple lifecycle: they're added to entities, their data is updated by systems, and eventually removed when no longer needed.
Adding Components#
import { addEntity, addComponent } from 'bitecs';import { Transform, RigidBody, Mesh } from '@web-engine/core'; // Create entityconst eid = addEntity(world); // Add componentsaddComponent(world, Transform, eid);addComponent(world, RigidBody, eid);addComponent(world, Mesh, eid); // Initialize component dataTransform.position.x[eid] = 0;Transform.position.y[eid] = 5;Transform.position.z[eid] = 0; RigidBody.mass[eid] = 10.0;RigidBody.friction[eid] = 0.5;RigidBody.type[eid] = 0; // Dynamic Mesh.geometryType[eid] = 0; // BoxMesh.castShadow[eid] = 1;Checking Components#
import { hasComponent } from 'bitecs'; // Check if entity has a componentif (hasComponent(world, RigidBody, eid)) { console.log('Entity has physics'); // Safe to access component data const mass = RigidBody.mass[eid]; console.log(`Mass: ${mass}`);}Removing Components#
import { removeComponent } from 'bitecs'; // Remove a component from an entityremoveComponent(world, RigidBody, eid); // Entity no longer matches queries that require RigidBody// Component data is zeroed out and slot is recycledComponent Data Zeroing
When a component is removed, its data slots are zeroed out and made available for reuse. Don't hold references to component data after removal.
Creating Custom Components#
You can define custom components for game-specific data. Keep them focused on a single concern and use composition to combine them.
import { defineComponent, Types } from 'bitecs'; // Custom weapon componentexport const Weapon = defineComponent({ damage: Types.f32, fireRate: Types.f32, // Shots per second ammo: Types.ui16, maxAmmo: Types.ui16, range: Types.f32, spread: Types.f32, // Accuracy cone in degrees reloadTime: Types.f32, isReloading: Types.ui8, lastFireTime: Types.f32,}); // Custom inventory componentexport const Inventory = defineComponent({ capacity: Types.ui8, itemCount: Types.ui8, selectedSlot: Types.ui8, weight: Types.f32, maxWeight: Types.f32,}); // Quest progress trackingexport const QuestProgress = defineComponent({ questId: Types.ui32, step: Types.ui8, completed: Types.ui8, failed: Types.ui8, timeStarted: Types.f32,});Component Registration#
Web Engine uses ComponentRegistry to track component schemas for serialization, networking, and editor integration. Register custom components to enable these features.
import { ComponentRegistry } from '@web-engine/core';import { Weapon, Inventory, QuestProgress } from './components'; // Register components with metadataComponentRegistry.register({ name: 'Weapon', component: Weapon, schema: { damage: { type: 'f32' }, fireRate: { type: 'f32' }, ammo: { type: 'ui16' }, maxAmmo: { type: 'ui16' }, range: { type: 'f32' }, spread: { type: 'f32' }, reloadTime: { type: 'f32' }, isReloading: { type: 'ui8' }, lastFireTime: { type: 'f32' }, }, category: 'Gameplay', version: 1,}); ComponentRegistry.register({ name: 'Inventory', component: Inventory, schema: { capacity: { type: 'ui8' }, itemCount: { type: 'ui8' }, selectedSlot: { type: 'ui8' }, weight: { type: 'f32' }, maxWeight: { type: 'f32' }, }, category: 'Gameplay', version: 1,});Why Register Components?
Registration enables automatic serialization, network replication, editor inspection, and schema validation. Unregistered components won't appear in the editor or serialize correctly.
Best Practices#
- Keep components focused — Each component should represent one logical concept (Transform, Health, Inventory)
- Use smallest types — Choose the smallest numeric type that fits your range for better cache efficiency
- Prefer composition — Combine simple components instead of creating large monolithic ones
- No logic in components — Components are pure data. All behavior belongs in Systems
- Initialize defaults — Always set default values when adding components to avoid NaN or undefined behavior
- Register for serialization — Register custom components in ComponentRegistry for save/load support
- Group related data — Store tightly coupled data in the same component (position.x/y/z together)
- Avoid string/object storage — Use numeric types in components, store complex data in EntityMetadataStore
Common Patterns#
Tag Components#
Tag components have no data — they're markers for entity categorization.
// Tag components for categorizationexport const Player = defineComponent({});export const Enemy = defineComponent({});export const Projectile = defineComponent({}); // Query for all player entitiesconst playerQuery = defineQuery([Player, Transform]); // Query for all enemiesconst enemyQuery = defineQuery([Enemy, Health]);Flags and State Components#
Use small integer types for boolean flags and state machines.
export const AIState = defineComponent({ state: Types.ui8, // 0=Idle, 1=Patrol, 2=Chase, 3=Attack previousState: Types.ui8, stateTime: Types.f32, // Time in current state}); export const Flags = defineComponent({ isGrounded: Types.ui8, isJumping: Types.ui8, isCrouching: Types.ui8, isRunning: Types.ui8,});Timer and Counter Components#
export const Cooldown = defineComponent({ duration: Types.f32, remaining: Types.f32, active: Types.ui8,}); export const SpawnCounter = defineComponent({ maxSpawns: Types.ui8, currentSpawns: Types.ui8, spawnInterval: Types.f32, timeSinceSpawn: Types.f32,});