Best Practices

Tips for building robust multiplayer games.

Message Design

Use a Type Field

// ✅ Good: typed messages
room.broadcast({ type: 'move', x: 100, y: 200 })
room.broadcast({ type: 'chat', text: 'Hello' })
room.broadcast({ type: 'shoot', angle: 45 })

// ❌ Avoid: guessing message contents
room.broadcast({ x: 100, y: 200 })  // Is this a move? A click?

Keep Messages Small

// ✅ Good: only what's needed
room.broadcast({ type: 'pos', x: 100, y: 200 })

// ❌ Avoid: sending everything
room.broadcast({
  type: 'pos',
  x: 100,
  y: 200,
  inventory: [...],  // Unchanged, don't send
  achievements: [...] // Unchanged, don't send
})

Batch Related Data

// ✅ Good: one message
room.broadcast({ type: 'state', x: 100, y: 200, health: 80 })

// ❌ Avoid: three messages
room.broadcast({ x: 100 })
room.broadcast({ y: 200 })
room.broadcast({ health: 80 })

Update Rates

Position Updates: 20Hz

// Good balance of smoothness vs bandwidth
setInterval(() => {
  room.broadcast({ type: 'pos', x, y })
}, 50)  // 20Hz

Events: On Demand

// Only send when action happens
function shoot() {
  room.broadcast({ type: 'shoot', angle })
}

function chat(text) {
  room.broadcast({ type: 'chat', text })
}

Smoothing Movement

For action games, smooth other players' positions:

const LERP = 0.15  // Adjust: 0.1 smooth, 0.3 snappy

function update() {
  for (const p of Object.values(players)) {
    p.x += (p.targetX - p.x) * LERP
    p.y += (p.targetY - p.y) * LERP
  }
  requestAnimationFrame(update)
}
update()

Host Authority

For games that need authoritative state, use the host pattern:

// Host broadcasts state
if (room.isHost) {
  room.broadcast({ type: 'state', ...gameState })
}

// Players send requests
room.broadcast({ type: 'action', action: 'move', data: {...} })

// Host validates and applies
room.on('message', (from, data) => {
  if (room.isHost && data.type === 'action') {
    if (isValid(data)) {
      applyAction(data)
      room.broadcast({ type: 'state', ...gameState })
    }
  }
})

Error Handling

room.on('error', (error) => {
  console.error('Room error:', error)
  // Show user-friendly message
  showNotification('Connection issue, reconnecting...')
})

room.on('disconnected', () => {
  showNotification('Reconnecting...')
})

room.on('connected', () => {
  hideNotification()
})

Clean Up

room.on('leave', (player) => {
  // Remove player from your game state
  delete players[player.id]
  
  // Remove their visual representation
  removePlayerSprite(player.id)
})

Testing

  • Multiple tabs — Open your game in 2+ browser tabs
  • Network throttling — Use DevTools to simulate slow connections
  • Disconnect testing — Turn off wifi, verify reconnect works
  • Late join — Join an in-progress game, verify state syncs

Common Mistakes

Not Handling Missing Players

// ❌ Crash if player doesn't exist
players[from].x = data.x

// ✅ Create if missing
if (!players[from]) {
  players[from] = { x: 0, y: 0 }
}
players[from].x = data.x

Not Cleaning Up on Leave

// ❌ Ghost players remain
room.on('message', (from, data) => {
  players[from] = data
})

// ✅ Clean up on leave
room.on('leave', (player) => {
  delete players[player.id]
})

Sending Too Frequently

// ❌ Every mouse move (could be 100Hz+)
document.onmousemove = (e) => {
  room.broadcast({ x: e.clientX, y: e.clientY })
}

// ✅ Throttled
let lastSend = 0
document.onmousemove = (e) => {
  if (Date.now() - lastSend < 50) return
  lastSend = Date.now()
  room.broadcast({ x: e.clientX, y: e.clientY })
}

Next Steps