Multiplayer

Host ticks. Loki delivers.

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

LokiThe game
Transport, sequencing, authority/round fencesSimulation and physics
Snapshot pacing and backpressureCollision
Input delivery and coalescingRendering
Reconnect and host migrationPrediction you configure
Diagnostics and calibrationCompetitive integrity — not provided

Loop

const room = client.createRealtimeRoom<RacerState, RacerInput>({
  snapshotHz: 30,
  adaptiveRate: true,
  initialSnapshotHz: 12,
  minSnapshotHz: 8,
  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 and cap 30), not a delivery guarantee. Start conservative with adaptiveRate: true. 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 only the returned RealtimeProfile into createRealtimeRoom().

import { calibrateRealtimeRoom } from "@lokiplay/sdk";

const { recommended } = await calibrateRealtimeRoom({
  snapshotHzCandidates: [8, 12, 15, 20, 25, 30],
  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({ ...recommended, predict, interpolate });

If calibration throws, fix the harness and rerun. If no candidate qualifies, use the floor profile (8 Hz, adaptive). 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.