Skip to content

Web Workers untuk logika game

Web Workers memungkinkan Anda menjalankan JavaScript di thread latar belakang. Untuk game, ini berarti fisika, AI, dan generasi prosedural dapat berjalan tanpa menyebabkan penurunan frame rate.

1) Kapan harus menggunakan Workers

Cocok digunakan untuk:

  • Simulasi fisika
  • Pencarian jalur (A*, navmesh)
  • Pengambilan keputusan AI
  • Generasi prosedural
  • Pemrosesan aset (manipulasi gambar, kompresi)
  • Matematika kompleks (FFT, deteksi tabrakan)

Kurang ideal untuk:

  • Rendering (Workers tidak dapat mengakses DOM/Canvas secara langsung)
  • Tugas yang sangat kecil (overhead thread tidak sepadan)

2) Membuat Worker dasar

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) {
  // Expensive work here
  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('Got result:', data)
  }
}

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

3) Worker inline (tanpa berkas terpisah)

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) Worker modul ES

Anda dapat menulis kode worker sebagai modul ES dan menggunakan import di dalamnya sebagai pengganti importScripts. Teruskan { type: 'module' } ke konstruktor:

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

Di dalam worker, Anda kemudian dapat menggunakan import untuk helper matematika, tabrakan, atau AI bersama dengan cara yang sama seperti di thread utama, sehingga logika game tidak perlu diduplikasi. Worker modul didukung di Chrome dan Edge 80+, Safari 15+, serta Firefox 114+.

5) Contoh Worker fisika

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) {
  // Initialize physics world
}

function step() {
  // Update physics
  for (const body of bodies) {
    body.vy += 9.8 * FIXED_DT // Gravity
    body.x += body.vx * FIXED_DT
    body.y += body.vy * FIXED_DT
  }
  
  // Send positions back
  self.postMessage({
    type: 'positions',
    data: bodies.map(b => ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
  })
}

6) Objek transferable untuk performa

Data berukuran besar dapat ditransfer tanpa disalin:

js
// Main thread
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions is now unusable here (transferred)

// Worker
self.onmessage = (e) => {
  const positions = e.data
  // Work with positions
  self.postMessage(positions, [positions.buffer])
}

7) Worker pencarian jalur

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) {
  // A* implementation
  const openSet = [start]
  const cameFrom = new Map()
  const gScore = new Map()
  gScore.set(key(start), 0)
  
  while (openSet.length > 0) {
    // ... A* logic
  }
  
  return reconstructPath(cameFrom, end)
}

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

8) Pool Worker untuk tugas paralel

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 untuk sinkronisasi real-time

Dengan isolasi lintas origin yang diaktifkan, Anda dapat berbagi memori:

js
// Main thread
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)

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

// Worker reads/writes directly to shared memory
// No postMessage overhead for position updates

10) OffscreenCanvas untuk Workers

Lakukan rendering di dalam Worker. OffscreenCanvas kini tersedia secara luas sebagai Baseline di Chrome, Edge, Firefox, dan Safari (Safari menambahkan dukungan pada versi 17.0 di macOS dan iOS):

js
// Main thread
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)
    // Draw...
    requestAnimationFrame(render)
  }
  render()
}

11) Penanganan error

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

// In worker
self.onerror = (e) => {
  self.postMessage({ type: 'error', message: e.message })
}

Terkait

Sumber Daya Eksternal