Build a multiplayer game
Drift is the live demo: a shared presence-relay room where every visitor is a moving dot on one canvas. Drop in, drop out, no rounds, no game-over — Silt’s pure-relay default, made alive. Open it in two tabs and the dots sync live.
The whole multiplayer surface is a handful of calls. The file is 397 lines; the
rest draws dots. This page walks the part that talks to Silt — every snippet
below is verbatim from examples/drift/src/main.ts.
The whole multiplayer surface
Section titled “The whole multiplayer surface”-
Import the client
Vanilla TypeScript, no framework. You import
joinRoomand the types you narrow against.import { joinRoom, type Room, type StatusUpdate } from "@siltrun/client"; -
Join the room
joinRoomresolves once you’re connected and the join snapshot has arrived. The bounded timeouts mean a blocked network fails loudly instead of spinning forever.room = await joinRoom(RELAY, {id: myId!,connectTimeoutMs: 6000, // honest port-blocked failure: bounded, no infinite spinnersnapshotTimeoutMs: 6000,}); -
Render everyone already here
Because the snapshot has landed,
room.peersis already populated. Draw the peers that arrived before you — some may not have a position yet (stateisnulluntil a peer broadcasts).for (const p of room.peers) {if (p.state) applyPresence(p.id, p.state as PresenceState);else ensureDot(p.id);} -
Listen — presence, join, leave, status
Presence is the firehose: every other dot’s latest position.
join/leavekeep membership honest, andstatussurfaces the connection lifecycle so a drop is visible on screen instead of a frozen room.room.on("presence", (peerId, state) => {if (peerId === myId) return; // never let a relayed echo of ourselves fight local inputapplyPresence(peerId, state as PresenceState);refreshCount();});room.on("join", (peer) => {ensureDot(peer.id); // fades in (or revives a mid-fade dot of the same id)refreshCount();});room.on("leave", (peerId, _reason) => {// both "left" and "timeout" → the dot fades out then is reaped by the render loopconst d = dots.get(peerId);if (d) d.dead = true;refreshCount();});room.on("status", (u: StatusUpdate) => {if (u.status === "reconnecting") setStatus("reconnecting…", "--warn");else if (u.status === "connected") { setStatus("connected", "--ok"); refreshCount(); }else if (u.status === "failed") { setStatus("can't reach the room (UDP may be blocked here)", "--bad"); }else if (u.status === "closed") setStatus("disconnected", "--muted");}); -
Broadcast your own position
Set your presence on a timer — latest-wins, droppable, so you fire it as often as you like. Here it’s ~20 Hz.
// broadcast our presence at ~20Hz (latest-wins datagram; skip while spectating)setInterval(() => {if (spectating) return;room.presence.set({ x: me.x, y: me.y, heading: me.heading });}, 1000 / SEND_HZ);
That’s the entire multiplayer surface. No QUIC handshake, no cert pinning, no server to deploy — you join a room by URL, publish your presence, and react to everyone else’s.
The fan-out is the infrastructure
Section titled “The fan-out is the infrastructure”Dots drifting is the easy part. What’s worth watching is the received-datagram rate: as peers join, each one adds ~20 Hz of presence to the feed. One peer is ~20 Hz; two peers is ~40 — the aggregate is the relay doing its job. That fan-out, over real WebTransport, is what Silt runs so you don’t have to.
- The two-lane model — presence vs events, and when each wins.
- API reference — every method, event, and option.
- Wire protocol — what these calls put on the wire.