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) // 20HzEvents: 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.xNot 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
- Game Patterns — Examples for different game types
- Error Handling — Handle edge cases