Multiplayer

Connect to a room. Send messages. Receive messages. That's the whole API.

Quick Start

import { connect } from '@watchtower-sdk/core'

// Connect to a room
const room = await connect('my-room')

// Send to everyone
room.broadcast({ x: 100, y: 200 })

// Receive messages
room.on('message', (from, data, meta) => {
  console.log(`Player ${from}:`, data)
})

// Room info
room.players   // Who's in the room
room.isHost    // Am I the host?
room.code      // Share this to invite others

How It Works

Watchtower gives you the infrastructure:

  • Connection — WebSocket to room, auto-reconnect
  • Rooms — Create/join with codes
  • Messaging — Broadcast to all, or send to one
  • Player tracking — Who's in, join/leave events
  • Timestamps — Server time + tick on every message

You build the game logic: how to sync state, how to smooth movement, how to handle game rules. The server is a message relay — it doesn't know what a "player position" is. It just routes your messages.

Rooms

// Create a new room (auto-generated code)
const room = await connect()
console.log('Share this:', room.code)  // e.g., "X7K2M9"

// Join a specific room
const room = await connect('X7K2M9')

// Leave the room
room.leave()

Messaging

Broadcast (to everyone)

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

Direct (to one player)

// Send to a specific player
room.send('player123', { type: 'private', text: 'Hey!' })

Receive

room.on('message', (from, data, meta) => {
  // from: who sent it
  // data: what they sent
  // meta.serverTime: when server received it
  // meta.tick: message sequence number
  
  if (data.type === 'move') {
    updatePlayer(from, data.x, data.y)
  }
  if (data.type === 'chat') {
    showChat(from, data.text)
  }
})

Player Tracking

// Current players
room.players     // [{ id, name, meta, joinedAt }, ...]
room.playerCount // Number of players
room.playerId    // Your ID
room.hostId      // Host's ID
room.isHost      // Are you the host?

// Events
room.on('join', (player) => {
  console.log(`${player.name || player.id} joined`)
})

room.on('leave', (player) => {
  console.log(`${player.id} left`)
})

Patterns

Cursor Party

const cursors = {}

// Send my position
document.onmousemove = (e) => {
  room.broadcast({ x: e.clientX, y: e.clientY })
}

// Receive others
room.on('message', (from, data) => {
  cursors[from] = data
})

room.on('leave', (player) => {
  delete cursors[player.id]
})

Turn-Based (Chess, Cards)

let gameState = { board: initialBoard, turn: null }

// Host controls game state
if (room.isHost) {
  gameState.turn = room.playerId
  room.broadcast({ type: 'state', ...gameState })
}

room.on('message', (from, data) => {
  if (data.type === 'state') {
    gameState = data
    render()
  }
  
  // Host validates and applies moves
  if (data.type === 'move' && room.isHost) {
    if (isValidMove(gameState, data)) {
      gameState = applyMove(gameState, data)
      gameState.turn = getNextPlayer()
      room.broadcast({ type: 'state', ...gameState })
    }
  }
})

function makeMove(from, to) {
  room.broadcast({ type: 'move', from, to })
}

Action Game (Shooter, Platformer)

const players = {}
const LERP = 0.15

// Send position updates
setInterval(() => {
  room.broadcast({ 
    type: 'pos', 
    x: myPlayer.x, 
    y: myPlayer.y,
    anim: myPlayer.animation
  })
}, 50)  // 20Hz

// Receive and store
room.on('message', (from, data, meta) => {
  if (data.type === 'pos') {
    if (!players[from]) players[from] = { x: data.x, y: data.y }
    players[from].targetX = data.x
    players[from].targetY = data.y
    players[from].anim = data.anim
  }
  if (data.type === 'shoot') {
    spawnBullet(from, data.angle)
  }
})

// Smooth interpolation in render loop
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()

Smoothing Movement

The SDK doesn't smooth movement automatically — you decide how. The simplest approach is lerping:

const LERP = 0.15  // 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)
}

Every message includes meta.serverTime and meta.tickif you need more sophisticated interpolation or ordering.

What Games Can I Build?

  • Cursor parties — broadcast positions
  • .io games — positions + simple actions
  • Turn-based — host-authoritative game state
  • Card games — use direct messages for private hands
  • Shooters — events for shots, positions for movement
  • Co-op — shared objectives, broadcast progress
  • Chat rooms — just messages
💡 Scaling

Rooms support ~50 players. For larger worlds, use zone sharding: split your world into regions, each as a separate room.

Next Steps