Architecture

In short

There are no scenes to set up: GameBootstrap spawns one GameRoot, which builds every system in code. Content is data (DefaultContent), every gameplay effect is a stat modifier, systems talk through GameEvents, and one seed drives a whole run. Each screen is an IMGUI view picked by the current GameState.

How it works

The diagram below is the README’s diagram, updated to what main does now. Solid arrows mean “creates or calls”. Dotted arrows mean “reads from”.

flowchart LR
    Boot["GameBootstrap<br/>press Play: spawns [Game]"] --> Root["GameRoot<br/>composition root,<br/>holds the GameState"]
    Content[("ContentDatabase<br/>Resources asset or DefaultContent")] -.-> Root
    Fit[("FitOverrides<br/>FitOverrides.json + fit-overrides.json")] -.->|applied at boot| Content
    Save[("SaveSystem<br/>save.json, run.json")] <--> Meta["MetaProgression<br/>unlocks, maps, XP, levels"]
    Root --> Meta
    Root --> Runs["RunManager (solo)<br/>map select, 9 innings, shop, run over"]
    Root --> Net["NetRuntime<br/>transport + NetSession"]
    Net -->|RunStarting| Coop["CoopRun (co-op)<br/>CoopRules on the host"]
    Root --> Cage["BattingCage"]
    Runs & Coop -->|IRunDriver = GameRoot.Run| Screens
    Runs & Coop -->|first inning| Intro["MatchIntroDirector<br/>match intro + walk-up"]
    Runs & Coop -->|boss down, run over| Outro["OutroDirector<br/>end-of-game cutscenes, Fireworks"]
    Intro --> AtBat
    Runs & Coop & Cage --> AtBat["AtBatController<br/>pitch, swing, flight, count"]
    AtBat --> Show["Hit show<br/>HitCounter, BallTracer,<br/>FlightCamera, replays"]
    Runs & Coop --> Field["BallparkBuilder<br/>field, crowd, scoreboard, sky"]
    Content -.-> Runs
    Content -.-> Catalog["IndexCatalog<br/>INDEX screen entries"]
    Runs --> Stats(["PlayerStats<br/>batter + level perks + upgrades<br/>+ synergies + park + pitcher"])
    Stats -.-> AtBat
    Runs -->|run over| Meta
    Runs -->|checkpoint| Save
    AtBat & Runs & Coop & Root -->|raise| Events{{"GameEvents<br/>event bus"}}
    Events -->|subscribe| Screens["UIRoot<br/>one IMGUI view per GameState"]
    Events -->|subscribe| Audio["GameAudio"]
    Events -->|subscribe| Fx["ScreenFade, GreenFeedback,<br/>TimingIndicator, Scoreboard, crowd"]
    Events -->|PlayEnded| Log["SwingLog<br/>swings.csv"]
    Catalog -.-> Screens
    Show -.-> Screens
    Intro & Outro -.-> Screens

Boot order inside GameRoot.Awake (see Boot and GameRoot): content, fit overrides, save, display settings, meta progression, leaderboard, camera and light, ballpark, at-bat, the add-on components (including SwingLog, the two cutscene directors and ParkSnapshots), networking, batting cage, RunManager, UIRoot, then the main menu.

The big ideas

  1. No scenes needed. GameBootstrap runs after any scene loads and adds a GameRoot if none exists. Everything else is built from code. See Boot and GameRoot.
  2. Everything is a stat modifier. Batters, level perks, upgrades, synergies, ballparks and boss pitchers all give List<StatModifier>. PlayerStats sums them: (base + adds) x (1 + percents). See Stats System.
  3. Content is data. Pitchers, pitch types, ballparks, upgrades, synergies, batters and cosmetics are ScriptableObjects (call signs are plain serializable CallSignDefinition entries inside the ContentDatabase), all created in code by DefaultContent.Build() so the game runs with zero assets. See Ballparks, Upgrades, Batters.
  4. Systems talk through GameEvents. Gameplay raises events; UI, audio and effects subscribe. Gameplay code never knows audio exists. See Game Events.
  5. Deterministic, seeded runs. One run seed builds the innings; pitches, special balls and each shop visit get their own SeededRandom stream. See Seeds and Determinism.
  6. IMGUI views per GameState. UIRoot.OnGUI draws one view for the current state. Add a screen = a state + a view + a case. See Game States and Screens and UI System.

