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

Components hold data. Behavior is defined in systems.

Flexible Format

Use SoA, AoS, TypedArrays, or any format you prefer.

By Reference

Component identity is determined by reference, not structure.
// 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 200

Defining Components

Structure of Arrays (SoA)

The recommended format for optimal performance. Each property is an array indexed by entity ID:

soa.ts
// 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] = 100
Why SoA?

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

typed.ts
// 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),
}
TypeBytesUse Case
Float32Array4Graphics, moderate precision
Float64Array8Physics, high precision
Int8Array / Uint8Array1Flags, small integers (0-255)
Int16Array / Uint16Array2IDs, medium integers
Int32Array / Uint32Array4Large integers, entity refs

SharedArrayBuffer for Multithreading

shared.ts
// 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 directly

Array of Structures (AoS)

For simpler code or complex nested data, use object-per-entity format:

aos.ts
// 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,
}
i
AoS is slightly slower for simple components but excels at complex, nested data that would be awkward in SoA format.

Tag Components

Components with no data are called "tags". They're useful for marking entities for queries without storing any values:

tags.ts
// 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:

adding.ts
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:

set.ts
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 })
)
!
set() Requires an Observer
Without an 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

FunctionDescription
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)
utilities.ts
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] = 100

Approach 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
!
If using global components with multiple worlds, you must use a shared entity index. Otherwise, entity IDs may collide and corrupt data.

Next Steps

Continue Learning