Architecture Overview
Two-Layer State Management
The game uses a multi-scene architecture where each scene represents a distinct game phase. Within GameRunScene, a two-layer state machine manages gameplay flow:
Layer 1: GameRunOrchestrator (Visual/UI within GameRunScene)
Location: Assets/_Game/Scripts/Managers/GameRunOrchestrator.cs
- Single source of truth for state transitions within a game run
- Handles: UI visibility, camera switching, canvas management
- Always use
TransitionToState()for state changes within a run - Validates transitions via
IsValidTransition() ReturnToOverworld()triggers a scene transition back to OverworldScene
Layer 2: GameRunManager (Meta-Game)
Location: Assets/_Game/Scripts/Managers/GameRunManager.cs
- Manages entire campaign attempt (a “run”)
- Tracks: WorkDayProgress, prestige currency, helper cat deck, upgrades
- Coordinates shops, cutscenes, and progression
- Lifetime: “Start Adventure” → Run completion or failure
Layer 3: GameManager (Session)
Location: Assets/_Game/Scripts/GameManager.cs
- Manages single gameplay session (one shift)
- Coordinates 15+ managers: InventoryManager, CatSpawnManager, ScoringManager, etc.
- Lifecycle:
StartSession()→ gameplay loop →GameOver() - Fires
OnGameSessionCompletewith metadata
State Flow Diagram
OverworldScene (Hub World) ↓ (TransitionToGameRun with GameRunStartConfig)GameRunScene ├── RunIntro ├── ShopPrepareNextWorkShiftState ├── Gameplay (The Shift) ├── GameplayLost └── Cutscene ↓ (TransitionToOverworld on completion/resign)OverworldScene (Hub World)Run vs Session
| Aspect | Run (GameRunManager) | Session (GameManager) |
|---|---|---|
| Scope | Full campaign attempt | Single shift/level |
| Duration | Multiple days/weeks | Time-limited (e.g., 120s) |
| Starts | ”Start Adventure” in Overworld | ”Start Shift” in Shop |
| Ends | All days complete OR failure | Timer expires OR all cats served |
| Persists | Prestige, deck, upgrades, flags | Score, metadata only |
Scene Architecture
Multi-scene additive loading setup:
| Scene | Purpose | Lifetime |
|---|---|---|
| BootstrapScene | App-level persistent services (ProfileManager, SaveLoadManager, SceneTransitionCoordinator, GameFlagManager) | Always loaded |
| GamePlaythroughBaseScene | Playthrough-scoped services (PersistentHelperCatManager, PersistentPlayerProgressionManager) | During active playthrough (auto-loaded by Init(args), unloaded on MainMenu return) |
| MainMenuSceneV2 | Menu UI, profile selection, 3D environment, “Play Tutorial” | Until game starts |
| OverworldScene | Hub world exploration, NPC interactions, restaurant selection | During hub phase |
| GameRunScene | Shop + gameplay + victory/loss + tutorial + cutscenes | During a game run |
Key classes:
BootstrapManager- Application entry point, loads MainMenuSceneV2PlaythroughBaseInitializer- Verifies playthrough services in GamePlaythroughBaseSceneSceneTransitionCoordinator- Handles scene loading/unloading, stores pending configsGameRunSceneInitializer- Entry point for GameRunScene, consumesGameRunStartConfigOverworldSceneInitializer- Entry point for OverworldScene, consumesOverworldStartConfig
Scene Transition Pattern
Scenes communicate via pending configs stored on SceneTransitionCoordinator:
- Caller creates a config (e.g.,
GameRunStartConfig) with all data the target scene needs - Calls
SceneTransitionCoordinator.TransitionToGameRun(config)— stores config, triggers scene load - Target scene’s initializer calls
ConsumePendingGameRunConfig()inStart()— one-shot consumption - Initializer routes to the appropriate setup (NewRun, RestoreSave, Tutorial, CustomRun)
Initialization Order
GameRunScene (GameRunSceneInitializer.Start())
// Phase 1: Initialize managers in dependency orderGameRunManager.Initialize(); // First (no dependencies)GameRunOrchestrator.Initialize(gameRunManager); // Second (depends on RunManager)
// Phase 2: Fade out main menu music// Phase 3: Load helper resources (HelperCatDatabase)// Phase 3b: Reload progression data with unified config (ensures REST_ IDs)ProgressionDataManager.Instance.ReloadProgressionData();// Phase 4: Consume pending config and route to start modeHandleStartConfig(config); // NewRun, RestoreSave, Tutorial, or CustomRunOverworldScene (OverworldSceneInitializer.Start())
// Phase 1: Verify active profile// Phase 2: Fade out main menu music// Phase 3: Load helper resources (HelperCatDatabase)// Phase 4: Restore persistent data (helper cats, progression, flags)RestorePersistentData();// Phase 5: Consume config and initialize overworldInitializeOverworld(config);Data Flow: Session Completion
GameManager.GameOver() ↓fires OnGameSessionCompleteWithMetadata(metadata) ↓GameRunManager.GameSessionManager receives it ↓Awards prestige, advances WorkDayProgress ↓Transitions to ShopPrepareNextWorkShiftStateKey Singletons
| Singleton | Scene | Purpose | Status |
|---|---|---|---|
GameManager.Instance | GameRunScene | Session gameplay | Active |
GameRunManager.Instance | GameRunScene | Run meta-game | Active |
GameRunOrchestrator.Instance | GameRunScene | State transitions within a run | Active |
GameOverworldManager.Instance | OverworldScene | Overworld coordination | Active |
SceneTransitionCoordinator.Instance | BootstrapScene | Scene transitions | Deprecated — use Init(args) injection. See DependencyInjection |
Most persistent services (ProfileManager, SaveLoadManager, SceneTransitionCoordinator, etc.) are registered via Init(args) [Service] attribute and injected automatically into scene initializers. See DependencyInjection for patterns and migration guide.
Assembly Architecture
The codebase is split into focused Unity assembly definitions to enable fast incremental compilation.
Assembly Dependency Graph
Game.Data (base — data types + IBossEncounterController + BossEncounterService; autoReferenced: true) ^ ^ ^ ^ | | | | | Game.CatPres Game.Overworld Game.CustomRun (autoReferenced: false) | ^ ^ ^ ^ | | | | | Game (refs all above + packages; does NOT ref Game.Bosses) ^ Game.Bosses (refs Game + Game.Data; autoReferenced: false) Game.CharCustom, Game.UI, PlayModeTests (as before)| Assembly | Path | Contents | autoReferenced |
|---|---|---|---|
Game.Data | Scripts/Data/ | ScriptableObjects, data structs, boss service interface | true |
Game.CatPresentation | Systems/CatPresentation/ | Cat visuals (Animancer, materials, audio interface) | false |
Game.Overworld | Systems/Overworld/ | Hub world — movement, NPCs, camera, lighting | false |
Game.CustomRun | Systems/CustomRun/ | Custom run config, UI, service interfaces & locators | false |
Game | Scripts/ (root) | Main gameplay, managers, shop, save/load | true |
Game.Bosses | Systems/Bosses/ | Boss encounters, phases, behaviors, UI | false |
Game.CharacterCustomization | Systems/CharacterCustomization/ | Cat appearance customization | true |
Game.UI | UI/Menu/ | Menu UI panels | false |
Two Assembly Extraction Patterns
Two patterns are used for extracting systems into their own assemblies:
Variant A — Full Extraction (e.g., Game.CustomRun):
New assembly sits below Game. It references only Game.Data. All dependencies on Game singletons are inverted: service interfaces + static locators live in the new assembly; adapter classes that wrap the concrete managers live in Game. Game.asmdef adds a reference to the new assembly.
Game.Data ← Game.CustomRun ← GameVariant B — Hybrid Extraction (e.g., Game.Bosses):
New assembly sits above Game. Boss code uses GameManager, CatSpawnManager, etc. — inverting all is uneconomical. Instead, the critical interface (IBossEncounterController) + service locator (BossEncounterService) live in Game.Data. Game.Bosses references Game, and Game uses only the Game.Data interface. Game never references Game.Bosses directly.
Game.Data ← Game ← Game.BossesCross-Assembly Communication: Service Locator + Adapter Pattern
Game.Overworld and Game.CustomRun cannot reference Game (circular dependency). Classes that previously called GameRunManager.Instance or similar now call through service interfaces instead.
Pattern (mirrors CatAudioService / CustomizationResolverService):
- Interface lives in the new assembly’s
Services/folder — e.g.,IOverworldRunService,ICustomRunManager - Static locator lives alongside — e.g.,
OverworldRunService,CustomRunServicewithRegister/Unregister/[Service|Manager|etc] - Adapter lives in
Gameassembly — wraps the concrete manager - Registration happens in the concrete manager’s
Awake()/OnDestroy()
Game.Overworld code: OverworldRunService.Service?.IsRunActive ↓ (interface)Game adapter (registered): GameRunManagerOverworldAdapter.IsRunActive ↓ (delegates to)Game concrete class: GameRunManager.Instance.IsRunActive
Game.CustomRun code: CustomRunService.Manager?.HasActiveCustomRun ↓ (interface)Game class (self-registers): CustomRunConfigManager.HasActiveCustomRunFor Sisus Init injection (e.g., IEnvironmentLightingManager into OverworldInteriorManager), the concrete class calls Service.Set<IEnvironmentLightingManager>(this) in Awake() so Init can resolve the interface type at startup.
Overworld Substates
OverworldSubstateManager manages internal substates within OverworldScene:
- Exploration - Player moving around hub
- CharacterCustomization - Customizing cat appearance
Camera switching and panel visibility are handled within Overworld.
Example Flow: First Day of New Run
1. OverworldScene (Exploration substate) → Player selects restaurant, star, difficulty → Creates GameRunStartConfig (mode=NewRun) → SceneTransitionCoordinator.TransitionToGameRun(config)
2. GameRunScene loads → GameRunSceneInitializer.Start() → Initializes GameRunManager, GameRunOrchestrator → Routes to StartNewRun(config) → Checks for START_OF_GAME_SESSION cutscene → Transitions to ShopPrepareNextWorkShiftState
3. ShopPrepareSubstate.ShiftPreparation → Player clicks "Start Shift"
4. Gameplay → GameManager.StartSession() runs the shift → If boss level: BossEncounterController.StartEncounter() activates → Player serves cats, earns score (boss encounters add phase/damage mechanics) → Session ends (time up / all served / boss defeated)
5. GameManager fires OnGameSessionComplete(metadata)
6. GameRunManager receives metadata → Awards prestige based on performance → Advances WorkDayProgress to next day → Transitions to ShopPrepareNextWorkShiftState
7. ShopPrepareSubstate.ResultsReview → Shows score, prestige earned → Player proceeds to shop or next shiftExample Flow: Tutorial from Main Menu
1. MainMenuScene → Player clicks "Play Tutorial" → Creates GameRunStartConfig (mode=Tutorial, tutorialConfig=...) → SceneTransitionCoordinator.TransitionToGameRun(config)
2. GameRunScene loads → GameRunSceneInitializer.Start() → Routes to StartTutorial(config) → TutorialRunManager.StartTutorialWithConfig(config.tutorialConfig)
3. Tutorial plays (auto-advances, no shop) → TutorialRunManager orchestrates level flow → TutorialSequence handles in-level interactions → DialogueBubblePanel shows typewriter dialogue via Febucci Text Animator → PictureInPictureManager shows speaker cat in PiP frame (manual-hide mode) → TutorialOnlyObject auto-disables tutorial objects when not in tutorial
4. Tutorial completes → GameRunOrchestrator.ReturnToOverworld() → SceneTransitionCoordinator.TransitionToOverworld()Editor shortcut: Check Debug Start As Tutorial on GameRunSceneInitializer to start tutorial directly from the editor Play button on GameRunScene.