Component
Components are modular data containers that define entity characteristics.
Overview
Components define what an entity has, not what it does. In bitECS, components can be any JavaScript reference - their identity is determined by reference equality.
Data Only
Flexible Format
By Reference
// Define a component
const Position = { x: [] as number[], y: [] as number[] }
// Add to entity
addComponent(world, eid, Position)
// Set data using entity ID as index
Position.x[eid] = 100
Position.y[eid] = 200
console.log(Position.x[eid], Position.y[eid]) // 100 200Defining Components
Structure of Arrays (SoA)
The recommended format for optimal performance. Each property is an array indexed by entity ID:
// SoA component - recommended for performance
const Position = {
x: [] as number[],
y: [] as number[],
}
const Velocity = {
x: [] as number[],
y: [] as number[],
}
const Health = {
current: [] as number[],
max: [] as number[],
}
// Access data by entity ID
Position.x[entity] = 100
Position.y[entity] = 200
Health.current[entity] = 100
Health.max[entity] = 100SoA keeps similar data together in memory. When iterating over all x positions, you read from contiguous memory rather than jumping between objects. This is more cache-friendly and typically faster for large datasets.
Typed Arrays
For fixed-size worlds or multithreading, use TypedArrays. They provide predictable memory layout, zero-initialization (ZAII-friendly), and enable SharedArrayBuffer for web workers:
// Pre-allocate TypedArrays for 10,000 entities
const Position = {
x: new Float32Array(10000),
y: new Float32Array(10000),
}
// Use Float32 for graphics, Float64 for physics precision
const Transform = {
x: new Float32Array(10000),
y: new Float32Array(10000),
rotation: new Float32Array(10000),
scale: new Float32Array(10000),
}
// Integer types for flags/IDs
const Sprite = {
textureId: new Uint16Array(10000),
frame: new Uint8Array(10000),
layer: new Int8Array(10000),
}| Type | Bytes | Use Case |
|---|---|---|
Float32Array | 4 | Graphics, moderate precision |
Float64Array | 8 | Physics, high precision |
Int8Array / Uint8Array | 1 | Flags, small integers (0-255) |
Int16Array / Uint16Array | 2 | IDs, medium integers |
Int32Array / Uint32Array | 4 | Large integers, entity refs |
SharedArrayBuffer for Multithreading
// Allocate shared memory for worker threads
const MAX_ENTITIES = 10000
const sharedBuffer = new SharedArrayBuffer(MAX_ENTITIES * 8 * 2) // 2 Float64s
const Position = {
x: new Float64Array(sharedBuffer, 0, MAX_ENTITIES),
y: new Float64Array(sharedBuffer, MAX_ENTITIES * 8, MAX_ENTITIES),
}
// Pass sharedBuffer to workers - they can read/write Position directlyArray of Structures (AoS)
For simpler code or complex nested data, use object-per-entity format:
// AoS component - simpler API
const Position = [] as { x: number; y: number }[]
// Set data as object
Position[entity] = { x: 100, y: 200 }
// Access properties
Position[entity].x += 1
Position[entity].y += 1
// Complex nested data
const Inventory = [] as {
slots: { itemId: number; count: number }[]
gold: number
}[]
Inventory[entity] = {
slots: [
{ itemId: 1, count: 5 },
{ itemId: 2, count: 1 },
],
gold: 100,
}Tag Components
Components with no data are called "tags". They're useful for marking entities for queries without storing any values:
// Tag components - just empty objects
const Player = {}
const Enemy = {}
const Alive = {}
const Dead = {}
const Grounded = {}
const Flying = {}
// Add tags like any component
addComponent(world, entity, Player)
addComponent(world, entity, Alive)
// Query by tags
const players = query(world, [Player, Alive])
const enemies = query(world, [Enemy, Not(Dead)])Adding Components
Use addComponent() to attach components to entities:
import { addComponent, addComponents, removeComponent } from 'bitecs'
// Add a single component
addComponent(world, entity, Position)
// Set initial values manually
Position.x[entity] = 100
Position.y[entity] = 200
// Add multiple components at once
addComponents(world, entity, Position, Velocity, Health)
// or as an array
addComponents(world, entity, [Position, Velocity, Health])
// Remove a component
removeComponent(world, entity, Position)
// Remove multiple components
removeComponent(world, entity, Position, Velocity)The set() Helper
Use set() to add a component with initial data in one call. This requires an onSet observer to handle the data:
import { addComponent, set, observe, onSet } from 'bitecs'
// First, set up an observer to handle the data
observe(world, onSet(Position), (eid, params) => {
Position.x[eid] = params.x
Position.y[eid] = params.y
})
// Now set() will work - data is passed to the observer
addComponent(world, entity, set(Position, { x: 100, y: 200 }))
// Works with addComponents too
addComponents(world, entity,
set(Position, { x: 0, y: 0 }),
set(Velocity, { x: 1, y: 0 }),
set(Health, { current: 100, max: 100 })
)onSet observer, set() does nothing with the data. The observer is what actually writes values to your component arrays. See Observers for more details.Component Utilities
| Function | Description |
|---|---|
registerComponent(world, comp) | Explicitly register a component |
registerComponents(world, comps) | Register multiple components |
hasComponent(world, eid, comp) | Check if entity has component |
getComponent(world, eid, comp) | Get data (triggers onGet) |
setComponent(world, eid, comp, data) | Set data (triggers onSet) |
import {
registerComponent,
registerComponents,
hasComponent,
getComponent,
setComponent
} from 'bitecs'
// Explicitly register (usually automatic on first add)
registerComponent(world, Position)
registerComponents(world, [Position, Velocity, Mass])
// Check if entity has component
if (hasComponent(world, entity, Position)) {
// Safe to access Position data
const x = Position.x[entity]
}
// Get component data (triggers onGet observers)
const posData = getComponent(world, entity, Position)
// Set component data directly (triggers onSet observers)
setComponent(world, entity, Position, { x: 10, y: 20 })Multiple Worlds
When using multiple worlds, you have two approaches:
Approach 1: Components in Context
Define components on each world's context for complete isolation:
const world = createWorld({
components: {
Position: { x: [] as number[], y: [] as number[] },
Velocity: { x: [] as number[], y: [] as number[] },
}
})
// Access via world context
const { Position, Velocity } = world.components
Position.x[entity] = 100Approach 2: Global Components with Shared Index
Share components across worlds using a shared entity index:
// Global components - defined once
const Position = { x: [] as number[], y: [] as number[] }
const Velocity = { x: [] as number[], y: [] as number[] }
// Shared entity index is REQUIRED for global components
const entityIndex = createEntityIndex()
const world1 = createWorld(entityIndex)
const world2 = createWorld(entityIndex)
// Entity IDs are unique across worlds
const e1 = addEntity(world1) // 1
const e2 = addEntity(world2) // 2
// Both worlds can safely use the same components
Position.x[e1] = 100
Position.x[e2] = 200