Messages

Send data to other players. The building block for all multiplayer interaction.

Broadcast

Send to everyone in the room:

// Send any JSON-serializable data
room.broadcast({ x: 100, y: 200 })
room.broadcast({ type: 'chat', text: 'Hello!' })
room.broadcast({ type: 'shoot', angle: 45, power: 0.8 })

Direct Messages

Send to a specific player:

// Send to one player
room.send('player-id-123', { type: 'private', text: 'Hey!' })

// Get player IDs from room.players
const otherPlayer = room.players.find(p => p.id !== room.playerId)
if (otherPlayer) {
  room.send(otherPlayer.id, { type: 'challenge', game: 'chess' })
}

Receiving Messages

room.on('message', (from, data, meta) => {
  // from: player ID who sent the message
  // data: the message data (whatever they sent)
  // meta: { serverTime, tick }
  
  console.log(`Message from ${from}:`, data)
  console.log('Server time:', meta.serverTime)
  console.log('Tick:', meta.tick)
})

Message Metadata

Every message includes metadata from the server:

room.on('message', (from, data, meta) => {
  meta.serverTime  // Unix timestamp (ms) when server received the message
  meta.tick        // Incrementing counter for message ordering
})

serverTime is useful for:

  • Ordering messages that arrive out of order
  • Time-based interpolation
  • Measuring latency

tick is useful for:

  • Ensuring you process messages in order
  • Detecting dropped messages

Message Patterns

Type-Based Messages

Use a type field to handle different message types:

// Sending
room.broadcast({ type: 'move', x: 100, y: 200 })
room.broadcast({ type: 'chat', text: 'Hello' })
room.broadcast({ type: 'attack', targetId: 'player-2', damage: 10 })

// Receiving
room.on('message', (from, data) => {
  switch (data.type) {
    case 'move':
      updatePlayerPosition(from, data.x, data.y)
      break
    case 'chat':
      addChatMessage(from, data.text)
      break
    case 'attack':
      handleAttack(from, data.targetId, data.damage)
      break
  }
})

Position Updates

// Send at fixed rate
setInterval(() => {
  room.broadcast({
    type: 'pos',
    x: player.x,
    y: player.y,
    vx: player.velocityX,
    vy: player.velocityY
  })
}, 50)  // 20Hz

// Receive
const players = {}

room.on('message', (from, data) => {
  if (data.type === 'pos') {
    players[from] = data
  }
})

Request/Response

// Player requests an action
room.broadcast({ type: 'request-move', from: 'a2', to: 'a4' })

// Host validates and responds
room.on('message', (from, data) => {
  if (data.type === 'request-move' && room.isHost) {
    if (isValidMove(data.from, data.to)) {
      applyMove(data.from, data.to)
      room.broadcast({ type: 'move-applied', from: data.from, to: data.to })
    } else {
      room.send(from, { type: 'move-rejected', reason: 'Invalid move' })
    }
  }
})

What to Send

You can send any JSON-serializable data:

// ✅ Good
room.broadcast({ x: 100, y: 200 })
room.broadcast({ items: ['sword', 'shield'] })
room.broadcast({ nested: { data: { works: true } } })
room.broadcast("just a string")
room.broadcast(42)
room.broadcast([1, 2, 3])

// ❌ Won't work
room.broadcast(undefined)
room.broadcast(() => {})  // Functions
room.broadcast(new Map()) // Special objects

Best Practices

  • Keep messages small — send only what changed
  • Use a type field — makes handling easier
  • Batch related data — one message with x,y,z vs three messages
  • Throttle position updates — 20Hz is enough for most games

Next Steps