# Netcode (Unity) — Recipes

> End-to-end: port the ECS racing sample onto the ClutchCall transport, and stand up a headless dedicated server with matchmaking.

Longer, worked examples that combine the install, the transport swap, lane
selection, and the lifecycle callbacks into something you can run. For the
per-method reference see [SDK methods](/modalities/netcode/sdk-methods); for
short snippets see the [Cookbook](/modalities/netcode/cookbook).

## Recipe 1 — Port the ECS racing sample onto ClutchCall

Unity's
[ECS-Network-Racing-Sample](https://github.com/Unity-Technologies/ECS-Network-Racing-Sample)
is a DOTS / Netcode for Entities project. Swapping its transport is the fastest
way to confirm an end-to-end match runs over the ClutchCall relay mesh.

  1. **Add the package**
Open the sample in Unity and add the transport via **Package Manager → Add
    package from git URL**:

    ```text
    https://github.com/clutchcall/core-sdk.git?path=unity/com.clutchcall.transport
    ```
  2. **Swap the transport on the NetworkManager**
Find the prefab holding the `NetworkManager`. Remove `UnityTransport`, add
    `ClutchCallTransport`, and repoint Netcode at it:

    ```csharp
    var nm = manager.GetComponent<NetworkManager>();
    nm.NetworkConfig.NetworkTransport = nm.GetComponent<ClutchCallTransport>();
    ```
  3. **Configure relay + token**
Drive the inspector fields from a small bootstrap so host and clients agree
    on the room:

    ```csharp
    var t = nm.GetComponent<ClutchCallTransport>();
    t.RelayHost = "relay.clutchcall.dev";
    t.RoomId    = "race-01";
    t.Token     = await FetchRoomTokenAsync("race-01", localPlayerId);
    ```
  4. **Assign host vs client roles**
Keep the sample's own "Go in game" flow. The host opens the room's authority
    namespace; each client announces a discovery namespace and joins it:

    ```csharp
    if (isHost) nm.StartHost();
    else        nm.StartClient();
    ```
  5. **Build players (not the editor)**
Editor batch mode does not drive Netcode for Entities. Build a **player** for
    each role and launch them. The `[Server]` / `[Client]` connection lifecycle
    completes, `NetworkId` is assigned, and the cars sync.

What flows where:

- **Car transforms / physics ticks** ride the **unreliable** lane (QUIC
  datagrams) — high frequency, latest-wins, no head-of-line blocking.
- **Spawns, despawns, and race-start / finish RPCs** ride the **reliable** lane
  (MoQT subgroup streams) — ordered, guaranteed.

Over a full match, tens of thousands of datagrams flow through the relay. You can
sanity-check link health from the transport:

```csharp
foreach (var id in nm.ConnectedClientsIds)
    Debug.Log($"peer {id}: rtt={nm.GetComponent<ClutchCallTransport>().GetCurrentRtt(id):F0}ms");
```

> **NOTE:**
> If a client fails to appear in the host roster, confirm both built players share
> the **same `RoomId`** and a valid token. Roster membership is driven by the
> discovery namespace announcement — a mismatched room means the host never sees
> the announce.

## Recipe 2 — Headless dedicated server with matchmaking

A common production shape: a **dedicated server** player runs the authority for a
room, and clients fetch a room-scoped token from your control-plane API before
connecting. The server has no display and starts on launch.

### Server bootstrap

```csharp
using Unity.Netcode;
using ClutchCall.Transport;
using UnityEngine;

public class DedicatedServer : MonoBehaviour {
    [SerializeField] NetworkManager nm;

    async void Start() {
        if (!Application.isBatchMode) return; // server build only

        var roomId = Args.Get("room", "lobby-1");
        var t = nm.GetComponent<ClutchCallTransport>();
        t.RelayHost = "relay.clutchcall.dev";
        t.RoomId    = roomId;
        t.Token     = await MintServerTokenAsync(roomId);

        nm.OnClientConnectedCallback  += id => Debug.Log($"join  {id} ({nm.ConnectedClientsIds.Count} in room)");
        nm.OnClientDisconnectCallback += id => Debug.Log($"leave {id}");
        nm.OnTransportFailure         += () => { Debug.LogError("relay session failed"); Application.Quit(1); };

        nm.StartServer();
        Debug.Log($"authority up for room {roomId}");
    }
}
```

### Client matchmaking + join

The client asks the control-plane API for a room and a token, configures the
transport, then connects:

```csharp
public class Matchmaker : MonoBehaviour {
    [SerializeField] NetworkManager nm;

    public async Task JoinAsync(string playerId) {
        // 1. control-plane API hands back a room + short-lived token
        var match = await Api.PostAsync<MatchResp>(
            "https://api.clutchcall.dev/matchmake",
            new { player = playerId });

        // 2. point the transport at the assigned room
        var t = nm.GetComponent<ClutchCallTransport>();
        t.RelayHost = match.RelayHost;     // nearest POP
        t.RoomId    = match.RoomId;
        t.Token     = match.Token;         // room-scoped, short-lived

        // 3. join — datagram lane carries inputs, reliable carries spawns
        nm.OnTransportFailure += () => SceneManager.LoadScene("Lobby");
        nm.StartClient();
    }
}
```

### Why this shape works

<AccordionGroup>
  <Accordion title="No self-hosted relay" icon="globe">
    Both the dedicated server and the clients connect **outbound** to the nearest
    relay POP. There is no inbound port to open and no relay infrastructure to
    run — the mesh fans state and input between regions.
  </Accordion>
  <Accordion title="One QUIC connection per process" icon="link">
    The server holds a single QUIC connection for the authority; each client
    holds one for its session. Reliable and unreliable lanes multiplex over it —
    no second socket, no separate reliability layer to tune.
  </Accordion>
  <Accordion title="Tokens are room-scoped and short-lived" icon="key">
    The control-plane API mints a token bound to a room and a short TTL, so a
    leaked client token cannot join arbitrary matches. The server mints its own
    authority token the same way.
  </Accordion>
</AccordionGroup>

## Recipe 3 — Lane discipline for a fast-paced shooter

When you control the gameplay code, route traffic onto the right lane explicitly.
The rule of thumb: **latest-wins high-frequency traffic is unreliable; anything
you cannot drop is reliable.**

```csharp
public class PlayerSync : NetworkBehaviour {
    // 60 Hz transform updates — drop-safe, latest-wins → unreliable datagram
    [Rpc(SendTo.Server, Delivery = RpcDelivery.Unreliable)]
    void MoveRpc(Vector3 pos, Quaternion rot, ulong tick) =>
        server.IntegrateMove(OwnerClientId, pos, rot, tick);

    // hit registration — must arrive, must be ordered → reliable subgroup stream
    [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
    void FireRpc(ulong targetTick, Vector3 origin, Vector3 dir) =>
        server.ResolveShot(OwnerClientId, targetTick, origin, dir);

    void Update() {
        if (!IsOwner) return;
        // guard the datagram budget for the move payload
        if (transform.HasMovedSince(lastTick))
            MoveRpc(transform.position, transform.rotation, NetworkTickSystem.Tick);
    }
}
```

> **WARNING:**
> Keep unreliable payloads under `MaxPayloadSize()`. A QUIC datagram larger than
> the negotiated max is **rejected, not fragmented** — so an oversized snapshot
> silently fails to send rather than splitting across packets. For large
> authoritative state, send a reliable baseline and unreliable deltas.

## Related

  - **[Details](/modalities/netcode/details)** — Wire model, lanes, and the architecture.
  - **[Games modality](/modalities/games/details)** — The pure-MoQT face of the same wire model, for non-Unity engines.
