Skip to content

02 — Your first runner

The NetworkRunner is the single object you drive everything through. It owns the native runner handle, marshals native callbacks into managed C# events, and exposes StartGame / Connect / Tick / Spawn / Rpc / SendEvent.

Verified against: bindings/lattice-unity/com.lattice.netcode/Runtime/NetworkRunner.cs.

The runner is a MonoBehaviour in Unity

In the editor, NetworkRunner is a MonoBehaviour. It drives its own Tick from FixedUpdate on the main thread, so every callback lands on the main thread and it is safe to touch Transform / GameObject from your handlers.

NetworkRunner is a MonoBehaviour under UNITY_5_3_OR_NEWER
public sealed partial class NetworkRunner :
#if UNITY_5_3_OR_NEWER
    UnityEngine.MonoBehaviour,
#endif
    IDisposable
{
    private void FixedUpdate()
    {
        if (_runner != IntPtr.Zero)
            Tick(UnityEngine.Time.fixedDeltaTime);
    }
    private void OnDestroy() => Dispose();
}

Verified against: NetworkRunner.cs:17-22 and NetworkRunner.cs:390-396.

Because it's a component, you create it with AddComponent<NetworkRunner>()not new — in the editor. (The headless build uses the plain new NetworkRunner(tickRateHz) constructor instead, which is why both exist.)

Verified against: NetworkRunner.cs:109-116; sample usage at samples/unity-tictactoe/Assets/TicTacToe/Scripts/TicTacToeGame.cs:65-77.

The four game modes

StartGame takes a LatticeGameMode:

Mode Value Meaning
Server 0 Dedicated authoritative server (not a player)
Host 1 Server that is also a local participant (listen-server)
Client 2 Joins a server; no authority
SharedHost 3 Shared / distributed authority

Verified against: bindings/lattice-unity/com.lattice.netcode/Runtime/Interop/LatticeNative.cs:31-37.

IsAuthority is true for Server, Host, and SharedHost — those roles own the simulation and may Spawn. A Client cannot spawn.

Verified against: NetworkRunner.cs:98-99.

Starting a host

A host is the authoritative server and a local player. StartGame(Host, port) starts the session and, because a host is an authority, begins listening on port in one call.

Start as host (from the sample)
public void StartHost()
{
    BuildRunner();
    _session.RegisterTypes();                       // register networked types FIRST (chapter 05)
    // No LocalPlayer assignment: StartGame reads the host's participant id from the core.
    _runner.StartGame(LatticeGameMode.Host, port);   // Host = server + local participant; begins listening
    _session.HostStartMatch();                       // spawn the world (chapter 05)
    _started = true;
}

Verified against: TicTacToeGame.cs:43-53.

StartGame only auto-listens when the role is an authority and port != 0:

NetworkRunner.StartGame
public void StartGame(LatticeGameMode mode, ushort port = 0)
{
#if UNITY_5_3_OR_NEWER
    if (_runner == IntPtr.Zero) Initialize(60, 0);
#endif
    Mode = mode;
    _started = true;
    LatticeException.ThrowIfError(LatticeNative.lattice_runner_start(_runner, mode), "start");
    // An authority knows its participant id (0) the moment it starts -- no tick needed.
    RefreshLocalPlayer();
    if (IsAuthority && port != 0)
        Listen(port);
}

Verified against: NetworkRunner.cs:213-225.

Don't set LocalPlayer on a host — earlier copies of this sample did

Notice there is no LocalPlayer assignment above. The runner reads its own participant id from the core (lattice_runner_local_peer_id), and for any authority role that id is 0.

Older copies of this sample carried a _runner.LocalPlayer = 1; line here. If you have one, delete it — it was wrong twice over:

  1. The host's participant id is 0, not 1. 1 is how a client addresses the server, not the host's own identity, so the pinned value disagreed with the owner the core stamps.
  2. Assigning LocalPlayer pins it, so the runner would never adopt the correct id the core reports — including the real id a client is assigned during the secure handshake.

Full semantics — including what a client reads before the authority has told it its id — are on Peer identity & local player.
Verified against: NetworkRunner.cs:60-96; conformance W.19a ("the authority's local participant id is 0 (not 1)"); the corrected sample at samples/unity-tictactoe/Assets/TicTacToe/Scripts/TicTacToeGame.cs:43-53, whose headless checks now assert "host is participant 0 (core convention)".

Starting a client and connecting

