7.5 KiB
ADR-0001: Balancing-Daten als Rust-Konstanten hinter einer Balance-Resource
- Status: Angenommen
- Datum: 2026-08-10
- Betrifft: Wellenkurven, Kosten und Wirkung von In-Run-Upgrades, Workshop-Upgrade-Kurven, Gegner-Stats pro Archetyp, Card-Effekte
Kontext
Der Slice braucht einen Ort für seine Balancing-Zahlen. Die Datenmenge ist klein und bekannt: drei bis vier Gegner-Archetypen, die tragenden Turm-Stats (Damage, Attack Speed, Health, Defense, optional Crit), Workshop-Upgrades mit 20–50 Leveln, Cards mit maximal sieben Stufen, eine Ultimate.
Zwei Größen sind nicht aus dem Referenzspiel ableitbar und müssen von Hand getunt werden:
die HP-/Schadens-Skalierung pro Welle und die Kostenkurven der Upgrades. Das Wiki dokumentiert
beides nicht (siehe docs/WikiData.md). Wie teuer eine Tuning-Iteration ist, ist deshalb das
entscheidende Kriterium.
Gemessen (dieses Repo, examples/perf_spike.rs als Stellvertreter für einen realen
Gameplay-Crate, ~490 Zeilen; Bevy-Dependencies bereits kompiliert im gemeinsamen
CARGO_TARGET_DIR):
| Vorgang | Zeit |
|---|---|
Inkrementeller Rebuild --release (Datei angefasst) |
9–10 s |
Inkrementeller Rebuild dev |
22–26 s |
| Prozessstart bis Fenster und Frames | ~1,4 s |
| Edit-to-see-Loop (Release) | ~11 s |
Der Release-Build ist schneller als der Debug-Build — das Linken der Debug-Info dominiert. Die Annahme „Balancing braucht Release, also ist der Rebuild teuer" ist damit empirisch widerlegt.
Dagegen steht der Rest der Schleife: Eine Wellenkurve bewertet man, indem man von Welle 1 an spielt. Das kostet Minuten, nicht Sekunden. Die ~11 s Rebuild sind ein einstelliger Prozentsatz eines Tuning-Zyklus.
Was Bevy 0.19 mitbringt (geprüft über docs.rs und context7, nicht aus dem Gedächtnis):
- Es gibt keinen eingebauten generischen Loader für strukturierte Daten (RON/TOML/JSON) für
eigene Typen. Ein eigener
AssetLoaderist Pflicht (Vorbild: Bevy-Beispielcustom_asset). - Hot-Reload erfordert das nicht-default Cargo-Feature
file_watcher.AssetPlugin::watch_for_changes_overrideist laut Doku ausdrücklich nicht der empfohlene Weg. ron(0.12.2) undserde(1.0.229) liegen bereits als transitive Dependencies imCargo.lock; sie direkt zu deklarieren kostet keine zusätzliche Compile-Zeit.file_watcherdagegen ziehtnotifyneu ein und funktioniert nur auf Desktop.
Randbedingungen: Desktop nativ zuerst, Mobile und Web später (siehe Slice-Scope in
CONTEXT.md) — auf Wasm gibt es keinen Dateisystem-Watcher, und Assets müssten asynchron über
HTTP geladen werden. Kein Modding-Ziel, keine externen Autoren, ein Entwickler mit Toolchain.
Entscheidung
Die Balancing-Daten leben als Rust-Konstanten in einem eigenen Modul balance, das eine
einzige Balance-Resource befüllt. Gameplay-Systeme lesen ausschließlich Res<Balance> und
sehen die Konstanten nie.
Verbindlich:
balanceenthält nur Daten und Datenstrukturen — keine Gameplay-Logik, keine Bevy-Systeme.Balanceist eineResourceaus reinen Datenstrukturen, so geformt, dass sie ohne Umbau serialisierbar wäre (flache Felder, Zahlen und Vektoren, keineHandle, keineEntity). Einserde-Derive wird jetzt nicht hinzugefügt.- Genau eine Funktion befüllt
Balanceaus den Konstanten. Sie ist die Naht, an der ein späterer Asset-Ladepfad ansetzt. - Kein Gameplay-Code liest eine Konstante aus
balancedirekt. Diese Regel ist die einzige Bedingung, unter der die Entscheidung ihren Wert behält.
Verworfene Alternativen
- Konstanten verstreut im Gameplay-Code — gleiche Iterationskosten wie die gewählte Lösung, aber ein späterer Wechsel auf Assets würde jedes System anfassen, das eine Zahl liest.
- Daten-Assets (RON) über den
AssetServermit Hot-Reload — der Preis (eigenerAssetLoader, direkteserde-/ron-Deps,file_watchermitnotify, ein Lade-Zustand im Start-Ablauf, ausgelieferte Asset-Dateien plus asynchrones Laden auf Web und Mobile) kauft ~11 s pro Tuning-Zyklus, dessen teurer Teil das Spielen ist. Zusätzlich: Eine Wellenkurve lässt sich mitten im Run ohnehin nicht sinnvoll austauschen — die laufende Welle hat ihre Werte bereits vergeben, man startet den Run neu. bevy_common_assetsstatt eigenem Loader (0.17.0 unterstützt bevy 0.19.0) — spart den Loader-Code, ändert aber nichts an den übrigen Kosten und tauscht sie gegen eine Dependency, deren Release-Takt an Bevy-Versionen hängt.- Konfiguration über Umgebungsvariablen (wie in
examples/perf_spike.rs) — trägt für ein paar Spike-Knöpfe, nicht für Tabellen mit 50 Leveln.
Abweichung von der naheliegenden Wahl
Die übliche Antwort in der Spieleentwicklung lautet „Daten raus aus dem Code". Sie stammt aus
Kontexten, die hier nicht gelten: mehrere Designer ohne Compiler, Balancing-Patches ohne
Client-Update, Modding. Der Slice hat keinen davon. Wo die Zahl steht, ist deshalb nicht die
Frage, die die Iterationszeit entscheidet — die entscheidende Frage ist, ob man die Zahl ohne
Neustart ändern und die interessante Welle ohne Vorspielen erreichen kann. Beides adressiert
kein Dateiformat, sondern ein Debug-Panel, das Balance im laufenden Prozess mutiert, plus
einen Sprung-zu-Welle-Pfad. Beides setzt auf Res<Balance> auf, also auf genau die Naht, die
diese ADR festlegt — und beides ist billiger als eine Asset-Pipeline.
Konsequenzen
Positiv
- Keine neuen Dependencies, kein neues Cargo-Feature, kein Lade-Zustand im Start-Ablauf.
- Keine Plattform-API im Spielkern — Mobile und Web erben keine Asset-Auslieferung.
- Falsche Zahlen und fehlende Felder sind Compile-Fehler statt Laufzeit-Parse-Fehler.
- Balancing-Werte sind in die Binary gebacken; ein Save kann nicht auf Werte verweisen, die es im aktuellen Build nicht mehr gibt.
Negativ
- Jede Zahländerung kostet einen Rebuild (~11 s heute, wachsend mit dem Crate).
- Nicht-Entwickler können nicht balancen. Für den Slice irrelevant, für ein späteres Ziel nicht.
- Das Bevy-Asset-System bleibt in diesem Bereich ungelernt. Wenn das ein Lernziel ist, gehört es in ein eigenes Ticket mit einem echten Asset (z. B. Sprites), nicht in die Balancing-Daten.
Folgeaufgaben (nicht Teil dieser ADR)
- Run-Telemetrie (Zeit bis Tod, Coins pro Welle, DPS-Verlauf) — ohne sie beschleunigt ein schnellerer Edit-Loop nur das Raten.
- Debug-Panel und Sprung-zu-Welle-Pfad, beide auf
Res<Balance>.
Revisions-Auslöser
Diese Entscheidung wird neu getroffen, sobald eines der folgenden Ereignisse eintritt:
- Rebuild-Schwelle:
cargo build --releasenach einer Änderung, die nursrc/balance/berührt, dauert wiederholt über 30 s (heute 9–10 s). Nachmessen mit derselben Methode wie oben: Datei anfassen, bauen, drei Läufe, Median. - Zyklus-Schwelle: Eine protokollierte Tuning-Session enthält mehr als 30
Rebuild-Zyklen pro Stunde, deren Diff ausschließlich
src/balance/betrifft. Das ist der Punkt, an dem die 11 s messbar zur dominierenden Kostenstelle werden — vorher nicht. - Scope-Änderung: Balancing-Werte müssen von jemandem geändert werden können, der keine Rust-Toolchain hat, oder ohne neuen Build ausgeliefert werden (Live-Balancing, Modding, externe Tester). Keines davon steht im Slice-Scope.
Auslöser 1 und 2 werden zuerst durch das Debug-Panel beantwortet, nicht durch Assets — es ist die billigere Antwort auf dieselbe Beobachtung. Erst wenn das Panel gebaut ist und die Schwelle weiterhin gerissen wird, ist der Wechsel auf Assets fällig. Auslöser 3 führt direkt zu Assets.