0.4

Simple Game

A complete game demonstrating ECS patterns: player movement, enemies, projectiles, and collision detection.

WASD to move | Click to shoot

How It Works

Components

The game uses simple SoA components for position, velocity, health, and more:

components.ts
// Position and movement
const Position = { x: [] as number[], y: [] as number[] }
const Velocity = { vx: [] as number[], vy: [] as number[] }
const Speed = { value: [] as number[] }

// Combat
const Health = { current: [] as number[], max: [] as number[] }
const Damage = { value: [] as number[] }
const Collider = { radius: [] as number[] }

// Tags (empty objects for classification)
const Player = {}
const Enemy = {}
const Projectile = {}
const Active = {}

Systems

Systems are just functions that query and update entities:

systems.ts
// Movement system - applies velocity to position
const movementSystem = (world: World) => {
  const dt = world.time.delta

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

// Player input system
const playerInputSystem = (world: World) => {
  const { input } = world

  for (const eid of query(world, [Player, Position, Velocity, Speed])) {
    const speed = Speed.value[eid]

    // Reset velocity
    Velocity.vx[eid] = 0
    Velocity.vy[eid] = 0

    // Apply input
    if (input.keys.w) Velocity.vy[eid] = -speed
    if (input.keys.s) Velocity.vy[eid] = speed
    if (input.keys.a) Velocity.vx[eid] = -speed
    if (input.keys.d) Velocity.vx[eid] = speed
  }
}

// Collision system - check overlaps
const collisionSystem = (world: World) => {
  const projectiles = query(world, [Projectile, Position, Collider])
  const enemies = query(world, [Enemy, Position, Collider, Health], isNested)

  for (const proj of projectiles) {
    for (const enemy of enemies) {
      if (checkCollision(proj, enemy)) {
        // Deal damage
        Health.current[enemy] -= Damage.value[proj]

        // Remove projectile
        removeEntity(world, proj)

        // Check if enemy died
        if (Health.current[enemy] <= 0) {
          removeEntity(world, enemy)
        }
        break
      }
    }
  }
}

// Render system
const renderSystem = (world: World) => {
  const { ctx, canvas } = world

  // Clear
  ctx.fillStyle = '#1c1c1c'
  ctx.fillRect(0, 0, canvas.width, canvas.height)

  // Draw player
  for (const eid of query(world, [Player, Position])) {
    ctx.fillStyle = '#0f62fe'
    ctx.beginPath()
    ctx.arc(Position.x[eid], Position.y[eid], 16, 0, Math.PI * 2)
    ctx.fill()
  }

  // Draw enemies
  for (const eid of query(world, [Enemy, Position, Health])) {
    const healthPct = Health.current[eid] / Health.max[eid]
    ctx.fillStyle = `hsl(${healthPct * 120}, 70%, 50%)`
    ctx.beginPath()
    ctx.arc(Position.x[eid], Position.y[eid], 12, 0, Math.PI * 2)
    ctx.fill()
  }

  // Draw projectiles
  ctx.fillStyle = '#78a9ff'
  for (const eid of query(world, [Projectile, Position])) {
    ctx.beginPath()
    ctx.arc(Position.x[eid], Position.y[eid], 4, 0, Math.PI * 2)
    ctx.fill()
  }
}

Game Loop

The game loop runs all systems in order:

game-loop.ts
const systems = [
  timeSystem,
  playerInputSystem,
  aiSystem,
  shootingSystem,
  movementSystem,
  boundarySystem,
  collisionSystem,
  cleanupSystem,
  renderSystem,
]

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

  if (!world.paused) {
    requestAnimationFrame(gameLoop)
  }
}

gameLoop()

Entity Creation

Entities are created by adding components:

entities.ts
// Create player
const createPlayer = (world: World, x: number, y: number) => {
  const eid = addEntity(world)

  addComponent(world, eid, Player)
  addComponent(world, eid, Position)
  addComponent(world, eid, Velocity)
  addComponent(world, eid, Speed)
  addComponent(world, eid, Collider)
  addComponent(world, eid, Health)

  Position.x[eid] = x
  Position.y[eid] = y
  Speed.value[eid] = 200
  Collider.radius[eid] = 16
  Health.current[eid] = 100
  Health.max[eid] = 100

  return eid
}

// Create enemy
const createEnemy = (world: World, x: number, y: number) => {
  const eid = addEntity(world)

  addComponent(world, eid, Enemy)
  addComponent(world, eid, Position)
  addComponent(world, eid, Velocity)
  addComponent(world, eid, Collider)
  addComponent(world, eid, Health)

  Position.x[eid] = x
  Position.y[eid] = y
  Collider.radius[eid] = 12
  Health.current[eid] = 30
  Health.max[eid] = 30

  return eid
}

// Create projectile
const createProjectile = (world: World, x: number, y: number, vx: number, vy: number) => {
  const eid = addEntity(world)

  addComponent(world, eid, Projectile)
  addComponent(world, eid, Position)
  addComponent(world, eid, Velocity)
  addComponent(world, eid, Collider)
  addComponent(world, eid, Damage)

  Position.x[eid] = x
  Position.y[eid] = y
  Velocity.vx[eid] = vx
  Velocity.vy[eid] = vy
  Collider.radius[eid] = 4
  Damage.value[eid] = 10

  return eid
}

Full Source

View the complete source code for this example on GitHub.