Game SDK
Multiplayer
Rooms let 2–16 Players share a Game session. The MiraDock host relays JSON messages between them; your Game code owns the rules.
How rooms work
Your Game still cannot open network connections. The play page owns the Realtime connection, presence, message storage, and sender identity, and passes events to your Game through window.MiraDock. Messages are stored before they are broadcast, and the host checks for missed messages every 5 seconds, so every member sees the same ordered stream.
Signed-in Players can create rooms. Guests can join with a code and appear as Guest Player with an id that changes on every page load. When the host Player leaves, the earliest remaining member becomes host, including guests. A room closes as soon as it is empty.
Create a room
Register handlers right after the room resolves. The host buffers initial membership and catch-up messages until your handler is registered.
const ctx = await MiraDock.ready();
if (MiraDock.capabilities.multiplayer) {
const room = await MiraDock.multiplayer.create({ maxPlayers: 2 });
if (room.status === "signin_required") {
// Guests cannot create rooms. The host has already offered sign-in.
} else {
showCode(room.joinCode); // Six characters, e.g. "K7P2QX"
await room.on("players", (players) => renderLobby(players));
await room.on("message", ({ from, seq, payload }) => apply(from, payload));
await room.on("closed", ({ reason }) => backToMenu(reason));
await room.send({ type: "hello" });
}
}Join by code and invite links
The play page shows a lobby outside your Game with the code, an invite link, the current Players with a host marker, capacity, and a Leave button. Opening /play/your-game?room=CODE joins that room once after MiraDock.ready(). If the invite is invalid, full, or closed, the host shows a message and your Game continues in its single-Player state.
// A friend types the code shown by the room creator.
const room = await MiraDock.multiplayer.join(codeFromInput);
// Invite links (/play/your-game?room=CODE) are joined by the host after ready().
await MiraDock.on("joined", async ({ joinCode }) => {
const invited = await MiraDock.multiplayer.join(joinCode); // no second server join
await invited.on("players", renderLobby);
await invited.on("message", ({ from, payload }) => apply(from, payload));
});Room API
| Call | Behavior |
|---|---|
MiraDock.multiplayer.create({ maxPlayers }) | Signed-in Players only. maxPlayers is an integer from 2 to 16. Resolves a room, or { status: "signin_required" } for guests. |
MiraDock.multiplayer.join(joinCode) | Guests and signed-in Players. Codes are six characters, trimmed and case-insensitive. Rejects with full, closed, wrong_game, not_found, already_joined, or invalid_argument. |
room.roomId, room.joinCode | Identify the room. Show joinCode to friends; the play page lobby also offers an invite link. |
room.send(payload) | Sends any JSON value to every member, including you. Resolves once the message is stored. |
room.on(event, handler) | Events: message, players, joined, closed. Other names reject with invalid_argument. |
room.leave() | Leaves the room. Resolves immediately if the room is already closed. |
Room events
| Event | Payload | Notes |
|---|---|---|
message | { from, seq, payload } | from is the sender's per-Game Player id; seq is a gapless, increasing room sequence. Delivered in order and deduplicated. |
players | [{ id, displayName, isHost, online }] | The current members. online is advisory presence, not proof of identity. |
joined | { roomId, joinCode, players } | The host joined an invite room for this Player after ready(). Also available as MiraDock.on("joined"). |
closed | { reason } | left, closed, not_member, not_found, signin_required, or error. The room object cannot send afterwards. |
Realtime multiplayer
The room host's browser simulates the game. It opens a WebRTC pair to each member; members connect only to the host. The play page owns all connections and signaling, so the Game iframe keeps its network sandbox. A host can cheat; paid rewards must never depend on realtime outcomes. Check MiraDock.capabilities.multiplayer.realtime.
const rt = await room.realtime();
rt.on("data", ({ from, payload, fast }) => applyPeerData(from, payload));
rt.on("peers", peers => showQuality(peers));
rt.on("host", ({ hostId, reason }) => restoreLatestSnapshot(hostId));
// A peer sends input to the host. The host sends snapshots to all peers.
await rt.send({ type: "input", dx: 1 }, { to: rt.hostId, fast: true });
console.log(rt.stats()); // peer RTT, transport, relay availabilityEach pair has ordered reliable and unordered fast channels. Send JSON or ArrayBuffer payloads up to 16 KiB. Each peer may send 60 fast or 20 reliable packets per second; extra sends reject with rate_limited. Non-hosts may send only to the host. At join and right after a host migration, a send to a player whose connection is not up yet does not reject: a fast send is skipped and its id listed in the resolved dropped array; a reliable send waits up to 20 seconds, then delivers or rejects with timeout. The host that left counts as such a player until your host handler switches targets. rt.on("data") receives sender-stamped data; peers and host events report connection quality and host migration. The game must carry its own state to a new host. Signaling stays private and ephemeral. Public STUN is the default; optional TURN credentials are minted server-side. If a pair cannot connect within 10 seconds, it falls back to the room relay and reports transport: "relay". Connections start when a Player joins, so call room.realtime() on every client right after joining; send must-arrive events on the reliable channel and treat relay data the same as WebRTC data.
Simulate on the host at 30 Hz, send snapshots at 20 Hz on fast, send peer inputs on fast, and use reliable for round events. Interpolate snapshots on peers. See Arena Dash for a 2–6 Player reference.
Limits
| Limit | Value |
|---|---|
| Players per room | 2 to 16, chosen at creation |
| Rooms per play session | One. Creating or joining another room leaves the current one. |
| Message size | 4 KiB of UTF-8 JSON per payload (limit_exceeded) |
| Send rate | 20 sends per rolling second per play session (rate_limited, never dropped silently) |
| Message history | The last 200 messages. Late joiners replay that suffix. |
| Idle expiry | Rooms idle for 30 minutes close at the next cleanup run, every 15 minutes |
| Call timeout | 20 seconds for create and join; 10 seconds for other room calls (timeout) |
Sends and membership changes count as room activity; presence and reading do not. When a room expires, its messages are deleted. If a Player needs history older than the last 200 messages, ask a peer for the current state.
Reference Game
Two-player Tap Duel shows room creation, joining by code, guest guidance, solo practice, and invite auto-join. The full contract, including error codes, is in the Game SDK specification.