Key responsibilities
This page owns three outcomes for every engineer reading it:Why this architecture
Three game modes (Single Player, Online Multiplayer, Pass & Play) sharing one codebase creates a real engineering problem: if game logic is entangled with Unity MonoBehaviours, the multiplayer client can’t share that logic with the Nakama server. If UI code reaches directly into networking classes, you can’t unit-test score calculation without spinning up a Nakama instance. The layered architecture solves this by enforcing one rule:Dependencies always point inward. Outer layers know about inner layers. Inner layers know nothing about outer layers.Domain knows nothing. Application knows only Domain. Infrastructure and Presentation both know Domain and Application, but neither knows the other — they are wired together at runtime by VContainer’s DI container inside the Bootstrap scene.
The four layers
Full dependency graph
The dashed line from Presentation to Infrastructure is not a compile-time reference. At startup, Bootstrap’s VContainerLifetimeScope registers NakamaMultiplayerService as the implementation of IMultiplayerService. GameSession receives IMultiplayerService via its constructor and never sees the concrete type.
Client-server model
The same four-layer architecture runs in all three game modes. What changes between modes is which infrastructure implementations are registered in the DI container.
In local modes,
GameSession acts as the single source of truth. It calls ISetValidator.Validate() directly and produces the authoritative result.
In online multiplayer (Planned), GameSession is a mirror. The client sends raw card IDs to Nakama via IMultiplayerService.SendClaim(); the server runs its own instance of the same Set validation logic and broadcasts the authoritative result back. The client GameSession applies that result and emits the reactive state update. The client never produces the authoritative verdict in a multiplayer match.
Three reactive streams
Application and Presentation communicate exclusively through three observable streams. Neither side polls, and neither calls the other directly.
ViewModels in Presentation subscribe to
StateStream and EventStream. TouchInputHandler pushes IGameCommand objects into CommandStream. GameSession subscribes to CommandStream and pushes into StateStream / EventStream. No component ever calls back across the boundary it owns.
Technology stack
Non-negotiable rules
These rules come from the Hard Boundaries document and are enforced by.asmdef compile-time checks and editor validators:
Domain and Application never reference UnityEngine or Nakama
Domain and Application never reference UnityEngine or Nakama
SET.Domain and SET.Application have no Unity or Nakama package references in their .asmdef files. Any attempt to using UnityEngine; in these assemblies is a compile error. This keeps game logic fully testable outside the Unity editor and portable to the Nakama server runtime.No UI polling in Update()
No UI polling in Update()
All UI state changes flow through R3 observable subscriptions.
MonoBehaviour.Update() is reserved for visual animation only (card movement along a path, for example). No game-state reads in Update().VContainer constructor injection throughout
VContainer constructor injection throughout
No
FindObjectOfType, no GetComponent<T>() for services, no static singletons. Every dependency is declared in a constructor parameter and wired by VContainer’s LifetimeScope in the Bootstrap scene.Implementation checklist
Use this checklist when adding a new cross-layer feature to ensure the architecture is respected:- Domain type added? Zero external references — no
UnityEngine,Nakama, orR3imports - New Application interface defined? Lives in
Application/Services/— Infrastructure implements it, Presentation consumes it via DI - New mode or flow added? State authority model documented — client-owned (local) or server-mirror (online multiplayer, Planned)
- No
GameSessionexpansion into Infrastructure concerns — audio, platform, and network belong in their own Infrastructure services - All reactive streams unidirectional — Application pushes state out; Presentation pushes commands in; no bidirectional callbacks
- New DI binding registered in Bootstrap — no
new ConcreteType()calls outside the composition root - Unit tests added for any new Domain or Application logic in
_Tests/EditMode/
Common mistakes
Related pages
Layer Responsibilities
Detailed ownership rules, forbidden dependencies, and communication contracts per layer.
Assembly Definitions
How
.asmdef files enforce the dependency graph at compile time.DI with VContainer
How Bootstrap wires interfaces to implementations and how to register new services.
Reactive UI Pipeline
R3 observable streams, ViewModel patterns, and subscription lifecycle management.