Multiplayer API (BETA)

Server-authoritative real-time multiplayer rooms. Game logic runs on the server in a GameRoom class; clients connect via ServerRoom to exchange messages.


Overview

The multiplayer system has two parts:

  • Server — You write a GameRoom subclass that holds all game state and validates every action. The server is the single source of truth.

  • Client — Players connect through ServerRoom and send typed messages.

// Client — join by matchmaking or room code
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'
const room = await RundotGameAPI.realtime.joinOrCreateRoom<MyProtocol>('tictactoe')
const room = await RundotGameAPI.realtime.joinRoomByCode<MyProtocol>('HX9KWR')

// Server
import { GameRoom } from '@series-inc/rundot-game-sdk/mp-server'
export default class TicTacToe extends GameRoom<MyProtocol> { ... }

Setup

Vite plugin

Add rundotMultiplayerPlugin to your vite.config.ts. This builds your server room code, copies rooms.config.json to dist/, and starts a local dev server for testing:

Option
Type
Default
Description

configPath

string

'rooms.config.json'

Path to your rooms config file

devPort

number

3001

Port for the local dev server

threaded

boolean

false

Run dev rooms in Worker Threads

maxBundleSize

number

5242880

Max server bundle size in bytes (5 MB)

rooms.config.json

Room types are defined in a standalone rooms.config.json file at your project root (not the shared config.json):

The file is uploaded with your game when you rundot deploy. The server reads it to register your room types.

Room type fields

Field
Type
Default
Description

type

string

required

Room type identifier used for matchmaking (e.g. "tictactoe", "lobby")

file

string

required

Path to the file exporting the GameRoom subclass, relative to project root

export

string

"default"

Named export of the GameRoom class

singleton

boolean

false

When true, only one room of this type exists. All players join the same room (no matchmaking).

config

RoomConfig

Room configuration overrides (see table below)

RoomConfig fields

Field
Type
Default
Description

maxPlayers

number

10

Maximum number of players allowed in the room

idleTimeout

number

300

Time in seconds before an empty room is disposed (5 min)

autoPersist

boolean

true

Whether to auto-persist state on a debounced interval

persistInterval

number

5000

Debounce interval for auto-persist in milliseconds

allowReconnect

boolean

true

Whether to allow reconnections after disconnect

reconnectTimeout

number

30

Time in seconds to hold a player slot for reconnection

startLocked

boolean

false

Whether the room starts locked (no new joins)

metadata

object

Custom metadata passed to the room on creation

Multi-room-type apps: Add one entry per game mode:


Quick Start

Server: TicTacToe room

Client: connect and play


Server: GameRoom

Defining messages

GameRoom takes one type parameter:

  • P — A discriminated union of message types (the protocol). Every member must have a { type: string } field. This union covers both client-to-server and server-to-client messages.

Lifecycle hooks

All hooks are optional. They can be async or synchronous.

Hook
When it's called

onCreate()

Room is first created. Initialize game data here.

onPlayerJoin(player)

A player requests to join. Call this.reject() to deny.

onGameMessage(message)

A player sends a typed message. message.sender is the player, message.payload is the typed message. Switch on message.payload.type to narrow.

onPlayerLeave(player, reason)

A player leaves. reason is 'leave', 'disconnect', or 'kick'.

onDispose()

Room is about to be destroyed. Final cleanup.

onRestore(snapshot)

Room is restored from a persisted snapshot (crash recovery). State keys are auto-applied before this hook; use it to restore server-only data.

onMigrate(snapshot, oldVersion)

Room is restored but the bundle version changed. Migrate state between versions here.

Messaging

Send typed messages to clients (these arrive via onMessage / onPrivateMessage on the client):

Room control

Persistence

Override getPersistState() to control what gets persisted for crash recovery:

Call this.save() to immediately persist. With autoPersist: true (the default), state is also auto-saved on a debounced interval (persistInterval, default 5000ms).

On crash recovery, onRestore(snapshot) is called with the persisted data. Use it to restore your fields:

Clock

Named timers with auto-cleanup and crash-recovery support:

Timers are automatically serialized and restored on crash recovery. In onRestore, re-register your timers with the same names — the harness adjusts their remaining time automatically so they resume where they left off rather than restarting from zero:

All timers are cleared automatically when the room is disposed.

Logger

Structured logging available on this.log:

Player object

The Player object is passed to lifecycle hooks and available via this.players (a ReadonlyMap<string, Player>):

Property
Type
Description

id

string (readonly)

Unique player identifier (profileId from RUN.game)

username

string (readonly)

Display name

avatarUrl

string | null (readonly)

Avatar URL, if available

joinedAt

number (readonly)

Timestamp when the player joined (ms since epoch)

connected

boolean

Whether the player is currently connected (updates on disconnect/reconnect)


Client: Connecting and Playing

Creating and joining rooms

All methods return a ServerRoom<P> typed with your message union:

Room properties

Property
Type
Description

roomCode

string

Shareable 6-character room code (e.g. "HX9KWR")

playerId

string

The current player's ID

locked

boolean

Whether the room is locked (no new joins)

latency

number

Current latency in ms (round-trip / 2)

connectionState

ConnectionState

'connecting', 'connected', 'reconnecting', or 'disconnected'

Events

Register event handlers with room.on():

All callbacks are optional — only register the ones you need.

Sending messages

Send typed messages to the server room (arrives in onGameMessage on the server):

Leaving

This closes the connection and triggers onPlayerLeave on the server with reason 'leave'.

Server time

Get the estimated server time (local time adjusted by server offset):

Useful for synchronized countdowns or time-based game logic.


Reconnection

Players automatically reconnect with exponential backoff when the connection drops.

Server-side

  • allowReconnect (default true) — enables reconnection. When a player disconnects, their slot is held for reconnectTimeout seconds (default 30).

  • While disconnected, player.connected is false. The player is still in this.players — they are only removed when the reconnect timeout expires (triggering onPlayerLeave with reason 'disconnect').

Client-side

The client fires connection events as the state changes:

Event
When

onReconnecting

Connection dropped, attempting to reconnect

onReconnected

Successfully reconnected — connection resumes

onDisconnect

Reconnection failed or timed out — connection is closed

Monitor the connection state at any time via room.connectionState:


Best Practices

  • Use messages for all client updates — broadcast game state changes via typed messages. Use onPlayerJoin return values (joinData) to send initial state to new players.

  • Use typed messages — define a discriminated union for P and switch on payload.type in onGameMessage. This gives you full type safety and autocompletion.

  • Handle disconnects gracefully — check player.connected before time-sensitive logic. Skip disconnected players' turns rather than stalling the game.

  • Lock when full — call this.lock() in onPlayerJoin when you have enough players to prevent extra joins during gameplay.

  • Persist strategically — use this.save() after critical state changes (game start, round end). Rely on autoPersist for routine saves.

  • Use the clock for timing — prefer this.clock.setInterval() / this.clock.setTimeout() over raw setInterval / setTimeout for automatic cleanup and crash-recovery support.

Last updated