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 objectsBest 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
- Game Patterns — See how to structure messages for different game types
- SDK Reference — Full API documentation