The major systems, how they actually talk to each other, and where the cost is. The Code Inventory lists every class; this page is the argument about how they fit together — which is the thing you cannot scrape.
Launch to a running match. Each arrow is a real scene change or a real call, not an abstraction.
Four mechanisms carry almost all the coupling in this codebase. Knowing which one a dependency uses tells you most of what you need to know about it.
| Mechanism | Reach | What it means in practice |
|---|---|---|
| GameServices | 29 services 103 files |
A static service locator. Nearly every system reaches nearly every other through it, which makes the dependency graph almost fully connected on paper and very hard to reason about. Its saving grace is that every access is null-checked, so a missing service degrades rather than crashes — the same instinct that makes the AI coordinator optional. |
| Static session state | DrillSessionMatchSetup |
Carries choices across scene changes, where nothing else survives. This is the
single most bug-prone mechanism in the project: it has caused a dead Escape key in
Chaos mode and a dead D-pad, both because a static outlived the scene that set it and
nothing clears it. MatchSetup is the corrected pattern — it is consumed,
so it cannot leak into a later match. |
| Scene-tree scans | GetNodesInGroup21 call sites |
How pucks and players are found. Four of those sites sit inside per-frame methods. Correct, and the reason several "which puck?" bugs were possible — scene-tree order is not a gameplay rule, and until recently pickup and steal both treated it as one. |
| Polled input | Input.IsActionPressed |
Distinct from Godot's event pipeline, and the distinction has bitten twice.
SetInputAsHandled() stops an event propagating and does nothing to a
poll, which is why the pause panel flickered open and closed on one press. |
The one place with real numbers, from a
performance audit recorded in the source and in git. ScoringOpportunityManager
rebuilds a grid of the whole ice, and each rebuild does multi-ray raycasts per goal per
cell plus O(cells × players) defender-distance checks.
The debounce (SpatialRebuildDelay,
0.01s in main.tscn) essentially never completes during live play — ten
moving skaters and a puck keep crossing grid cells — so the hard cap
MaximumRebuildDelay is the real governor. It was firing at 0.35s: a full rebuild
~2.86 times a second, continuously. Raised to 0.5s for a ~30% cut, changing
cadence only — no ray count, cell size or scoring weight was touched.
This system already instruments itself:
LastRebuildMilliseconds, RebuildCount and
MergedRebuildRequestCount are live counters. It is the only system that can
currently answer a performance question with data.
Found on 2026-09-24 and worth repeating because
it generalises: the expensive mistake in a per-frame HUD update is not the update count, it is
what the update allocates. An AddThemeStyleboxOverride handed a freshly
constructed StyleBoxFlat every frame is sixty throwaway Godot
Resources a second to recolour one bar. Cache per state, write only on change,
gate the redraw.
These are candidates, not findings. They are reasoned from the code, and none has been profiled. Calling any of them "the bottleneck" without measuring would repeat a mistake this project has already paid for — a rendering bug that took eleven rounds because each round measured a hypothesis instead of the symptom.
Every candidate receiver gets a lane evaluation,
and LaneEvaluator fires a fan of rays per candidate. With a full roster that is
rays × receivers × carriers. A deliberately-deferred caching fix for this is recorded in git —
deferred because caching a pass target would make an in-flight pass aim at a frozen position,
which is a correctness change rather than a tuning one. Any fix here must not freeze the
target.
Four GetNodesInGroup call sites are
inside per-frame methods, and each walks every puck in the scene. Cheap with one puck; Mayhem
allows a hundred. The puck list changes rarely compared to how often it is read, which
is the classic shape for a cached, invalidated collection.
24 _PhysicsProcess and 30
_Process implementations. Many are throttled already —
RefreshGate, DecisionInterval and
MaximumRebuildDelay appear across 24 sites — but the coverage is uneven, and the
project's own UI-08 milestone records that older readouts were never migrated onto the gate.
Not a cost problem today — it is a cost problem waiting. The tactical snapshot is rebuilt per team on a cadence for one puck (see Chapter 32). Making the AI see all twenty would multiply that work, so the fix and the optimisation have to be designed together rather than in sequence.
Godot's Performance singleton,
logging frame time and allocations per session, charted on the dashboard. It is on the roadmap
and not started. Until it exists, the heat-map rebuild is the only cost anybody can speak about
with evidence, and everything above it on this page is an argument rather than a measurement.