System

Systems define entity behavior by operating on components.

Overview

In bitECS, systems are just functions. There's no formal System class or registration - you simply write functions that query for entities and update their component data.

Just Functions

No classes, no inheritance. Pure functions that take a world.

Data-Oriented

Query for entities, process their data, move on.

Composable

Chain systems together in any order you need.
const movementSystem = (world) => {
  for (const eid of query(world, [Position, Velocity])) {
    Position.x[eid] += Velocity.x[eid]
    Position.y[eid] += Velocity.y[eid]
  }
}

// Call the system
movementSystem(world)

Defining Systems

A system is a function that takes a world and operates on entities:

systems.ts
import { query } from 'bitecs'

// Movement system - updates positions based on velocity
const movementSystem = (world) => {
  for (const eid of query(world, [Position, Velocity])) {
    Position.x[eid] += Velocity.x[eid]
    Position.y[eid] += Velocity.y[eid]
  }
}

// Gravity system - applies gravity to entities with mass
const gravitySystem = (world) => {
  const GRAVITY = 9.81
  for (const eid of query(world, [Velocity, Mass])) {
    Velocity.y[eid] += GRAVITY * Mass.value[eid]
  }
}

// Collision system - handles entity collisions
const collisionSystem = (world) => {
  const entities = query(world, [Position, Collider])

  for (let i = 0; i < entities.length; i++) {
    for (let j = i + 1; j < entities.length; j++) {
      const a = entities[i]
      const b = entities[j]
      // Check collision between a and b...
    }
  }
}

Common System Patterns

PatternDescription
TransformUpdate position, rotation, scale based on velocity
InputRead keyboard/mouse state, update Input component
AIMake decisions, set target positions or velocities
PhysicsApply forces, handle collisions, resolve constraints
AnimationUpdate sprite frames, blend between states
RenderDraw entities to canvas/WebGL
CleanupRemove dead entities, process removals

System Pipeline

Organize systems into a pipeline that runs each frame:

pipeline.ts
// Define your system pipeline
const systems = [
  timeSystem,      // Update delta time first
  inputSystem,     // Read player input
  aiSystem,        // AI decisions
  movementSystem,  // Apply velocities
  gravitySystem,   // Apply gravity
  collisionSystem, // Handle collisions
  cleanupSystem,   // Remove dead entities
  renderSystem,    // Draw everything
]

// Run all systems
const runSystems = (world) => {
  for (const system of systems) {
    system(world)
  }
}

// Game loop
const loop = () => {
  runSystems(world)
  requestAnimationFrame(loop)
}
loop()

Pipeline Helper

Create a reusable pipeline function to compose systems:

const pipeline = (...systems) => (world) => {
  for (const system of systems) system(world)
}

const update = pipeline(
  timeSystem,
  inputSystem,
  movementSystem,
  renderSystem
)

// Run all systems
update(world)
!
System Order Matters
  • Input → AI → Movement (input affects AI affects movement)
  • Movement → Collision (move first, then resolve overlaps)
  • Everything → Render (draw after all updates complete)
  • Cleanup at end (remove entities after they're processed)

Conditional Systems

Run systems only when certain conditions are met:

conditional.ts
// Only run when game is not paused
const gameplayPipeline = (world) => {
  if (world.paused) return

  movementSystem(world)
  collisionSystem(world)
  aiSystem(world)
}

// Only run every N frames
let frameCount = 0
const throttledAI = (world) => {
  frameCount++
  if (frameCount % 3 !== 0) return  // Run every 3rd frame

  for (const eid of query(world, [AI])) {
    // Expensive AI calculations...
  }
}

// Phase-based systems
const renderPhase = (world) => {
  renderBackground(world)
  renderEntities(world)
  renderUI(world)
  renderDebug(world)
}

Delta Time

Use custom world context to pass delta time to systems:

delta-time.ts
// World with time context
const world = createWorld({
  time: {
    delta: 0,
    elapsed: 0,
    then: performance.now(),
  }
})

// Time system - updates delta and elapsed time
const timeSystem = (world) => {
  const now = performance.now()
  world.time.delta = (now - world.time.then) / 1000 // seconds
  world.time.elapsed += world.time.delta
  world.time.then = now
}

// Use delta time in movement (frame-rate independent)
const movementSystem = (world) => {
  const dt = world.time.delta

  for (const eid of query(world, [Position, Velocity])) {
    Position.x[eid] += Velocity.x[eid] * dt
    Position.y[eid] += Velocity.y[eid] * dt
  }
}

Game Loop Integration

For browser games, use requestAnimationFrame. For server-side or headless simulations, Bun.sleep provides sub-millisecond precision:

Browser (requestAnimationFrame)

browser-loop.ts
const gameLoop = () => {
  timeSystem(world)
  inputSystem(world)
  movementSystem(world)
  collisionSystem(world)
  renderSystem(world)

  requestAnimationFrame(gameLoop)
}

gameLoop()

Server / Headless (Bun.sleep)

Bun.sleep is much faster and more precise than Node's setTimeout, enabling sub-millisecond timing for high-frequency game loops:

server-loop.ts
const TICK_RATE = 60
const TICK_MS = 1000 / TICK_RATE

const gameLoop = async () => {
  while (true) {
    const start = performance.now()

    timeSystem(world)
    physicsSystem(world)
    aiSystem(world)
    networkSyncSystem(world)

    const elapsed = performance.now() - start
    const sleepTime = Math.max(0, TICK_MS - elapsed)
    await Bun.sleep(sleepTime)
  }
}

gameLoop()

Fixed Timestep

For deterministic physics, use a fixed timestep with accumulator:

fixed-timestep.ts
const FIXED_STEP = 1 / 60 // 60 updates per second
let accumulator = 0

const fixedGameLoop = () => {
  const now = performance.now()
  const frameTime = (now - world.time.then) / 1000
  world.time.then = now

  accumulator += frameTime

  // Run physics at fixed rate (may run multiple times per frame)
  while (accumulator >= FIXED_STEP) {
    physicsSystem(world, FIXED_STEP)
    accumulator -= FIXED_STEP
  }

  // Render with interpolation for smoothness
  const alpha = accumulator / FIXED_STEP
  renderSystem(world, alpha)

  requestAnimationFrame(fixedGameLoop)
}

Best Practices

Single Responsibility

Each system should do one thing. Split large systems into smaller ones.

Avoid State

Systems should be stateless. Store all state in components or world context.

Use isNested

Inner queries need isNested to prevent iterator invalidation.

Profile First

Don't optimize prematurely. Profile to find actual bottlenecks.
best-practices.ts
// Nested queries need isNested to prevent iterator invalidation
const collisionSystem = (world) => {
  for (const a of query(world, [Position, Collider])) {
    for (const b of query(world, [Position, Collider], isNested)) {
      // ✓ Safe - isNested prevents removal commits during iteration
    }
  }
}

// Keep systems focused
const inputSystem = (world) => { /* Only input */ }
const aiSystem = (world) => { /* Only AI */ }
const movementSystem = (world) => { /* Only movement */ }

// Avoid god systems that do everything
const updateEverything = (world) => {
  // ❌ 500 lines of mixed concerns
}

Next Steps

Continue Learning