Multiplayer: rooms, lobbies & .io-style games
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Multiplayer: rooms, lobbies & .io-style games
Opt in by publishing with realtime: true. The relay then accepts connections from your game's sandbox and fans small payloads between players in a room.
Rooms are lobbies
A room is just a name you choose — there's nothing to provision. Rooms are created on first join, scoped to your game (two games can both use "lobby-1" without colliding), and destroyed when the last player leaves.
const room = GameSDK.connectRoom({ room: "lobby-1", name: "Ana" });
room.on("joined", ({ peerId, peers, snapshots }) => {
// `snapshots` is every peer's latest state — draw the world immediately.
});
room.on("peerJoined", ({ peerId, name }) => {});
room.on("peerLeft", ({ peerId }) => {});
room.on("state", ({ peerId, data }) => {}); // another player's setState
room.on("message", ({ peerId, data }) => {}); // another player's send
room.setState({ x, y, mass }); // your latest state (kept for late joiners)
room.send({ type: "shoot" }); // a one-off event (not retained)
room.close();Lobby browser + matchmaking
GameSDK.listRooms() returns your game's active rooms with live player counts (busiest first) — enough to draw a lobby list or auto-place a player:
const { rooms, maxPeersPerRoom } = await GameSDK.listRooms();
// rooms: [{ room: "lobby-1", peers: 61 }, { room: "lobby-2", peers: 12 }]
// Matchmaking: fill the busiest room that still has space, else open a new one.
const open = rooms.find((r) => r.peers < maxPeersPerRoom);
const target = open ? open.room : `lobby-${rooms.length + 1}`;
const room = GameSDK.connectRoom({ room: target });
// Always handle a full/limited room — another player may take the last slot
// between your list and your join.
room.on("error", ({ code }) => {
if (code === "room_full" || code === "game_room_limit") retryWithAnotherRoom();
});⚠️ The relay is NOT authoritative — design for it
The server fans out messages; it never runs your game logic. Every client broadcasts its own state and the others believe it. That's fine for co-op, racing, drawing, or social games — but for anything competitive (agar.io-style eating, PvP damage, leaderboard-relevant scoring) a modified client can simply claim any position, mass, or score.
For competitive games use a host-authoritative model:
Pick a host deterministically — e.g. the lowest
peerIdin the room.Everyone else sends inputs (
room.send({ up: true })), not state.The host runs the simulation and broadcasts the world with
setState().Handle host migration: on
peerLeft, recompute the host; the new host seeds from its last known snapshot.
This also scales far better. 64 players each broadcasting 30×/sec is ~120k fan-out sends per second per room; one host broadcasting the world at 20 Hz is a tiny fraction of that. Send deltas, not the whole world, and keep state under the payload cap.
Limits (per game, enforced server-side)
Limit | Value |
|---|---|
Players per room | 64 |
Rooms per game | 200 |
Message size | 8 KB |
Messages per second, per player | 30 |
Retained snapshot per player | 8 KB |
Concurrent connections per IP | 20 |
Exceeding them returns an error event (room_full, game_room_limit, rate_limited, too_large) rather than dropping you silently — handle those events and degrade gracefully.