
Games client for a (room, player)
and call publishState / publishInput / publishEvent. The room namespace,
the lane, and the per-channel QoS come pre-set so you can’t mis-route a channel.
The three channels
| Channel | Direction | Lane | Delivery | Use for |
|---|---|---|---|---|
| state | server → all | datagram (lossy) | latest-wins | tick-rate world snapshots |
| input | player → server | datagram (lossy) | latest-wins | tick-rate player inputs |
| event | any → any | stream (reliable) | ordered, no-drop | chat, lobby, item grants, settlement |
Internally all three channels are MoQT frame tracks — opaque binary with a
per-frame priority — fanned out by the relay mesh. state/input default to
priority
100 on the datagram lane; event defaults to priority 50 on the
reliable stream lane. You can override priority per write.Authoritative server vs player roles
A room has exactly one authority (the server) and any number of players. The role is decided by one option: whether you pass aplayerId.
Authority (server)
Construct
Games without playerId. It publishes state to every
player and subscribes to inputs from all of them on a single callback.
It is the source of truth for the world.Player (client)
Construct
Games with a playerId. It publishes input up to the
server and subscribes to state coming down. It may also publish/subscribe
events.publishInput (or publishEvent) without a
playerId throws — only players carry the from identity the wire format
needs. The authority never publishes input and never needs a playerId.
The from header — fan-in without N subscriptions
Every player publishes input to the same track (game/<room>/input), and
the relay fans every player’s frames to the authority. To tell senders apart
without a track-per-player, the SDK prefixes a tiny header on each input/event
frame:
subscribeInputs((playerId, bytes) => …) callback gets the
decoded playerId for free — the SDK strips the header. state frames skip
the header entirely, because the source is always the room authority.
Tick rates
Both state and input are tick-driven. You drive your own loop and callwrite once per tick; the modality does not run a clock for you. When you create
the state publisher you can pass a tickHz hint:
tickHz is a hint — it informs the relay’s admission control and the
recorder, but it does not pace your writes. Typical rates:
| Game shape | state tick | input tick |
|---|---|---|
| Turn-based / card / board | 1–5 Hz | event-driven |
| Casual real-time / .io | 10–20 Hz | 20–30 Hz |
| Action / shooter | 30–60 Hz | 60+ Hz |
Latency budget
A round trip of player input → server tick → state broadcast is dominated by the player→relay RTT plus the relay→player RTT — one MoQT group time each way. On the same continent as a relay POP this is typically 30–80 ms end to end; across continents it is RTT-bound. Datagram lanes add no retransmit or head-of-line blocking, so a single lost tick costs you one tick, not a stall.Architecture
Under the modality sits the shared substrate every modality uses:- One QUIC connection, one session. A
Gamesclient opens a single MoQT session per(room, player), lazily on the first publish or subscribe, and reuses it for every channel. - MoQT track fan-out. state/input/event are MoQT frame tracks; the relay mesh fans them to every subscriber. The authority does not know how many players are subscribed to state — it publishes once.
- Right lane per channel. Lossy snapshots take the QUIC datagram lane; reliable events take an ordered subgroup stream. One connection multiplexes both — no separate stack per channel.
- Kernel-bypass data plane. The engine runs a shard-per-core reactor with an AF_XDP NIC fast path and lock-free mcache / dcache rings on io_uring, so tick fan-out stays flat as rooms scale.
- One auth token. A single tenant token, scoped to
(tenant, room, player?), authorizes the session — the same token model as every other modality.
When to use it
Use games when…
You have authoritative-server multiplayer: rooms, ticks, a server world, and
players sending inputs and receiving snapshots. Real-time
.io games,
action games, board/card games, co-op sessions.Use Netcode (Unity) when…
You’re in Unity and want Netcode for GameObjects / Entities to “just work”
over the same wire — drop in the transport package instead of calling this
client directly. See Netcode (Unity).
Use data when…
You want hierarchical MQTT-style topics with
+ / # filters and retained
messages rather than fixed room channels. See Data.Drop to MoQT when…
Your wire model isn’t a room with three channels — import
@clutchcall/sdk/moqt and publish/subscribe frames directly.Wire convention
game.state, game.input, game.event. The session URL
encodes the role — players dial …/games/<room>/<player>, the authority dials
…/games/<room>/_authority.
Related
- Games — SDK Methods — the typed surface
- Games — Cookbook — short task recipes
- Games — Recipes — full worked examples
- Netcode (Unity) — the UPM transport drop-in
- Realtime Tracks — the MoQT primitive underneath