Solo and co-op share the screens

GameRoot.Run returns an IRunDriver: the active CoopRun while a co-op game runs, else RunManager. The map, shop, build panel, pause menu and results screen only talk to IRunDriver, so they work for both. Starting a solo run still goes through GameRoot.Runs. See Co-op Run Flow.

Presentation layers added in #40 to #46

These sit on top of the gameplay and never change a score:

  • Cutscenes (Scripts/Intro/): MatchIntroDirector plays the match intro before a run’s first inning, then the batter’s walk-up; OutroDirector plays BOSS DOWN, MAP BEATEN and GAME OVER. Both are components on [Game] (GameRoot.Intro, GameRoot.Outro), have their own GameState (Intro, Outro) and draw through IntroOverlayView / OutroOverlayView with the shared BroadcastDraw kit. See Cutscenes.
  • Hit show (AtBatController.HitShow.cs, HitCounter, HitTally, BallTracer, FlightCamera, HitCounterView): the live distance counter, the tracer, the TV-style flight camera and the big-moment replay. See Hit Show.
  • INDEX screen (IndexCatalog + IndexView, GameState.Index): a list of everything in the game, built from the content. See Index Screen.
  • Swing log (SwingLog on [Game]): every pitch you see appended to swings.csv. See Swing Log.

Assembly definitions

AssemblyFolderReferencesNotes
HomeRunDerbyAssets/_Project/Scripts/Unity.InputSystem, Unity.RenderPipelines.Universal.Runtime, Unity.RenderPipelines.Core.RuntimeRoot namespace HomeRunDerby. Auto-referenced.
HomeRunDerby.EditorAssets/_Project/Editor/HomeRunDerby, URP Runtime, URP EditorRoot namespace HomeRunDerby.EditorTools. Editor platform only. Not auto-referenced.

The Facepunch Steamworks DLLs in Assets/Plugins/Steamworks/ are precompiled plugins, so they need no asmdef reference. The Steam code compiles only with the HOMERUNDERBY_STEAM define (see Build and Release).

Numbers

WhatValueWhere to tune
Unity version6000.6.6f1ProjectSettings/ProjectVersion.txt
URP package17.6.0Packages/manifest.json
Input System package1.19.0Packages/manifest.json
Active input handlingBoth (old Input and Input System)Project Settings > Player
UI reference height720 px (everything scales from it)UITheme.RefHeight

In the code

FileWhat it does
Scripts/Core/GameBootstrap.csResetStatics clears GameEvents; Boot spawns [Game] with a GameRoot.
Scripts/Core/GameRoot.csComposition root. Owns Content, Save, Meta, Leaderboard, Mode, Runs, ActiveCoop, Field, AtBat, Cage, Net, Intro, Outro, CameraRig, Sun.
Scripts/Intro/The match intro and end-of-game cutscenes (Cutscenes).
Scripts/Core/GameEvents.csThe event bus.
Scripts/Run/IRunDriver.csWhat in-run screens talk to (solo or co-op).
Scripts/UI/UIRoot.csDraws the view for the current GameState, owns pause.
Scripts/HomeRunDerby.asmdef, Editor/HomeRunDerby.Editor.asmdefThe two assemblies.

Gotchas

README drift

The README’s diagram predates co-op, the batting cage, Fit Mode, audio and XP. Its folder table leaves out Audio/, FitMode/ and Intro/ (the cutscenes folder from #40). The code is the source of truth; the diagram above is current for 45a2167.

  • GameRoot.IsReady is false after a script recompile during Play mode wipes the services. UIRoot and DevMenu skip drawing then.
  • Services are reached through the GameRoot.Instance singleton, not injected.
  • Resources/ContentDatabase.asset is not in the repo on main, so the game always runs on DefaultContent.