Multiplayer --> Realtime rooms
Use this when the host simulation advances every tick. JavaScript only. Calibrate the rate. Do not guess 30 Hz.
Every member must be realtime-capable before the room activates. Native clients cannot join. A turn-based game with smooth animation is not a realtime room.
Responsibility
| Loki | The game |
|---|---|
| Transport, sequencing, authority/round fences | Simulation and physics |
| Snapshot pacing and backpressure | Collision |
| Input delivery and coalescing | Rendering |
| Reconnect and host migration | Prediction you configure |
| Diagnostics and calibration | Competitive integrity — not provided |
Loop
const room = client.createRealtimeRoom<RacerState, RacerInput>({
snapshotHz: 30,
diagnostics: true,
predict(state, localInput, dtSeconds) { return state; },
interpolate(from, to, t) { return to; },
});
await room.create();
room.publishSnapshot(currentState, { simulationTick });
room.setInput({ throttle: 75 });
await room.sendInput({ type: "placeBomb", cell: 12 });
function frame(now: number) {
room.advanceFrame(now);
render(room.getRenderState(now));
}
Drive rendering from one game-owned requestAnimationFrame loop. Call advanceFrame() and getRenderState() at most once per frame. setInput() is safe every frame; the SDK paces the network send.
Rates
snapshotHz is a ceiling (default 30, cap 100), not a delivery guarantee. Leave adaptiveRate off: omit it or pass false, and publish at a fixed snapshotHz. Raise that fixed rate within the ceiling to find the best rate for the game, with diagnostic evidence. The 12 Hz floor applies only when someone explicitly turns the adaptive rate on. Do not copy snapshotHz into game.json tickRate.
Calibrate
Prefer calibrateRealtimeRoom() over guessing. It needs two distinct player identities — two isolated browser profiles or two real players, not two tabs of one signed-in account. Write the recommended snapshotHz into createRealtimeRoom() and leave adaptiveRate off.
import { calibrateRealtimeRoom } from "@lokiplay/sdk";
const { recommended } = await calibrateRealtimeRoom({
snapshotHzCandidates: [12, 15, 20, 25, 30, 45, 60],
simulationHz: 60,
durationMsPerCandidate: 20_000,
createHostRoom: (candidate) =>
hostClient.createRealtimeRoom({
snapshotHz: candidate.snapshotHz,
adaptiveRate: false,
diagnostics: true,
}),
createGuestRoom: () =>
guestClient.createRealtimeRoom({ diagnostics: true }),
driveHost: (host, tick) =>
host.publishSnapshot(simulateOneStep(tick), { simulationTick: tick }),
driveGuest: (guest) => guest.setInput(representativeControl()),
});
const room = client.createRealtimeRoom({
snapshotHz: recommended.snapshotHz,
inputHz: recommended.inputHz,
simulationHz: recommended.simulationHz,
predict,
interpolate,
});
If calibration throws, fix the harness and rerun. If no candidate qualifies, publish at a fixed 12 Hz and leave adaptiveRate off. Write the recommended snapshotHz into the room. Do not copy adaptiveRate, minSnapshotHz, or initialSnapshotHz from the returned profile. Fine-tuning a racer after install means running this sweep on the real wiring, not copying another game’s Hertz.
Render streams
Predict the local entity. Interpolate remotes. Use getRenderStates() / composeRenderState, or createEntityCompositor / createLocalPrediction with bulk setEntities. Use sendEffect() / onConfirmedEffect() for authoritative events such as collisions.