# ADR-0002: Scene Architecture and DontDestroyOnLoad Boundaries

**Status:** Accepted  
**Date:** 2026-06-23  
**Deciders:** Mahmoud Aglan  
**Engine:** Unity 6 + FishNet v4  

---

## Context

IStalk has two distinct phases: a pre-game lobby (relay JSON signaling) and an in-game match (FishNet networked gameplay). These phases have different system requirements, different UI, and different object lifetimes. The transition between them involves loading a new scene, starting FishNet networking, and spawning player NetworkObjects.

Key constraints from the GDDs:
- `RelayClient` must persist through the scene transition (it holds the active WebSocket connection).
- `LobbyManager` orchestrates the transition and must survive to trigger FishNet start after the new scene loads.
- FishNet's `NetworkManager` must exist before any NetworkObjects can be spawned.
- All gameplay NetworkObjects (players, items, props) should be destroyed cleanly when returning to lobby.
- `GameStartData` is a static class (no GameObject) — it survives by nature.

---

## Decision

**Two-scene architecture with explicit DontDestroyOnLoad boundaries.**

| Scene | Contains | Loaded When |
|-------|----------|-------------|
| `LobbyScene` | Lobby UI canvas, `RelayClient` (spawns here), `LobbyManager` (spawns here), `AudioManager` (spawns here) | Application start, and when returning from a match |
| `GameScene` | `NetworkManager` prefab (auto-activates), spawn points, map geometry, extraction altars, lighting | Loaded via `SceneManager.LoadSceneAsync` on `game_start` |

### DontDestroyOnLoad Objects

These persist across scene transitions:

| Object | Reason |
|--------|--------|
| `RelayClient` | Holds active WebSocket — cannot be closed and reopened without losing room state |
| `LobbyManager` | Orchestrates scene load → FishNet start sequence; must survive the transition it triggers |
| `AudioManager` | Music/ambient should crossfade through transitions, not cut abruptly |
| `NetworkManager` | FishNet singleton — once started, persists until explicitly stopped. Spawned in GameScene but marked DDOL by FishNet internally. |

### Scene-Bound Objects (destroyed on scene unload)

Everything else, including:
- All player NetworkObjects (`RunnerController`, `EyeController`, `MinionController`)
- All world NetworkObjects (`WorldItem`, `ExtractionAltar`, `PropBase` instances)
- `GameManager` NetworkObject
- `PlayerSpawner`
- All HUD canvases (`RunnerHUD`, `EyeHUD`)
- Cinemachine cameras
- Map geometry and lighting

---

## Rationale

1. **Minimal DDOL surface.** Only objects that MUST survive transitions are marked DDOL. This prevents "zombie object" bugs where stale gameplay objects persist into the lobby.

2. **Clean match teardown.** When a match ends, the return-to-lobby path is: FishNet `ServerManager.StopConnection()` → `ClientManager.StopConnection()` → `SceneManager.LoadScene("LobbyScene")`. All NetworkObjects in `GameScene` are automatically destroyed by both scene unload and FishNet's despawn-on-disconnect. No manual cleanup needed.

3. **FishNet NetworkManager is DDOL by default.** FishNet marks its own `NetworkManager` as `DontDestroyOnLoad` internally when `ServerManager.StartConnection()` is called. We don't fight this — we let it persist. On match end, we call `StopConnection()` which cleans up all NetworkObjects but leaves the manager alive for the next match.

4. **AudioManager in DDOL** enables ambient music to loop through the lobby→game transition without cutting out. A simple `CrossfadeTo(gameAmbient)` call during scene load provides professional-feeling transitions.

---

## Transition Sequences

### Lobby → Game

```
1. RelayClient receives game_start JSON
2. LobbyManager.HandleGameStart():
   a. GameStartData.Set(msg)              — static class, no scene dependency
   b. LobbyManager disables lobby UI
   c. await SceneManager.LoadSceneAsync("GameScene")
   d. (GameScene Awake): NetworkManager finds itself, PlayerSpawner initializes
   e. LobbyManager.StartFishNet():
      - if IsHost: ServerManager.StartConnection() + ClientManager.StartConnection("localhost")
      - else: ClientManager.StartConnection(relayAddress)
   f. FishNet server: OnServerStart → PlayerSpawner.WaitForAllClients()
   g. All clients connect → spawn sequence begins
```

### Game → Lobby (match ends or host disconnects)

```
1. GameManager.OnMatchEnded fires (or connection lost detected)
2. LobbyManager.ReturnToLobby():
   a. if IsHost: ServerManager.StopConnection()
   b. ClientManager.StopConnection()
   c. (FishNet auto-despawns all NetworkObjects)
   d. SceneManager.LoadScene("LobbyScene")
   e. (GameScene unloads: all scene-bound objects destroyed)
   f. LobbyManager re-enables lobby UI
   g. RelayClient.ReturnToLobby() — sends return_to_lobby to relay
```

---

## Consequences

**Positive:**
- Match teardown is automatic and complete — no dangling references.
- Lobby re-entry is a clean slate (fresh scene load).
- DDOL objects are few and well-defined — easy to reason about.
- FishNet's own DDOL behavior is respected, not fought.

**Negative:**
- Scene load is not instantaneous (~0.5-2s depending on map complexity). A loading screen is needed.
- `NetworkManager` persists even in lobby — must ensure it doesn't interfere with lobby-only logic. Mitigation: `NetworkManager` does nothing until `StartConnection()` is called.
- If `AudioManager` or `RelayClient` get accidentally destroyed, the game breaks silently. Mitigation: singleton pattern with `if (Instance != null && Instance != this) { Destroy(gameObject); return; }`.

---

## GDD Requirements Addressed

- TR-SCN-001: Two scenes (LobbyScene + GameScene)
- TR-SCN-002: RelayClient + LobbyManager are DontDestroyOnLoad
- TR-SCN-003: GameStartData static class for cross-scene data
- TR-SCN-004: FishNet starts AFTER GameScene loads

---

## References

- `design/gdd/sys-21-lobby-matchmaking.md` — Scene transition orchestration
- `design/gdd/sys-20-relay-transport.md` — RelayClient lifecycle
- `design/gdd/sys-33-fishnet-replication.md` — NetworkManager and NetworkObject spawn/despawn
