Skip to content

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 OnGameSessionComplete with 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

AspectRun (GameRunManager)Session (GameManager)
ScopeFull campaign attemptSingle shift/level
DurationMultiple days/weeksTime-limited (e.g., 120s)
Starts”Start Adventure” in Overworld”Start Shift” in Shop
EndsAll days complete OR failureTimer expires OR all cats served
PersistsPrestige, deck, upgrades, flagsScore, metadata only

Scene Architecture

Multi-scene additive loading setup:

ScenePurposeLifetime
BootstrapSceneApp-level persistent services (ProfileManager, SaveLoadManager, SceneTransitionCoordinator, GameFlagManager)Always loaded
GamePlaythroughBaseScenePlaythrough-scoped services (PersistentHelperCatManager, PersistentPlayerProgressionManager)During active playthrough (auto-loaded by Init(args), unloaded on MainMenu return)
MainMenuSceneV2Menu UI, profile selection, 3D environment, “Play Tutorial”Until game starts
OverworldSceneHub world exploration, NPC interactions, restaurant selectionDuring hub phase
GameRunSceneShop + gameplay + victory/loss + tutorial + cutscenesDuring a game run

Key classes:

  • BootstrapManager - Application entry point, loads MainMenuSceneV2
  • PlaythroughBaseInitializer - Verifies playthrough services in GamePlaythroughBaseScene
  • SceneTransitionCoordinator - Handles scene loading/unloading, stores pending configs
  • GameRunSceneInitializer - Entry point for GameRunScene, consumes GameRunStartConfig
  • OverworldSceneInitializer - Entry point for OverworldScene, consumes OverworldStartConfig

Scene Transition Pattern

Scenes communicate via pending configs stored on SceneTransitionCoordinator:

  1. Caller creates a config (e.g., GameRunStartConfig) with all data the target scene needs
  2. Calls SceneTransitionCoordinator.TransitionToGameRun(config) — stores config, triggers scene load
  3. Target scene’s initializer calls ConsumePendingGameRunConfig() in Start() — one-shot consumption
  4. Initializer routes to the appropriate setup (NewRun, RestoreSave, Tutorial, CustomRun)

Initialization Order

GameRunScene (GameRunSceneInitializer.Start())

// Phase 1: Initialize managers in dependency order
GameRunManager.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 mode
HandleStartConfig(config); // NewRun, RestoreSave, Tutorial, or CustomRun

OverworldScene (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 overworld
InitializeOverworld(config);

Data Flow: Session Completion

GameManager.GameOver()
fires OnGameSessionCompleteWithMetadata(metadata)
GameRunManager.GameSessionManager receives it
Awards prestige, advances WorkDayProgress
Transitions to ShopPrepareNextWorkShiftState

Key Singletons

SingletonScenePurposeStatus
GameManager.InstanceGameRunSceneSession gameplayActive
GameRunManager.InstanceGameRunSceneRun meta-gameActive
GameRunOrchestrator.InstanceGameRunSceneState transitions within a runActive
GameOverworldManager.InstanceOverworldSceneOverworld coordinationActive
SceneTransitionCoordinator.InstanceBootstrapSceneScene transitionsDeprecated — 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)
AssemblyPathContentsautoReferenced
Game.DataScripts/Data/ScriptableObjects, data structs, boss service interfacetrue
Game.CatPresentationSystems/CatPresentation/Cat visuals (Animancer, materials, audio interface)false
Game.OverworldSystems/Overworld/Hub world — movement, NPCs, camera, lightingfalse
Game.CustomRunSystems/CustomRun/Custom run config, UI, service interfaces & locatorsfalse
GameScripts/ (root)Main gameplay, managers, shop, save/loadtrue
Game.BossesSystems/Bosses/Boss encounters, phases, behaviors, UIfalse
Game.CharacterCustomizationSystems/CharacterCustomization/Cat appearance customizationtrue
Game.UIUI/Menu/Menu UI panelsfalse

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 ← Game

Variant 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.Bosses

Cross-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):

  1. Interface lives in the new assembly’s Services/ folder — e.g., IOverworldRunService, ICustomRunManager
  2. Static locator lives alongside — e.g., OverworldRunService, CustomRunService with Register/Unregister/[Service|Manager|etc]
  3. Adapter lives in Game assembly — wraps the concrete manager
  4. 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.HasActiveCustomRun

For 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 shift

Example 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.