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¶
_runner.LocalPlayer = 1;
_runner.StartGame(LatticeGameMode.Host, port); // starts + listens (chapter 02)
_session.HostStartMatch(); // Spawn(board), seat host as X (chapter 05)
_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
NetworkRunnersibling component (whoseFixedUpdatepumps 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.
[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 call — TicTacToeSession + 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:
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.