Skip to content

Web Workers para la lógica del juego

Los Web Workers permiten ejecutar JavaScript en un hilo en segundo plano. En los juegos, esto significa que la física, la IA y la generación procedimental pueden ejecutarse sin provocar caídas de fotogramas.

1) Cuándo usar Workers

Buenos casos de uso:

  • Simulación física
  • Búsqueda de rutas (A*, navmesh)
  • Toma de decisiones de la IA
  • Generación procedimental
  • Procesamiento de recursos (manipulación de imágenes, compresión)
  • Matemáticas complejas (FFT, detección de colisiones)

No son ideales para:

  • Renderizado (los Workers no pueden acceder directamente al DOM ni a Canvas)
  • Tareas muy pequeñas (la sobrecarga del hilo no compensa)

2) Crear un Worker básico

worker.js:

js
self.onmessage = (e) => {
  const { type, data } = e.data
  
  if (type === 'calculate') {
    const result = heavyCalculation(data)
    self.postMessage({ type: 'result', data: result })
  }
}

function heavyCalculation(input) {
  // Trabajo intensivo aquí
  return input * 2
}

main.js:

js
const worker = new Worker('worker.js')

worker.onmessage = (e) => {
  const { type, data } = e.data
  if (type === 'result') {
    console.log('Resultado obtenido:', data)
  }
}

worker.postMessage({ type: 'calculate', data: 42 })

3) Workers en línea (sin archivo separado)

js
function createInlineWorker(fn) {
  const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
  return new Worker(URL.createObjectURL(blob))
}

const worker = createInlineWorker(() => {
  self.onmessage = (e) => {
    const result = e.data * 2
    self.postMessage(result)
  }
})

4) Workers como módulos ES

Puedes escribir el código del Worker como un módulo ES y usar import dentro de él, en lugar de importScripts. Pasa { type: 'module' } al constructor:

js
const worker = new Worker('physics-worker.js', { type: 'module' })

Dentro del Worker puedes usar import para cargar utilidades compartidas de matemáticas, colisiones o IA del mismo modo que en el hilo principal, para no duplicar la lógica del juego. Los Workers como módulos son compatibles con Chrome y Edge 80+, Safari 15+ y Firefox 114+.

5) Ejemplo de Worker de física

physics-worker.js:

js
const bodies = []
const FIXED_DT = 1 / 60

self.onmessage = (e) => {
  const { type, data } = e.data
  
  switch (type) {
    case 'init':
      initWorld(data)
      break
    case 'step':
      step()
      break
    case 'addBody':
      bodies.push(data)
      break
  }
}

function initWorld(config) {
  // Inicializar el mundo físico
}

function step() {
  // Actualizar la física
  for (const body of bodies) {
    body.vy += 9.8 * FIXED_DT // Gravedad
    body.x += body.vx * FIXED_DT
    body.y += body.vy * FIXED_DT
  }
  
  // Devolver las posiciones
  self.postMessage({
    type: 'positions',
    data: bodies.map(b => ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
  })
}

6) Objetos transferibles para mejorar el rendimiento

Los datos de gran tamaño pueden transferirse sin copiarlos:

js
// Hilo principal
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions ya no se puede usar aquí (se ha transferido)

// Worker
self.onmessage = (e) => {
  const positions = e.data
  // Trabajar con las posiciones
  self.postMessage(positions, [positions.buffer])
}

7) Worker para búsqueda de rutas

pathfinding-worker.js:

js
let grid = null

self.onmessage = (e) => {
  const { type, data } = e.data
  
  if (type === 'setGrid') {
    grid = data
  }
  
  if (type === 'findPath') {
    const path = aStar(data.start, data.end, grid)
    self.postMessage({ type: 'path', id: data.id, path })
  }
}

function aStar(start, end, grid) {
  // Implementación de A*
  const openSet = [start]
  const cameFrom = new Map()
  const gScore = new Map()
  gScore.set(key(start), 0)
  
  while (openSet.length > 0) {
    // ... Lógica de A*
  }
  
  return reconstructPath(cameFrom, end)
}

function key(pos) {
  return `${pos.x},${pos.y}`
}

8) Grupo de Workers para tareas en paralelo

js
class WorkerPool {
  constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
    this.workers = []
    this.queue = []
    this.available = []
    
    for (let i = 0; i < size; i++) {
      const worker = new Worker(workerUrl)
      worker.onmessage = (e) => this.handleResult(worker, e)
      this.workers.push(worker)
      this.available.push(worker)
    }
  }
  
  run(data) {
    return new Promise((resolve) => {
      const task = { data, resolve }
      
      if (this.available.length > 0) {
        this.dispatch(this.available.pop(), task)
      } else {
        this.queue.push(task)
      }
    })
  }
  
  dispatch(worker, task) {
    worker._currentTask = task
    worker.postMessage(task.data)
  }
  
  handleResult(worker, e) {
    const task = worker._currentTask
    task.resolve(e.data)
    
    if (this.queue.length > 0) {
      this.dispatch(worker, this.queue.shift())
    } else {
      this.available.push(worker)
    }
  }
  
  terminate() {
    this.workers.forEach(w => w.terminate())
  }
}

9) SharedArrayBuffer para sincronización en tiempo real

Con el aislamiento entre orígenes activado, puedes compartir memoria:

js
// Hilo principal
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)

worker.postMessage({ type: 'init', buffer: shared })

// El Worker lee y escribe directamente en la memoria compartida
// Sin sobrecarga de postMessage para actualizar posiciones

10) OffscreenCanvas para Workers

Renderiza en un Worker. OffscreenCanvas ya forma parte de Baseline y está ampliamente disponible en Chrome, Edge, Firefox y Safari (Safari añadió compatibilidad en la versión 17.0 para macOS e iOS):

js
// Hilo principal
const canvas = document.getElementById('game')
const offscreen = canvas.transferControlToOffscreen()
worker.postMessage({ canvas: offscreen }, [offscreen])

// Worker
self.onmessage = (e) => {
  const canvas = e.data.canvas
  const ctx = canvas.getContext('2d')
  
  function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    // Dibujar...
    requestAnimationFrame(render)
  }
  render()
}

11) Gestión de errores

js
worker.onerror = (e) => {
  console.error('Error del Worker:', e.message, e.filename, e.lineno)
}

// En el Worker
self.onerror = (e) => {
  self.postMessage({ type: 'error', message: e.message })
}

Contenido relacionado

Recursos externos