A client starts in Client mode (no port — it isn't listening) and then Connects to the server's address and port. Connect takes an optional connection token (a byte[]).

Start as client and connect (from the sample)
public void StartClient(string address = "127.0.0.1")
{
    BuildRunner();
    _session.RegisterTypes();                        // SAME types, SAME order as the host
    _runner.StartGame(LatticeGameMode.Client);       // no port: a client does not listen
    _runner.Connect(address, port, System.Text.Encoding.ASCII.GetBytes("ttt"));
    _started = true;
}

Verified against: TicTacToeGame.cs:56-63.

NetworkRunner.Connect
public void Connect(string address, ushort port, byte[] token = null)
{
    token ??= Array.Empty<byte>();
    LatticeException.ThrowIfError(
        LatticeNative.lattice_runner_connect(_runner, address, port, token, (uint)token.Length),
        $"connect({address}:{port})");
}

Verified against: NetworkRunner.cs:230-236.

The token here is arbitrary

In the sample the token is just the bytes "ttt" — the loopback/local server does not validate it. In a managed deployment the token would be the director-minted session token you get from /resolve (see chapter 04). The binding forwards whatever bytes you pass to the native lattice_runner_connect.

Wiring up the runner

The sample builds the runner as a sibling component so its FixedUpdate pumps the tick automatically — you never call Tick yourself in the editor:

BuildRunner (from the sample)
private void BuildRunner()
{
    // NetworkRunner is itself a MonoBehaviour; add it as a sibling component so its
    // FixedUpdate drives Tick on the main thread (no manual pumping needed here).
    _runner = gameObject.AddComponent<NetworkRunner>();
    _session = new TicTacToeSession(_runner);

    // Subscribe to gameplay events (chapters 05–08).
    _session.OnBoardChanged  += b => _board = b;
    _session.OnMarkAssigned  += m => _myMark = m;
    _session.OnChat          += (mark, text) => _chatLog.Add($"{MarkGlyph(mark)}: {text}");
}

Verified against: TicTacToeGame.cs:65-77.

The tick pump

Tick(double dt) pumps one or more fixed steps — recv → simulate → send — and fires every callback synchronously inside the call. In Unity, FixedUpdate calls it for you with Time.fixedDeltaTime; in a headless/console app you call it in your own loop.

NetworkRunner.Tick
public void Tick(double dt)
{
    LatticeException.ThrowIfError(LatticeNative.lattice_runner_tick(_runner, dt), "tick");
    RefreshLocalPlayer();         // a client learns its participant id mid-handshake
    AdvanceSimulationClock(dt);   // advances CurrentTick / SimulationTime / LocalAlpha
}

Verified against: NetworkRunner.cs:240-250.

Because callbacks fire inside Tick, and Tick runs on the main thread in Unity, your OnSpawned / OnStateUpdated / OnEvent handlers can safely instantiate prefabs, move transforms, and update UI directly.

The runner's events

Subscribe to these to react to the network. All fire inside Tick.

Event Signature Fires when
OnConnected Action The connection is established
OnDisconnected Action<int> Disconnected (arg = reason code)
OnSpawned Action<NetworkObject> A replicated object appeared
OnDespawned Action<ulong> An object (by netid) was removed
OnStateUpdated Action<NetworkObject> A replicated object's state changed
OnRpc Action<ulong, ushort, byte[]> An RPC arrived (netid, rpcId, payload)
OnEvent Action<ushort, ulong, ulong, byte[]> A custom event arrived (eventId, netid, sender, payload)
OnAuthorityChanged Action<ulong, ulong, uint> State authority changed (netid, newOwner, authorityTick)
OnLog Action<LatticeLogLevel, string> The core logged a line

Verified against: NetworkRunner.cs:24-38.

Connection state

Poll State any time to see where the connection is:

if (_runner.State == LatticeConnectionState.Connected) { /* … */ }

LatticeConnectionState is Disconnected, Connecting, Connected, Disconnecting.

Verified against: NetworkRunner.cs:252, LatticeNative.cs:39-45. The Fusion-compat helpers IsRunning, IsConnectedToServer, and IsServer derive from this and Mode. Verified against: NetworkRunner.Simulation.cs:95-101.

Tearing down

Dispose() frees the native runner and clears the object table. In Unity, OnDestroy calls it for you, so destroying the GameObject cleans up. Shutdown() is an async-friendly alias that returns a completed Task.

Verified against: NetworkRunner.cs:372-385, NetworkRunner.cs:396.


The minimal shape

Putting it together, here is the smallest useful runner setup, host and client:

Minimal host + client (real API)
using Lattice;
using Lattice.Interop;
using UnityEngine;

public sealed class MyNet : MonoBehaviour
{
    public ushort port = 7777;
    private NetworkRunner _runner;

    public void Host()
    {
        _runner = gameObject.AddComponent<NetworkRunner>();
        _runner.OnConnected += () => Debug.Log("a client connected");
        // No LocalPlayer assignment: the runner reads its own id from the core (it is 0 on a host).
        // RegisterTypes() here — see chapter 05
        _runner.StartGame(LatticeGameMode.Host, port);   // starts + listens
    }

    public void Join(string address)
    {
        _runner = gameObject.AddComponent<NetworkRunner>();
        _runner.OnConnected += () => Debug.Log("connected to host");
        // RegisterTypes() here — SAME order as the host
        _runner.StartGame(LatticeGameMode.Client);
        _runner.Connect(address, port, token: null);
    }
    // FixedUpdate on the runner pumps Tick automatically.
}

Register types before you connect

Both peers must register the same networked types in the same order before the client connects, so type ids and content hashes agree. The sample calls RegisterTypes() inside StartHost / StartClient before StartGame/Connect. This is covered in chapter 05.
Verified against: TicTacToeGame.cs:46,59; TicTacToeSession.cs:62-84.


Next: 03 — Logging in, to get a real player identity from the auth service.