Skip to main content
In SET: 3D Edition’s online modes, no gameplay decision — Set validity, score change, or claim resolution order — is ever made by a client. Every competitive outcome flows from one source of truth: the Nakama authoritative match handler running on the server. This page explains why that design was chosen, how the client and server communicate, and what code contracts enforce the boundary so that client code can never accidentally break it.
Pre-production — Planned Feature. The Nakama integration, match handler, and all multiplayer systems described on this page are architectural specifications for a game currently in pre-production. None of the networking code is implemented yet. Implementation should follow this document as the blueprint.

Why Server Authority?

In a real-time card game where two players can tap the same Set within milliseconds of each other, the client cannot be trusted to resolve claims fairly or correctly. Three problems arise if clients self-validate:
  1. Cheating is trivial. A modified client can pre-validate a Set and send the result rather than the raw card IDs, guaranteeing a win on every contested claim.
  2. Race conditions are unresolvable. Two clients seeing two different “first claim” results with no shared clock to arbitrate creates an unsolvable split-brain.
  3. State diverges. Without a single broadcaster, clients accumulate subtle differences in board state, scores, or deck order that become corrupted game sessions.
Server authority eliminates all three: the server owns the canonical state, runs the validation, resolves ties by server-received timestamp, and broadcasts the result. Every client receives the same update.

The Core Rule

One sentence to internalise: In multiplayer, the client sends card IDs only. It never calls SetValidator to decide whether those cards form a valid Set.
The Unity client collects the player’s three card selections and passes them to the OnlineMatchController, which fires a match_claim message over the Nakama WebSocket. The Nakama match handler runs SetValidator on the server, updates match state, and broadcasts the new MatchState to all connected clients. The OnlineMatchController receives that state update, translates it into an ApplyServerStateCommand, and the GameSession applies it via GameSession.ApplyServerState(). The UI reacts automatically through the R3 reactive stream. There is no shortcut path where the client checks first and sends only valid claims. That would still be a form of cheating enablement — an attacker could intercept the send and replay it with different IDs.

Architecture

The diagram below shows every component involved in a single multiplayer claim, from the player’s finger to the state broadcast that reaches all clients. The GameSession is a mirror in multiplayer — it holds a copy of the server’s state and applies updates atomically. It never computes state independently. Critically, GameSession does not hold a reference to IMultiplayerService directly; the OnlineMatchController acts as the intermediary that bridges the network layer and the game session.

Why Nakama?

The project evaluated several networking options before choosing Nakama. The comparison below captures the reasoning: The key advantage: Nakama replaces at least four previously separate infrastructure concerns — matchmaking, leaderboards, cloud save, and auth — with one coherent, self-hostable backend. The Heroic Labs managed cloud option reduces DevOps overhead during launch and can be migrated to self-hosted when scale justifies it.

Nakama Components Used


Client-Side Interface Contract

The entire networking layer is hidden behind a single interface defined in the Application assembly. No gameplay or UI code ever imports a Nakama type directly:
The concrete NakamaMultiplayerService in the Infrastructure assembly implements IMultiplayerService using the Nakama .NET Client SDK. The Application layer depends only on the interface — it can be swapped for a mock during testing without any changes to game logic.

The OnlineMatchController Pattern

GameSession does not hold a direct reference to IMultiplayerService. Instead, the OnlineMatchController (Infrastructure assembly) sits between the two:
Why this separation matters:
  • GameSession remains ignorant of networking. It processes IGameCommands regardless of whether they originate from local input, the AI, or the network.
  • OnlineMatchController is the single translation point: ServerMessageIGameCommand. If the message schema changes, only OnlineMatchController needs updating.
  • The pattern makes GameSession trivially unit-testable for the multiplayer path — inject a mock IMultiplayerService, emit messages, assert commands.

The IOnlineGameSession Contract

The IOnlineGameSession interface, which GameSession implements when in multiplayer mode, is deliberately designed with no ValidateSet method:
The absence of ValidateSet is not an oversight — it is the architecture’s structural enforcement of server authority. A developer cannot accidentally add client-side validation because the interface does not expose the affordance.

Risks and Mitigations


Implementation Checklist

  • NakamaMultiplayerService implements IMultiplayerService in the Infrastructure assembly — no Nakama SDK types leak into Application or Domain assemblies
  • OnlineMatchController is the sole consumer of IMultiplayerService.Messages; it translates each ServerMessage into an ApplyServerStateCommand for GameSession
  • GameSession does NOT hold a direct reference to IMultiplayerService — only OnlineMatchController does
  • IOnlineGameSession has no ValidateSet method — only SendClaim and ApplyServerState
  • ApplyServerState() is the only write path for all multiplayer game state on the client

Common Mistakes

Common Mistakes
  • Adding SetValidator calls to multiplayer client code “for responsiveness.” Even a call that only controls a visual (e.g., playing a pre-emptive valid-set animation) leaks outcome information before the server confirms it, and creates a confusing rollback experience when the server disagrees. Show a “claiming…” state instead.
  • Importing Nakama SDK types into the Application or Domain assembly. NakamaMultiplayerService lives in the Infrastructure assembly. If you find yourself writing using Nakama; in a GameSession or ViewModel file, you’ve broken the dependency boundary.
  • Implementing ValidateSet on IOnlineGameSession. The interface omits it on purpose. Adding it bypasses the authority model at the contract level.
  • Having GameSession call IMultiplayerService.SendClaim directly. All network calls from the game session go through OnlineMatchController. The session should only receive IGameCommands — it must not know that some of them originate from a network.

Match Lifecycle

The full flow from matchmaking queue to post-match results, including the in-match play loop.

Sync & Reconnect

How the 20 Hz server tick, full-state broadcasts, and the 30-second reconnect window work.

Anti-Cheat

Rate limiting, server-side data ownership, and why the client cannot influence outcomes.

Architecture Layers

How the Domain, Application, Infrastructure, and Presentation assemblies enforce dependency boundaries.