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; for
short snippets see the Cookbook.
Recipe 1 — Port the ECS racing sample onto ClutchCall
Unity’s
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:
Find the prefab holding the NetworkManager. Remove UnityTransport, add
ClutchCallTransport, and repoint Netcode at it:
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:
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:
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:
foreach (var id in nm.ConnectedClientsIds) Debug.Log($"peer {id}: rtt={nm.GetComponent<ClutchCallTransport>().GetCurrentRtt(id):F0}ms");
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.
The client asks the control-plane API for a room and a token, configures the
transport, then connects:
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(); }}
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.
One QUIC connection per process
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.
Tokens are room-scoped and short-lived
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.
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.
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); }}
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.