Skip to content

09 — Full walkthrough: networked tic-tac-toe

This chapter assembles everything into the complete sample: a two-player, host-authoritative tic-tac-toe with chat over the real NetworkRunner. Every file referenced here exists and is verified headlessly by a .NET harness that drives a full match over the native library — the run ends 55/55 checks passed.

Verified against: samples/unity-tictactoe/ (all files below); samples/unity-tictactoe/README.md ("Latest result: 55/55 checks passed").

The architecture

The sample is split so the netcode is provable without the Unity editor:

flowchart TD
  subgraph EA["Engine-agnostic (no UnityEngine) — verified headlessly"]
    R["TicTacToeRules.cs<br/>pure win/draw/legal-move logic"]
    P["TicTacToeProtocol.cs<br/>BoardState [Networked] block, event ids, codecs"]
    S["TicTacToeSession.cs<br/>NetworkRunner glue: register types, spawn, validate, chat"]
  end
  subgraph UI["Unity front end — needs the editor"]
    G["TicTacToeGame.cs<br/>MonoBehaviour: IMGUI board + chat, FixedUpdate tick"]
  end
  subgraph PROOF["Headless proof — runs without the editor"]
    H["MatchHarness.cs + Program.cs<br/>host + client over the local server; asserts the match"]
  end
  G --> S
  H --> S
  S --> P
  S --> R

Verified against: samples/unity-tictactoe/README.md (project layout and split); TicTacToeSession.cs:1-8, TicTacToeGame.cs:1-6.

File Role Unity-free?
Assets/TicTacToe/Scripts/TicTacToeRules.cs Pure rules: legal move, win/draw eval
Assets/TicTacToe/Scripts/TicTacToeProtocol.cs Wire contract: BoardState, event ids, codecs
Assets/TicTacToe/Scripts/TicTacToeSession.cs Netcode glue over NetworkRunner
Assets/TicTacToe/Scripts/TicTacToeGame.cs Unity MonoBehaviour front end (IMGUI) ❌ editor-only
headless/MatchHarness.cs + headless/Program.cs Runnable proof

The full flow, end to end

Here is the complete lifecycle of a match, tying each step to its chapter and source.

sequenceDiagram
  participant H as Host (server + X)
  participant C as Client (O)
  H->>H: RegisterTypes(); StartGame(Host, port); HostStartMatch() → Spawn(board)
  C->>C: RegisterTypes(); StartGame(Client); Connect(127.0.0.1, port)
  C-->>H: OnConnected → SendEvent(EventJoin, Server)
  H-->>C: SendEventToPeer(EventAssignMark=O)
  H-->>C: board replicates (OnSpawned)
  C-->>H: SendEvent(EventMove, Server)  [client requests]
  H->>H: TryApplyMove → validate → WriteState + MarkDirty
  H-->>C: dirty fields replicate (OnStateUpdated)
  C-->>H: SendEvent(EventChat, Server) → host re-broadcasts All
  H->>H: Evaluate → XWins / Draw replicates to C

Verified against: TicTacToeSession.cs (the handlers) and headless/Program.cs (the asserted sequence).

1. Both peers register the same types

Before connecting, host and client register the board type identically so ids and content hashes agree (chapter 05):

// on BOTH peers, before StartGame/Connect
_session.RegisterTypes();   // → NetworkType with matching Id + ContentHash across peers

Verified against: TicTacToeGame.cs:46,59; MatchHarness.cs:66-68; asserted at headless/Program.cs:61-63.

2. Host starts + spawns; client connects

Host
_runner.LocalPlayer = 1;
_runner.StartGame(LatticeGameMode.Host, port);   // starts + listens (chapter 02)
_session.HostStartMatch();                        // Spawn(board), seat host as X (chapter 05)
Client
_runner.StartGame(LatticeGameMode.Client);
_runner.Connect("127.0.0.1", port, System.Text.Encoding.ASCII.GetBytes("ttt"));

Verified against: TicTacToeGame.cs:43-63; MatchHarness.cs:70-77.

3. Join handshake assigns the client its mark

On OnConnected, the client sends EventJoin; the host reads the sender, assigns O, and replies to that peer with EventAssignMark (chapter 07).

Verified against: TicTacToeSession.cs:169-178,252-259; asserted at headless/Program.cs:71-74.

4. The board replicates

The client's OnSpawned fires with the board; it reads BoardState and renders. Subsequent host writes arrive via OnStateUpdated.

Verified against: TicTacToeSession.cs:180-191; asserted at headless/Program.cs:76-80.

5. Moves: request → validate → replicate

The client sends EventMove; the host validates against the trusted seat and only then writes + marks dirty (chapter 08). Illegal moves are rejected and change nothing.

Verified against: TicTacToeSession.cs:106-165,218-224; asserted at headless/Program.cs:84-107,175-189.

6. Chat both ways

