Easy Multiplayer v0.4 Consumer API
This is the game developer's reference for the public v0.4 authoring surface. Internal engine, ledger, bus, and worker classes are not compatibility APIs.
Start with the quickstart.
Import
import { EasyMultiplayer } from
'https://libs.letsinspire.com/easy_multiplayer/releases/0.4/EasyMultiplayer.js';
Primary namespace:
EasyMultiplayer.performanceMode
defineGame(definition)
Creates and freezes a game definition.
const game = EasyMultiplayer.performanceMode.defineGame({
initialState: () => ({}),
queries: {},
step(ctx) {},
});
Fields:
| Field | Required | Meaning |
|---|---|---|
initialState |
yes | Factory or initial canonical state. Prefer a factory. |
step(ctx) |
yes | One deterministic simulation tick. |
queries |
no | Named pure query predicates. |
schema |
no | Opts into schema-generated state. Omit for default proxied state. |
simpleMode |
no | Enables rawInputs(). Defaults to false. |
autoDiscoverParticipants |
no | Worker-game admission convenience; product games normally perform explicit discovery. |
Game delivery forms:
game: inlineDefinition
Inline games are time-sliced only.
game: {
module: new URL('./game.js', import.meta.url),
export: 'createGame',
options: {},
}
Module games work in worker and time-sliced modes. The factory options must be structured-cloneable and identical on every peer.
createSession(options)
const session = EasyMultiplayer.performanceMode.createSession({
room: { id: 'room-123', appId: 'my-game' },
game: { module: gameUrl, export: 'createGame' },
getLocalInputs,
tickMs: 50,
maxParticipants: 4,
dev: true,
});
Stable options
| Option | Meaning |
|---|---|
room: {id, appId} |
Built-in peer-to-peer room. |
transport |
Custom transport. Mutually exclusive with room. |
game |
Inline game or module reference. |
getLocalInputs() |
Samples the local intent envelope. |
runtimeMode |
'worker-multi-bus' or 'time-sliced'; worker is the default. |
buses |
Total simulation bus count, including render bus. |
tickMs |
Fixed simulation tick duration. Default 50. |
maxParticipants |
Admission ceiling. Spectators may still connect. |
time |
Optional live {offsetMs()} provider. |
dev |
Enables strict deterministic guards and diagnostics. |
advanced |
Validated expert configuration; not a long-term compatibility surface. |
Input envelope
getLocalInputs: () => ({
discoverable: true,
stopParticipating: false,
input: {
left: held.left,
jumpPress: counters.jump,
},
})
inputis the game payload seen by query predicates.discoverableallows discovery predicates to inspect the candidate.stopParticipatingends the current participation attempt.- Non-discoverable input from a spectator with no active discoverable attempt may be silenced before transmission.
Session methods
| Method | Meaning |
|---|---|
connect() |
Starts room/network participation. |
disconnect() |
Stops the session and owned transport. |
ready() |
Resolves when game/runtime prerequisites are loaded. |
pump(options?) |
Advances host/session work; normally called each animation frame. |
settle() |
Test/diagnostic settling primitive. |
getMirror() |
Current read-only render mirror. |
getPresentationHints() |
Render-only interpolation hints. |
getRuntimeStatus() |
Stable runtime summary. |
diagnostics() |
Detailed unstable diagnostics. |
Runtime status includes runtimeMode, runtimeFallback, tick, connected, isLive,
localId, participant counts/peer IDs, and rollback count.
Step context
step(ctx) receives:
| Member | Meaning |
|---|---|
ctx.state |
Live canonical state for this tick. |
ctx.tick |
Canonical simulation tick. |
ctx.dt |
Tick duration supplied to the runtime. |
ctx.query(ids, frozenContext, predicateOrName) |
Rollback-tracked input decision. |
ctx.participants() |
Current admitted stable IDs. |
ctx.isParticipant(id) |
Membership guard for a possibly spectator/departed ID. |
ctx.discoverParticipants(predicate, limit, context?) |
Deterministic admission query. |
ctx.getStoppedParticipating() |
Drains canonical departures. |
ctx.releaseParticipant(id) |
Game-initiated departure. |
ctx.queryDisconnected(id, tick?) |
Rollback-tracked liveness decision. |
ctx.rawInputs() |
Simple-mode field-query proxy. |
ctx.trail(id, data, time?) |
Render-only movement sample. |
ctx.clearTrail(id) |
Clears a trail across teleport/wrap/death. |
ctx.emit(type, payload) |
Deterministic presentation event. |
ctx.world() |
Convenience facade over the same context. |
Query predicates
Gameplay query:
const moveAxis = (inputs, context) =>
Number(inputs[0]?.right) - Number(inputs[0]?.left);
inputs[i] corresponds to participantIds[i]. Access builds the dependency read-set.
context is frozen/canonicalized for later rechecks. Return a specific serializable
decision, never the inputs accessor itself.
Discovery predicate:
const claimSlot = (input, context) => input?.claimSlot === context.slot;
Discovery receives one candidate input as its first argument, not the gameplay inputs
array. The second argument is the frozen context supplied to
discoverParticipants.
rawInputs()
Requires:
simpleMode: true
Usage:
const inputs = ctx.rawInputs();
if (inputs[playerId].left) moveLeft();
Each property access is one standard query. It remains branch-sensitive and
rollback-correct, but separate fields produce separate decision results. Under dev,
first use warns that named queries are preferred.
World facade
Both schema and proxied worlds route engine services:
const world = ctx.world();
world.query(...);
world.participants();
world.isParticipant(id);
world.discoverParticipants(...);
world.rawInputs();
world.emit(...);
Schema mode additionally provides class-bound table helpers:
world.get(Type, id);
world.all(Type);
world.spawn(Type, id, record);
world.owned(Type, ownerId, factory);
world.destroy(entityOrType, id);
world.table(name);
world.id(prefix, ...parts);
Canonical state rules
Default proxied state supports plain objects, arrays, and JSON scalars. It rejects
functions, class instances, Map, Set, Date, undefined, and object aliases.
Standard array reads work normally. Mutating arrays support push, pop, shift,
unshift, splice, sort, reverse, copyWithin, and fill, provided the final
state contains no persistent object aliases.
Writes are legal only inside step. Canonical relationships should use stable IDs:
state.heldItemId = item.id;
Do not store the same mutable item object in two locations.
Mirror contract
Proxied mode:
const mirror = session.getMirror();
const state = mirror.state; // deeply frozen
Schema mode:
const players = mirror.tryTable('players');
for (const id of players.ids()) {
const row = players.get(id);
}
Treat mirrors as replaceable, read-only projections. Never use mirror values to drive canonical simulation.
Presentation
Simulation code must not access DOM, canvas, audio, timers, or sockets.
Use trails for interpolation:
ctx.trail(player.id, { x: player.x, y: player.y });
Use deterministic events for one-shot presentation:
ctx.emit('sound', { key: `jump:${player.id}:${ctx.tick}`, name: 'jump' });
Only finalized events should reach irreversible browser effects.
Advanced configuration
Common expert settings:
advanced: {
engine: {
inputDelay: 1,
autoRelease: { demote: 'consume' },
},
backend: {
snapshotInterval: 10,
patchesInterval: 3,
},
workerPoolSize: 3,
}
Advanced keys are validated but intentionally outside the stable compatibility contract. Measure before tuning.
Custom transport
Provide one of room or transport. A custom transport supplies stable local identity,
message delivery, peer events, current peers, disconnect, and the clock-sync channel.
See contracts/transport.md for the exact interface.
Determinism checklist
- Same initial state and queried results produce the same next state.
- No wall clock, ambient randomness, asynchronous work, DOM, audio, or network calls in
step. - Every mutable gameplay fact is canonical.
- Every input read goes through query, discovery, or explicitly enabled
rawInputs. - Queries are pure and registered.
- Participant input is guarded by membership.
- IDs remain stable across deletion and reordering.
- Presentation effects wait for confirmation.