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.