API Reference
Complete reference for the EasyMultiplayer class.
Constructor
new EasyMultiplayer(options)
| Option | Type | Required | Description |
|---|---|---|---|
room | string | Yes | Room name. Players with the same room join together. |
state | object | No | Initial game state. Must be JSON-serializable. Defaults to {}. |
tickRate | number | No | Ticks per second. Default: 20. |
inputDelay | number | No | Input delay in frames. Higher = fewer rollbacks but more latency. Default: 4. |
canvas | HTMLCanvasElement | No | Canvas element passed to the draw callback. |
debug | boolean | No | Enable debug mode (detailed logging, incompatibility reports). Default: false. |
Methods
defineInput(name, sampler)
Register a named input channel. The sampler function is called every tick to read the local player's input.
game.defineInput('left', () => keys.ArrowLeft || false);
game.defineInput('right', () => keys.ArrowRight || false);
game.defineInput('action', () => keys.Space || false);
Return values must be JSON-serializable. Each input becomes a field in the per-frame input data sent to other peers.
on(event, callback)
Register a callback for a game event. Events:
'tick'
Runs at the fixed tick rate. This is your deterministic game logic.
game.on('tick', (ctx) => { ... });
| ctx field | Type | Description |
|---|---|---|
state | object | Your mutable game state. Changes are automatically captured. |
query(playerId, predicate) | function → boolean | Ask a question about a player's input. The predicate receives the full input object and returns a boolean. Records the query for smart rollback avoidance. |
getInput(playerId) | function → any | Get raw input data for a player (all defineInput values). Prefer query() over this. |
random() | function → number | Deterministic random number [0, 1]. Synced across peers and rollbacks. |
players | string[] | Array of active player IDs for the current frame. |
frame | number | Current frame number. |
deltaTime | number | Milliseconds per tick (1000 / tickRate). |
time | number | Current tick time in milliseconds. |
emit(type, data) | function | Emit a transient event (sound, explosion, etc.). Rollback-aware: events are confirmed or cancelled during resimulation. |
hints.trail(id, data, t) | function | Record a presentation hint — a position/data point for smooth interpolation. Not part of state. |
'draw'
Runs every animation frame (~60fps). Render your game here.
game.on('draw', (ctx) => { ... });
| ctx field | Type | Description |
|---|---|---|
state | object | Current game state (treat as read-only during draw). |
myId | string | Local player's ID. |
canvas | HTMLCanvasElement | The canvas from the constructor options. |
frame | number | Current frame number. |
t | number | Current draw time (for interpolation between ticks). |
events | object | Access to emitted events. events.active(type), events.new(type), events.cancelled(type), events.forEach(type, {onNew, onCancelled}). |
hints.sample(id, t) | function | Get interpolated presentation hint data for an entity at time t. |
hints.clearTrail(id) | function | Remove all hint data for an entity (e.g., when destroyed). |
'playerJoined'
Called when a player joins. Initialize their state here.
game.on('playerJoined', ({ playerId, state, random }) => {
state.players[playerId] = { x: 0, y: 0 };
});
random() for randomness. No Math.random(), no Date.now(), no DOM manipulation.
'playerLeft'
Called when a player disconnects.
game.on('playerLeft', ({ playerId, state }) => {
delete state.players[playerId];
});
manageState(exportFn, importFn)
For complex games that manage their own state objects instead of using the plain state object. The library calls these instead of its default JSON serialization.
game.manageState(
() => myGame.ExportState(), // called every tick to snapshot
(s) => myGame.ImportState(s) // called to restore during rollback
);
See Examples: Migrating an existing game for a full walkthrough.
onEvent(type, callback)
Subscribe to events of a specific type. The callback fires when the event is emitted (but not when it's re-confirmed during rollback).
game.onEvent('explosion', (data, frame) => startExplosionEffect(data));
onEventCancelled(type, callback)
Subscribe to event cancellations. Fires when a rollback undoes an event that was previously emitted.
game.onEventCancelled('explosion', (data, frame) => stopExplosionEffect(data));
start()
Connect to the room and start the game loop. Creates the P2P connections, syncs clocks, and begins ticking/drawing.
stop()
Stop the game loop. Does not disconnect from the room.
destroy()
Stop the game loop and clean up all resources.
Properties
| Property | Type | Description |
|---|---|---|
myId | string | The local player's unique peer ID. Available after start(). |
playerCount | number | Number of currently active players. |
frame | number | Current frame number. |
scene | SyncedScene | The underlying SyncedScene instance. For advanced use only. |
The Query System
query(playerId, predicate) is the recommended way to read input. The predicate receives the full input object (all defineInput values) and returns a boolean. The query result is recorded. When corrected input arrives, the predicate is re-evaluated. Rollback only occurs if a result changes.
Why use query() instead of getInput()?
With getInput(), any change to raw input triggers a rollback. With query(), only meaningful changes trigger rollback.
// BAD: raw input comparison. Any change = rollback.
var input = getInput(pid);
if (input.left) player.x--;
// GOOD: predicate over full input. Rollback only if this answer changes.
if (query(pid, inputs => !!inputs.left)) player.x--;
Compound Predicates
Since the predicate receives all inputs at once, you can express compound intent in a single query:
// ONE query with compound logic: "wants left AND NOT also right"
var wantsLeft = query(pid, inputs => !!inputs.left && !inputs.right);
// If prediction was "no buttons" and actual is "both buttons",
// wantsLeft is still false — no rollback needed!
// This is ONE recorded query with ONE boolean result.
query(pid, i => i.left) + query(pid, i => !i.right)) would record two results, either of which could trigger rollback. One compound query records one result — only the final boolean matters.
Context-Sensitive Queries
Only query when the answer matters:
// At a junction with only [up, right] available:
// Don't query 'left' or 'down' — walls block them anyway.
// Don't query 'right' if already going right — it's the default.
// Only query 'up' — the one direction that could change the outcome.
for (var dir of availableDirections) {
if (dir === currentDirection) continue; // default, no query needed
var opp = opposite(dir);
if (query(pid, inputs => !!inputs[dir] && !inputs[opp])) {
chosenDirection = dir;
break;
}
}