Chat rides SendEvent; the host re-broadcasts and re-stamps the authoritative sender mark.

Verified against: TicTacToeSession.cs:119-132,230-248; asserted at headless/Program.cs:109-126.

7. Win / draw detection

After each applied move the host Evaluates the board; XWins / OWins / Draw replicates via the result field.

Verified against: TicTacToeRules.cs:64-78; TicTacToeSession.cs:154-156; asserted at headless/Program.cs:128-138,147-169.

The Unity front end

TicTacToeGame.cs is a single MonoBehaviour you drop on a GameObject. It's entirely guarded by #if UNITY_5_3_OR_NEWER, so it compiles to nothing in the headless build. It:

  • adds a NetworkRunner sibling component (whose FixedUpdate pumps the tick),
  • owns a TicTacToeSession,
  • draws the lobby, board, chat, and status with IMGUI (OnGUI), and
  • calls _session.RequestMove(cell) and _session.SendChat(text) from buttons.
The whole front end is one component (abridged)
[AddComponentMenu("Lattice Samples/TicTacToe Game")]
public sealed class TicTacToeGame : MonoBehaviour
{
    public ushort port = 7777;
    private NetworkRunner _runner;
    private TicTacToeSession _session;

    public void StartHost()  { BuildRunner(); _session.RegisterTypes();
                               _runner.LocalPlayer = 1;
                               _runner.StartGame(LatticeGameMode.Host, port);
                               _session.HostStartMatch(); _started = true; }

    public void StartClient(string address = "127.0.0.1")
                             { BuildRunner(); _session.RegisterTypes();
                               _runner.StartGame(LatticeGameMode.Client);
                               _runner.Connect(address, port, Encoding.ASCII.GetBytes("ttt"));
                               _started = true; }

    private void BuildRunner()
    {
        _runner  = gameObject.AddComponent<NetworkRunner>();
        _session = new TicTacToeSession(_runner);
        _session.OnBoardChanged += b => _board = b;
        _session.OnMarkAssigned += m => _myMark = m;
        _session.OnChat         += (mark, text) => _chatLog.Add($"{MarkGlyph(mark)}: {text}");
    }
}

Verified against: TicTacToeGame.cs:19-77.

The MonoBehaviour + IMGUI need the editor

The rendering and lifecycle in TicTacToeGame require Unity, so they're authored but not run in the headless environment. Everything they callTicTacToeSession + the rules — is the same code the harness verifies. To play in the editor, open the folder as a Unity project (2022.3 LTS), open Assets/TicTacToe/Scenes/TicTacToe, press Play in two editor instances, and click Host in one and Join in the other.
Verified against: samples/unity-tictactoe/README.md ("To play it in the editor").

Run the proof yourself

Because the netcode is Unity-free, you can run the whole match headlessly against the real native library:

cd samples/unity-tictactoe/headless
bash run.sh          # in-process loopback transport
bash run.sh udp      # the identical flow over real localhost UDP — no code change

Expected tail:

 RESULT: 55/55 checks passed, 0 failed.
 ALL HEADLESS CHECKS PASSED.

The harness runs a host + client as two in-process NetworkRunners over the local server, wraps each in a TicTacToeSession, and pumps both with Tick until predicates hold — the same StartGame / Connect / Tick / Spawn / SendEvent API this tutorial taught.

Verified against: samples/unity-tictactoe/headless/run.sh, samples/unity-tictactoe/headless/MatchHarness.cs, headless/Program.cs.

Where to go next

  • Serialize richer state with compressed floats, vectors, and quaternions — see the field kinds in chapter 05 and the BitWriter/BitReader in chapter 07.
  • Go to a dedicated server (LatticeGameMode.Server) and register it with the fleet, then wire the matchmaking flow.
  • Add shared authority with RequestAuthority / OnAuthorityChanged (chapter 08) for objects whose owner changes hands.
  • Browse the API reference for the full C ABI behind the binding.

Recap: what's verified vs. what you wire

Capability Status in this tutorial
Runner host/client, tick, connect ✅ verified (sample + headless)
Type registration, spawn, replicated state ✅ verified
Custom events (moves, chat, join, assign) ✅ verified
Host-authoritative validation + rejection ✅ verified
RPCs (Rpc / OnRpc) ✅ verified by the binding's headless P/Invoke test (sample uses events instead — see ch. 06)
Auth login → access token ✅ real endpoints (/login, /guest); SDK applies the bearer
Matchmake → resolve → connect ✅ the Matchmaker helper does the exchange and connects (headless tests incl. a real loopback connect) — you still must run/register a game server, or /matchmake 409s (ch. 04)
Shared authority transfer ⚠️ real API, not exercised by the sample/headless test (ch. 08)
[Networked] source generator, plugin import, IMGUI 📝 need the Unity editor (documented, not run here)

That honesty is deliberate: build on the verified paths first, and treat the ⚠️/📝 rows as integration work you own.