# Games — Details

> Multiplayer rooms over QUIC. State (server→all), input (player→server), event (any→any), each on the lane it needs.

<img src="/images/diagrams/games.png" alt="games data flow" />

The **games** modality is multiplayer rooms with three baked-in channels, each
mapped onto the QUIC lane that fits its delivery semantics. You don't wire up
tracks or pick priorities — you create a `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 |

**state** and **input** ride QUIC **datagrams** — there is no retransmit, so a
dropped tick is simply superseded by the next one. That's exactly what you want
for a position snapshot or a controller frame: re-sending a stale tick would
arrive too late to matter. **event** rides a reliable, ordered MoQT subgroup
**stream** — nothing is dropped and order is preserved, which is what you want
for anything a player would notice going missing (a chat line, a "you picked up
the item" grant, an end-of-match settlement).

> **NOTE:**
> 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 a `playerId`.

  - **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**.

The roles are enforced. Calling `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`.

```ts
// authority — no playerId
const auth = new Games({ relayHost, token, roomId: "duel-42" });

// player — playerId required
const me = new Games({ relayHost, token, roomId: "duel-42", playerId: "alice" });
```

## 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:

```
[u8 from_len][utf-8 player_id][payload]
```

The authority's `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.

> **WARNING:**
> A `playerId` must encode to **255 bytes or fewer** in UTF-8; the SDK throws on
> overflow. Keep player ids short and stable for the life of the room.

## Tick rates

Both **state** and **input** are tick-driven. You drive your own loop and call
`write` once per tick; the modality does not run a clock for you. When you create
the state publisher you can pass a `tickHz` hint:

```ts
const state = await auth.publishState({ tickHz: 30 });
setInterval(() => state.write(serializeWorld(world)), 1000 / 30);
```

`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     |

> **TIP:**
> Keep state snapshots small and self-contained — latest-wins means each one must
> stand alone. Send deltas only if your client can recover from a dropped delta
> (e.g. periodic keyframes). For anything that must not be lost, use an **event**,
> not state.

## 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 `Games` client 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)](/modalities/netcode/details).
  - **Use data when…** — You want hierarchical MQTT-style topics with `+` / `#` filters and retained
    messages rather than fixed room channels. See [Data](/modalities/data/details).
  - **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/<room>/state             server → all     datagram, no from-header
game/<room>/input             player → server  datagram, from-header
game/<room>/event/<channel>   any → any        reliable stream, from-header
```

Capability tags: `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](/modalities/games/sdk-methods) — the typed surface
- [Games — Cookbook](/modalities/games/cookbook) — short task recipes
- [Games — Recipes](/modalities/games/recipes) — full worked examples
- [Netcode (Unity)](/modalities/netcode/details) — the UPM transport drop-in
- [Realtime Tracks](/concepts/realtime-tracks) — the MoQT primitive underneath
