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.

packages/core/src/engine/ecs/components/physics.ts
typescript
import { defineComponent, Types } from 'bitecs';
// Define a physics component
export 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 mixed
class 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 efficiency
const players = [new Player(), new Player(), new Player()];

ECS Approach#

// ECS - pure data in components
const 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 separately
function 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 efficiency

Cache 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:

TypeRangeStorageUse Case
Types.f32-3.4e38 to 3.4e38Float32ArrayPositions, velocities, physics values
Types.f64-1.8e308 to 1.8e308Float64ArrayHigh-precision calculations
Types.ui80 to 255Uint8ArrayFlags, small counters, enums
Types.ui160 to 65,535Uint16ArrayBitmasks, medium counters
Types.ui320 to 4,294,967,295Uint32ArrayEntity IDs, large counters
Types.i8-128 to 127Int8ArraySigned small values
Types.i16-32,768 to 32,767Int16ArraySigned medium values
Types.i32-2.1B to 2.1BInt32ArraySigned large values
Types.eid0 to 4,294,967,295Uint32ArrayEntity 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 only
const prop = addEntity(world);
addComponent(world, Transform, prop);
addComponent(world, Mesh, prop);
// Dynamic physics object - Add RigidBody + Collider
const crate = addEntity(world);
addComponent(world, Transform, crate);
addComponent(world, Mesh, crate);
addComponent(world, RigidBody, crate);
addComponent(world, Collider, crate);
// Light source - Transform + Light
const lamp = addEntity(world);
addComponent(world, Transform, lamp);
addComponent(world, Light, lamp);
// Moving light - Transform + Light + Velocity
const 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#

packages/core/src/engine/ecs/components/transform.ts
typescript
// 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 relationship
export const Parent = defineComponent({
eid: Types.ui32, // Parent entity ID
});
// Sibling ordering
export const SiblingIndex = defineComponent({
value: Types.ui32,
});

Physics Components#

packages/core/src/engine/ecs/components/physics.ts
typescript
// Rigid body physics
export 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 shape
export 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#

packages/core/src/engine/ecs/components/render.ts
typescript
// Mesh renderer
export 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 source
export 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,
});
// Camera
export 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#

packages/core/src/engine/ecs/components/gameplay.ts
typescript
// Health system
export const Health = defineComponent({
current: Types.f32,
max: Types.f32,
regenRate: Types.f32, // HP per second
invulnerable: Types.ui8,
});
// Character controller
export const CharacterController = defineComponent({
speed: Types.f32,
jumpForce: Types.f32,
isGrounded: Types.ui8,
height: Types.f32,
radius: Types.f32,
slopeLimit: Types.f32,
});
// Lifetime timer
export 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 entity
const eid = addEntity(world);
// Add components
addComponent(world, Transform, eid);
addComponent(world, RigidBody, eid);
addComponent(world, Mesh, eid);
// Initialize component data
Transform.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; // Box
Mesh.castShadow[eid] = 1;

Checking Components#

import { hasComponent } from 'bitecs';
// Check if entity has a component
if (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 entity
removeComponent(world, RigidBody, eid);
// Entity no longer matches queries that require RigidBody
// Component data is zeroed out and slot is recycled

Component 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.

src/components/Weapon.ts
typescript
import { defineComponent, Types } from 'bitecs';
// Custom weapon component
export 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 component
export const Inventory = defineComponent({
capacity: Types.ui8,
itemCount: Types.ui8,
selectedSlot: Types.ui8,
weight: Types.f32,
maxWeight: Types.f32,
});
// Quest progress tracking
export 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.

src/registerComponents.ts
typescript
import { ComponentRegistry } from '@web-engine/core';
import { Weapon, Inventory, QuestProgress } from './components';
// Register components with metadata
ComponentRegistry.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 categorization
export const Player = defineComponent({});
export const Enemy = defineComponent({});
export const Projectile = defineComponent({});
// Query for all player entities
const playerQuery = defineQuery([Player, Transform]);
// Query for all enemies
const 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,
});
Components | Web Engine Docs