champions-of-luneraria/docs/port/U2-WORLD.md
Martin Stickelbroeck 4744a47114 U2 Exporter: pfadneutraler FBX-Kopf (Version 11)
Blenders FBX-Schreiber trug den absoluten Pfad der geöffneten .blend-Datei
in den Dateikopf ein (ApplicationNativeFile, z. B. /Users/martin/Documents/…).
Die Bytes hingen damit vom Ort des Checkouts ab; nach dem Umzug nach
~/Developer galt die byteidentische Reproduzierbarkeit für die eingecheckten
Dateien nicht mehr.

Ab Version 11 steht dort der relative Quellpfad aus dem Manifest (Generatoren:
leer). Geprüft an der Grenzwacht: zwei Läufe byteidentisch, kein absoluter Pfad,
Prüfprotokoll gleich bis auf Version und Größe. Die eingecheckten FBX-Dateien
bleiben Version 10 bis zum nächsten Gesamtexport (spätestens U5); Inhalt
unverändert. Dokumentiert in U2-WORLD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:57:13 +02:00

480 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# U2 · Unity-Spielwelt
Stand: 1. Oktober 2026.
| Teil | Inhalt | Zustand |
| --- | --- | --- |
| U2A | Blender→FBX-Pipeline, sechs repräsentative Assets vollständig geprüft | **fertig** |
| U2B | Weltgeometrie (G1-Karte, Türme, Ancients, Grotte, Aufstellungsbereiche), Materialsystem | **fertig** |
| U2C | Simulation/Darstellung: fester Takt, Interpolation, Bildraten-Unabhängigkeit | **fertig** |
| U2D | spielbare Unity-Partie, Vertical Function Test | **fertig** |
Voraussetzung U1.5 ist erfüllt ([U1.5-UNITY.md](U1.5-UNITY.md)).
## Ziel
Die bestehende Partie läuft sichtbar in Unity (URP). Das ist noch keine grafische Neugestaltung; die kommt erst mit U5.
Zuerst kommen:
- die G1-Drei-Lane-Karte,
- die G2-Türme (Grenzwacht, Bastion, Kernwacht),
- die G2-Ancients,
- die Bossgrotte und die Schlundkrone,
- repräsentative vorhandene Einheiten.
## Vorgaben
- **Adapter:** Unity treibt `LuneMatch` im festen Takt (0,05 s) über einen Akkumulator, unabhängig von der Bildrate. Ansichten lesen nur; sie schreiben nie in die Regeln. Beispiel: `Gebäude-HP = 0` führt zu Einsturz und Ruine, nie umgekehrt eine Animation zu einer Regelwirkung.
- **Gemeinsames Materialsystem** (URP): Stein, Metall, Holz, Vegetation, Kristall, Runen, Wasser. Die G2-Shader aus Godot (stone, metal, crystal, rune, cloth, vein, water_glow, hide) dienen als fachliche Vorlage.
- **Assets:** Quelle sind die Blender-Dateien und Generatoren im Referenz-Repository. Ein zentraler, reproduzierbarer Blender-Export erzeugt FBX; GLB nur, wo keine bearbeitbare Quelle existiert.
- **Reihenfolge:** Erst sechs repräsentative Assets vollständig prüfen, dann die Welt und eine vollständige Bot-Partie. Den Batchlauf über alle Assets gibt es erst nach dem Vertical Function Test.
## U2A · Blender→FBX-Pipeline
### Aufbau
| Datei | Aufgabe |
| --- | --- |
| `tools/blender/export_manifest.json` | Asset-Liste: Blender-Quelle, Art (statisch, starre Teile, Skelett), Exportregeln und Verweis auf den Godot-Manifesteintrag des GLB, das die Referenz benutzt |
| `tools/blender/lune_fbx_export.py` | Blender im Hintergrund: Quelle öffnen, Regeln anwenden, prüfen, FBX schreiben, FBX wieder einlesen und erneut prüfen |
| `tools/blender/export_fbx.sh` | Einstieg; macht die FBX-Dateien byteidentisch reproduzierbar |
| `Assets/Luneraria/Art/<Gruppe>/<id>.fbx` und `<id>.export.json` | Ergebnis: Modell und Protokoll (Blender-Fakten und Prüfungen) |
| `Assets/Luneraria/Editor/Art/LuneModelPostprocessor.cs` | Importregeln in Unity |
| `Assets/Luneraria/Editor/Art/LuneAssetVerifier.cs` | Unity-Prüfung jedes importierten Modells gegen das Blender-Protokoll |
| `Assets/Luneraria/Editor/Art/LuneAssetPreview.cs` | Vorschaubilder in URP |
```bash
sh tools/blender/export_fbx.sh
```
```bash
/Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -executeMethod Luneraria.EditorTools.LuneAssetVerifier.VerifyAll -logFile -
```
Weitere Einstiege: `LuneAssetVerifier.ReimportArt` (nach geänderten Importregeln) und `LuneAssetPreview.RenderAll`.
### Exportregeln
Die Regeln wiederholen, was die Godot-Pipeline beim GLB-Export getan hat. Sonst entstünde ein anderes Modell:
- **Phase-4-Einheiten:** Die Modifikatoren „HIGH RES“ (Unterteilung und Mikrorelief) kamen erst *nach* dem Laufzeitexport in die `.blend`-Datei. Sie werden nicht angewendet (bei `g01`: 36 Stück).
- **Logische Namen:** Die Namen stammen aus den Eigenschaften `part_name` und `material_name`; Blender hatte teils Suffixe wie `.004` ergänzt.
- **Figuren mit Skelett:** Die `.blend`-Datei ist vor dem Zusammenfügen gespeichert. Der Export fügt die Teile wie in Godot zu einem Skin-Mesh zusammen (Funkenlanze: 104 Teile → `FunkenBody`).
- **Achsen:** Die FBX-Datei behält Blenders Z-oben-Achsen; Unity rechnet sie beim Import mit „Bake Axis Conversion“ um, also ohne Wurzelrotation. Blender (x, y, z) wird dabei Unity (x, z, y); die Vorderseite (Blender −Y) läge auf Unity −Z.
- Deshalb dreht der Export jedes Asset vorher um 180° um die Hochachse und wendet die Drehung an. Danach gilt Unity = (−x, y, z) der Godot-/glTF-Koordinaten, und die Vorderseite liegt auf +Z wie in Godot.
- Blenders „Apply Transform“ ist für Skelette als fehlerhaft dokumentiert und wird nicht benutzt.
- **Maßstab und Inhalt:** 1 Blender-Einheit = 1 m; Normalen und lineare Vertexfarben; nur Deform-Bones, keine Leaf-Bones; jede Action wird ein Clip. Tangenten berechnet seit dem Batch-Import Unity mit MikkTSpace, wie Blender; vorher standen sie im FBX.
- **Reproduzierbarkeit:** Blenders FBX-Schreiber stempelt die Uhrzeit ein und leitet Objekt-IDs aus Pythons String-Hash ab. Der Export fixiert beides (feste Uhrzeit, `PYTHONHASHSEED=0` mit `--python-use-system-env`). Zwei Läufe liefern byteidentische FBX-Dateien und identische Protokolle.
### Prüfungen
Blender-Seite, vor und nach dem Schreiben (Rundreise durch einen FBX-Import):
| Prüfpunkt | Wie |
| --- | --- |
| Einheiten, Maßstab | metrisch, `scale_length` 1, keine Objektskalierung, keine negative Skalierung |
| Achsen, Vorderseite | Drehung angewendet; Vorwärtsmarker (Phase 4: Empty `Forward`) liegt in Unity auf +Z |
| Mesh-Namen | eindeutig; gleich den Knoten des Godot-GLB |
| Dreiecke | gleich dem Godot-GLB, auch nach dem Wiedereinlesen |
| Materialslots | keine leeren Slots; Materialien gleich dem Godot-GLB |
| Normalen | Einheitslänge an allen Ecken |
| Tangenten | Voraussetzungen (UVs, keine N-Ecke); Meshes ohne UVs sind vermerkt und werden triplanar oder unbeleuchtet schattiert |
| Maße | Ruhelage gleich den `bounds_godot` des Godot-Manifests (Türme und Ancient: ohne Schadens- und Ruinenteile, wie dort gemessen) |
| Skelett | genau eine Armature, ein Root Bone `Root`, Bone-Namen und -Anzahl wie im Godot-GLB, portable Namen |
| Clips | Namen und Längen wie im Godot-GLB; Pflichtclips vorhanden; Loop-Flag und Kontaktzeit aus den Action-Eigenschaften |
| Blend Shapes | keine in den Quellen |
Unity-Seite (`LuneAssetVerifier`, gegen das Blender-Protokoll):
| Prüfpunkt | Wie |
| --- | --- |
| Importregeln | Maßstab 1, Dateieinheiten, Achsen-Bake, Normalen aus der Datei, Tangenten mit MikkTSpace (seit dem Batch-Import) |
| Achsen | Wurzel und alle Nicht-Bone-Knoten ohne Rotation und Skalierung |
| Mesh-Namen, Dreiecke | Menge und Anzahl je Mesh gleich |
| Maße | aus den Vertices berechnet, Abweichung höchstens 2 mm; gemessen höchstens 0,05 mm |
| Materialslots | Namen je Renderer gleich den Blender-Slots |
| Normalen, Tangenten, UVs, Vertexfarben | vorhanden und gültig, wo Blender sie hat; Tangenten mit w = ±1 |
| Vorderseite | Vorwärtsmarker auf +Z |
| Skelett | Root Bone, Bone-Namen, gültiger Generic-Avatar |
| Clips | Namen, Längen (auf einen halben Frame genau), Loop-Flags, Kontakt-Events (`OnLuneContact`) |
### Ergebnis: sechs repräsentative Assets und drei Szenenbauten
| Asset | vertritt | Dreiecke (= Godot-GLB) | Blender-Prüfungen | Unity-Prüfungen |
| --- | --- | ---: | --- | --- |
| `g01` Kohlenbeißer | normale Einheit, 131 benannte Teile | 79.780 | 18/18 | 14/14 |
| `funkenlanze` | Einheit mit Skelett (Held), 26 Bones, 9 Clips | 24.036 | 26/26 | 18/18 |
| `grenzwacht` | Turm (mit Schadens- und Ruinenteilen) | 6.516 | 18/18 | 13/13 |
| `ancient` Mondherz | Ancient | 19.032 | 18/18 | 13/13 |
| `schlundkrone` | Schlundkrone (Boss), 24 Bones, 11 Clips | 5.712 | 28/28 | 18/18 |
| `map_props` | Vegetation und Kartenrequisiten (Eichen, Kiefern, Sträucher, Felsen, Brücke, Randsteine) | 7.338 | 14/14 | 13/13 |
| `bastion`, `kernwacht`, `bossgrotte` | für die Szene in U2B (Innen- und Basisturm, Mondschlund) | 9.364, 9.224, 15.182 | je 18/18 | je 13/13 |
Summe: 176 Blender-Prüfungen und 128 Unity-Prüfungen, alle bestanden. Protokolle:
- `Assets/Luneraria/Art/*/*.export.json` (Blender).
- `docs/port/logs/u2a/unity-verify.json` (Unity).
Bilder unter `docs/port/screenshots/u2a/`:
- je Asset eine Ansicht von vorn-rechts,
- die Grenzwacht zusätzlich als Ruine,
- Funkenlanze und Schlundkrone zusätzlich im Kontaktframe von `attack_a` (Clips laufen).
### Bekannte Grenzen
- **Materialien:** Die Vorschau benutzt URP-Lit-Materialien, die Unity aus den FBX-Materialbeschreibungen erzeugt (Blender-Palettenfarben). Das eigentliche Materialsystem (Stein, Metall, Kristall, Runen, Stoff, Wasser; Teamfarben; Vertex-AO) folgt in U2B.
- Stoff (Wimpel, Banner) ist einseitig modelliert und braucht dort doppelseitiges Rendern; in der Vorschau fehlt er deshalb.
- **Ohne UVs:** Diese Meshes haben keine Tangenten (Unity meldet das beim Import): Kristalle, Trümmer, Felsen, Kartenrequisiten und einige Phase-4-Kleinteile. Sie brauchen triplanare oder unbeleuchtete Materialien.
- **Starre Einheiten:** Die Phase-4-Einheiten werden in Godot aus benannten Teilen prozedural animiert. Die Teile kommen mit, die Animation folgt mit den Einheitenansichten.
- **Größe:** Die FBX-Dateien liegen direkt im Repository (9 Dateien, 16 MB). Für den Batchlauf über alle Assets ist Git LFS zu erwägen. Entschieden wurde später: Auch der Batch liegt direkt in Git (siehe PORT-STATUS).
### Batch-Import (nach dem Vertical Function Test)
Nach bestandenem Vertical Function Test (U2D) laufen alle Modelle der Referenz durch dieselbe Pipeline. `tools/blender/make_manifest.py` erzeugt das Exportmanifest aus den Godot-Manifesten. Die neun Einträge aus U2A behalten ihre geprüften Regeln.
| Gruppe | Assets | Dreiecke | FBX | Quelle |
| --- | ---: | ---: | ---: | --- |
| Units (Glutpakt, Nietenkonvent, Nebelchor, Wurzelbund, Söldner) | 45 | 1.569.710 | 53,5 MB | `phase4/blend/*.blend` |
| Enemies (Wellengegner e01–e10) | 10 | 474.534 | 16,4 MB | `phase4/blend/*.blend` |
| Props (Arbeiter alt, Teamkerne, Lane-Segment) | 4 | 108.226 | 3,6 MB | `phase4/blend/*.blend` |
| Characters (Ringborke, Arbeiter, Basaltbastion, Funkenlanze, Glasballiste, Quellgeflecht, Schlundkrone) | 7 | 202.880 | 19,1 MB | `.blend` je Figur |
| Peoples (Mensch, Elf, Zwerg, Troll, Fee, Ork, Biest) | 7 | 120.374 | 15,7 MB | `luneraria/peoples/*.blend` |
| Buildings (G2-Türme, Ancient, Bossgrotte; G1-Turm und G1-Grotte) | 7 | 61.854 | 2,3 MB | `luneraria/g2`, `luneraria` |
| Environment (Kartenrequisiten, Grüne Kluft, Menübühne, drei Vergleichsrequisiten) | 6 | 1.664.822 | 37,3 MB | `.blend`; Vergleichsrequisiten nur als Generator |
| **gesamt** | **86** | **4,2 Mio.** | **148 MB** | |
Die Referenz hat 89 Modelle. Die drei übrigen Vergleichs-GLBs sind Dateikopien von Ringborke, Arbeiter und Teamkern 0; sie stehen als Aliase im Manifest statt als doppelte FBX. Alle sieben Völker haben eine `.blend`-Quelle. Die frühere Notiz, Zwerg und Troll fehlten, war ein Lesefehler einer abgeschnittenen Verzeichnisliste.
Neu in der Pipeline:
- **Tangenten:** Unity berechnet sie beim Import mit MikkTSpace (`CalculateMikk`), wie Blender. Die FBX-Dateien enthielten sie zuvor je Ecke in doppelter Genauigkeit; ohne sie sind die Dateien halb so groß. Die Unity-Prüfung „Tangenten, wo UVs“ bleibt.
- **Generatoren ohne `.blend`** (Vergleichsrequisiten): Der Generator läuft in einer temporären Kopie des Referenzaufbaus, damit seine eigenen Ausgaben das Referenz-Repository nicht berühren. Danach wird die Szene exportiert.
- **Laufzeit-Zusammenfassung** der Grünen Kluft wie beim GLB: 2.586 Quellobjekte, nach 16-m-Zelle und Material zu 62 Blöcken zusammengefügt; 1.607.600 Dreiecke wie in Godot.
- **Texturen**, auf die FBX-Materialien verweisen, kommen nach `Art/Textures`: Ringborkes Rinde (Albedo, Normal, Rauigkeit, 4K) und der Kalkstein.
Befunde beim ersten Lauf (71 von 86 grün) und ihre Behandlung:
| Befund | betroffen | Behandlung |
| --- | --- | --- |
| N-Ecken in Meshes mit UVs (Zylinderdeckel) | m01–m10, n07, n08, Grüne Kluft | war nur für Blenders Tangentenexport eine Voraussetzung; Unity trianguliert und berechnet selbst. Jetzt vermerkt statt Fehler |
| Godots Teileliste enthält Ankerobjekte | `map_lane_segment` | Teilenamen werden über Meshes und Empties verglichen |
| Der Arbeiter hat Arbeits- statt Kampfclips | `arbeiter` | Pflichtclips aus seinem eigenen Manifest; die Clipliste stimmt mit Godot überein |
| Null-Normalen an 4 Ecken in 2 Flächen | Grüne Kluft (2 Blöcke) | Artefakt von Blenders Join (die Quellobjekte sind sauber); Blender kann dort keine eigene Normale speichern (entartete Ecke). Explizit erlaubt und vermerkt; Unity verwirft genau diese Dreiecke (4 von 1,6 Mio.); die Kluft wird im Spiel nicht verwendet |
| Unity benennt das einzige Mesh einer Datei nach der Datei | G1-Turm, G1-Grotte, Menübühne, Vergleichsrequisiten | der Verifier vergleicht dort den Mesh-Namen |
Ergebnis:
- 1.616 Blender-Prüfungen und 1.247 Unity-Prüfungen, alle bestanden.
- Zwei vollständige Exportläufe liefern byteidentische FBX-Dateien und Protokolle (172 Dateien).
- Kontaktbögen je Gruppe: `docs/port/screenshots/u2a-batch/<Gruppe>.png`, Reihenfolge in `logs/u2a/sheets.json`. Die Vergleichsrequisiten stehen gedreht wie ihre GLBs; Godots Vergleichsbühne drehte sie.
In der Partie zeigt jede Einheit jetzt ihr eigenes Modell. Die Zuordnung folgt Godots `AssetLibrary`:
- Die Modell-ID ist die ID der Einheitendefinition; Helden und Champions tragen dort ihr Modell.
- Figuren mit Skelett ersetzen das starre Modell derselben ID: w04 Ringborke, w10 Quellgeflecht, g04 Basaltbastion, g05 Funkenlanze, g06 Glasballiste, worker Arbeiter.
- Bosse sind die Schlundkrone.
Starre Modelle werden beim ersten Auftreten zur Laufzeit zu einem Mesh je Material zusammengefasst und zwischengespeichert. Gespeicherte Kombinationsmeshes wären etwa 300 MB YAML gewesen. 14 Animator-Controller tragen die Blender-Clips; Figuren spielen nur Clips, die sie haben.
Die Einheitenbibliothek (`Settings/LuneUnits.asset`) enthält nur die Gruppen, die Einheiten darstellen: Units, Enemies, Characters, Peoples. Das sind 69 Modelle und der Alias `worker`, zusammen 70 Einträge, 20 davon mit Skelett. Gebäude und Umgebung haben eigene Ansichten. Mit allen 86 Modellen landeten auch die Grüne Kluft und die Menübühne im Player (346 statt 228 MB).
#### Partie mit allen Modellen
Die PlayMode-Tests liefen nach dem Batch-Import erneut, jetzt mit den echten Modellen:
| Test | Ergebnis |
| --- | --- |
| Szene bei 30, 60, 120 fps und unbegrenzt (Seed 2) | viermal `DCE95C96556F3A1A`, 12.690 Schritte = Kern |
| Vertical Function Test, Seed 2 ab Runde 1 | Rot gewinnt; 12.690 Schritte, `DCE95C96556F3A1A` = Kern |
| Vertical Function Test, Seed 9 ab Runde 21 | Blau gewinnt; 4.195 Schritte, `7B8E43AEFC0D60C5` = Kern |
Die Modelle ändern also nichts am Spielverlauf. Bilder: `docs/port/screenshots/u2a-batch/01-runde1-aufmarsch.png` bis `14-welle-ende.png`, dieselben Schlüsselmomente wie in U2D. Protokolle: `logs/u2a-batch/vertical-function-test.json` und `scene-frame-rates.json`.
Damit die Nachweise von U2C und U2D erhalten bleiben, liefen die Tests mit `LUNE_EVIDENCE=u2a-batch`:
```bash
LUNE_EVIDENCE=u2a-batch /Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -runTests -testPlatform PlayMode -testResults "$PWD/tools/out/playmode-results.xml" -logFile -
```
#### Befund beim Player-Build: Projekt in iCloud Drive
Der erste Player mit allen Modellen brach beim Start ab: „sharedassets0.assets is corrupted“. Die Ursache ist der Projektort. `~/Documents` wird von iCloud Drive synchronisiert, und ersetzt Unity eine Datei, behält iCloud die alte Fassung als Konfliktkopie `Name 2.ext`.
So kam es zum Fehler:
- Im Build-Cache lag eine alte `Library/Bee/PlayerScriptAssemblies/Luneraria.View 2.dll` neben der aktuellen.
- Der Build las beide in seine Typdatenbank, die alte zuerst (`TypeDB: Class … was already registered`).
- Er schrieb die Einheitenbibliothek deshalb mit dem alten Feldlayout: 260 Byte, ohne Modelle. Der Player liest mit dem neuen Layout und läuft über das Dateiende hinaus.
Insgesamt lagen über 800 solcher Kopien in `Library/`. Einige waren schon committet:
- die doppelten Controller `funkenlanze 2.controller` und `schlundkrone 2.controller` (U2C; jetzt entfernt),
- `World/Generated 2.meta` zu einem leeren Ordner (U2B; jetzt entfernt),
- drei U2D-Benchmarkläufe als `player-benchmark-macos 2.json` bis `4.json`. Sie sind die Rohdaten der U2D-Spannen und heißen jetzt `-run1` bis `-run3` (siehe U2D, Leistung).
Das Git-Repository selbst ist sauber: `git fsck` ohne Befund, keine Kopien in `.git`.
Behandlung:
- `LunePlayerBuild` entfernt solche Kopien vor dem Build aus den Build-Caches. Entstehen während des Builds neue, bricht er mit Fehler ab, statt einen fehlerhaften Player zu liefern.
- Nach den Abstürzen fragte macOS beim nächsten Start, ob die Fenster wiederhergestellt werden sollen, und wartete auf einen Klick. Der Benchmark startet deshalb mit `-ApplePersistenceIgnoreState YES`. Das gilt nur für diesen Start und ändert keine Einstellung.
- Dauerhaft hilft nur ein Projektort außerhalb von iCloud Drive. Deshalb zieht der Arbeitsordner nach `~/Developer/TowerDefense` (Entscheidung vom 1. Oktober 2026, [PORT-STATUS.md](PORT-STATUS.md)).
#### Befund nach dem Umzug: absoluter Pfad im FBX-Kopf
Blenders FBX-Schreiber trägt den absoluten Pfad der geöffneten `.blend`-Datei in den Dateikopf ein (`ApplicationNativeFile`). Deshalb enthalten 83 der 86 FBX-Dateien aus Exporter-Version 10 `/Users/martin/Documents/…`, und ihre Bytes hängen vom Ort des Checkouts ab. Die byteidentische Reproduzierbarkeit galt damit nur am selben Pfad; nach dem Umzug nach `~/Developer` gilt sie für die eingecheckten Dateien nicht mehr.
- **Korrektur:** Ab Exporter-Version 11 steht dort der relative Quellpfad aus dem Manifest, bei Generatoren nichts.
- **Geprüft an der Grenzwacht:** Zwei Läufe sind byteidentisch, es steht kein absoluter Pfad mehr in der Datei, und das Prüfprotokoll ist bis auf Version und Dateigröße gleich (64 Byte kürzer, genau der weggefallene Pfadanfang).
- **Eingecheckte Dateien bleiben Version 10** bis zum nächsten Gesamtexport, spätestens in U5, das alle Modelle neu erzeugt. Ein Neuexport aller Dateien nur für diesen Kopfeintrag hätte das Repository um rund 140 MB vergrößert. Geometrie, Skelette, Clips und Materialien sind nicht betroffen.
#### Leistung mit allen Modellen
Messung wie in U2D (macOS-Player, Apple M5, 1920 × 1080 im Fenster, VSync aus, Seed 9 ab Runde 21), drei Läufe:
| Ansicht | Einheiten | fps (Mittel) | Bildzeit Median | p95 | p99 | Hauptthread (Mittel/p95) | Render-Thread | Warten auf Present (Mittel/p95) |
| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | --- |
| ganze Karte (Zoom 64) | 116 | 158–160 | 4,4–4,6 ms | 20,6–20,8 ms | 24,2–24,5 ms | 2,5–2,6 / 3,7–3,8 ms | 3,1–3,4 ms | 3,7–3,8 / 18,2–18,7 ms |
| Mid-Lane nah (Zoom 22) | 63 | 205–207 | 3,9–4,1 ms | 8,3–8,8 ms | 21,6–22,1 ms | 2,0–2,2 / 2,8–3,0 ms | 2,9–3,0 ms | 2,7–2,8 / 6,5–6,8 ms |
Gegenüber U2D mit dem Platzhaltermodell:
- **CPU:** Haupt- und Render-Thread brauchen etwa doppelt so lange (U2D: 1,3 und 1,3 ms in der Übersicht). Die Ansichten kosten 0,31–0,33 statt 0,12–0,14 ms je Bild, die Simulation 0,05–0,11 ms.
- Vermutete Ursache, nicht gemessen: Statt eines Platzhalter-Meshes für alle starren Einheiten gibt es jetzt viele verschiedene Meshes. Das bedeutet weniger Batching, mehr Draw Calls und mehr Figuren mit Skelett (Animator, Skinning).
- Draw Calls zählt der Release-Player nicht; die Messung gehört zu U5.
- **Ausreißer:** Sie sind weiter Wartezeit beim Present, aber kürzer: p95 des Wartens 18 statt 24 ms (Übersicht), 7 statt 13 ms (Mid-Lane). Die echten Einheiten haben weniger Dreiecke als der Platzhalter (Units im Mittel 35.000 statt 80.000). Das stützt die Vermutung, dass die GPU-Last zu den Ausreißern beiträgt.
- **Garbage Collection:** unverändert (72–73 Sammlungen in 20 s Übersicht).
- **Folgerung für U5:** Zuerst Draw Calls und Batching messen (Development-Player oder Profiler). Wahrscheinliche Hebel sind Materialzusammenlegung oder Atlas, Instancing und LOD; die Dreiecke sind es weniger.
Rohdaten: `logs/u2a-batch/player-benchmark-macos-run1.json` bis `-run3.json`.
## U2B · Weltgeometrie
### Quellen
Die Szene `Assets/Luneraria/Scenes/G1_Map.unity` entsteht reproduzierbar aus drei Quellen, ohne Handarbeit:
| Quelle | liefert | Weg nach Unity |
| --- | --- | --- |
| Godot-Generator der G1-Welt (`lune_world.gd`, `lune_terrain.gd`) | Gelände, Wasser, Lanebeläge, Plattform, Brücke, Randsteine, Basen, Landmarken, Vegetation, Adern, Grottentreppen, Aufstellungsbereiche, Licht | `tools/godot-golden/world_export.gd` backt die Welt nach `Assets/Luneraria/World/Bake/` (JSON und Binärdaten); `LuneWorldBuilder` baut daraus Meshes und Szene |
| Blender-Pipeline (U2A) | Türme, Ancients, Grotte, Requisiten | FBX-Modelle, in der Szene als Prefab-Instanzen |
| C#-Kern | Gebäude-IDs, Positionen, Ausrichtung | `LuneMap` direkt; die Szene enthält keine eigenen Kartendaten |
Warum gebacken statt portiert:
- Gelände und Vegetation hängen an Godots FastNoiseLite und Zufallsgenerator. Ein Nachbau wäre nicht nachweisbar exakt; der Bake ist es.
- Es gibt für diese Welt keine Blender-Quelle; ihre Quelle ist der Generator. Der Bake ist dessen reproduzierbarer Export, kein GLB-Umweg.
- U5 bekommt ohnehin eine neue Weltquelle.
```bash
HEADLESS=0 sh tools/godot-golden/run_godot.sh world_export --out="$PWD/Assets/Luneraria/World/Bake"
```
```bash
/Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -executeMethod Luneraria.EditorTools.LuneWorldBuilder.BuildScene -logFile -
```
Der Bake braucht einen echten Renderer (`HEADLESS=0`). Godots Headless-Renderer liefert für MultiMesh-Instanzen Einheitstransformationen; so lag im ersten Versuch die ganze Vegetation im Kartenmittelpunkt. Das Werkzeug bricht jetzt ab, wenn es so einen Bake erkennt.
### Inhalt
| Teil | Umfang |
| --- | --- |
| Gelände | 121 × 121 Punkte (1 m) plus Bergring 67 × 67 (4 m), Zonenfarbe und Felsanteil als Vertexfarbe, Dreiecksmuster wie in Godot |
| Welt | 59 Knoten: 35 Einzelmeshes, 98 MultiMesh-Instanzen (Randsteine ausgenommen), 532 Requisiten (372 Vegetation und Felsen, 159 Randsteine, Brücke) |
| Bauten | 16 Türme (Grenzwacht, Bastion, Kernwacht), 2 Ancients, Bossgrotte; Positionen und Blickrichtung aus dem Kern; Teammaterialien |
| Aufstellungsbereiche | 3 je Team und Grotteneingänge, ausgeblendet (Planungsphase schaltet sie ein) |
| Höhen | `LuneGround`: Lauf-Höhe (1-m-Raster) und Standhöhe mit Plaza, Stufen und Grottenterrasse (0,5-m-Raster) für Figuren |
| Licht | Sonne und Aufhellung aus dem Bake, flaches Umgebungslicht, prozeduraler Himmel, ACES, Bloom, Kontrast/Sättigung wie `lighting.gd`; kein Nebel (wie in Godot) |
Koordinaten: Unity = (−x, Höhe, y) der Kartenpunkte. Mit dem Wechsel der Händigkeit bleiben Bild und Vorderseiten der Dreiecke erhalten; die Indexfolge bleibt deshalb unverändert. Gemeinsame Umrechnung: `Assets/Luneraria/View/LuneSpace.cs`.
Reproduzierbarkeit:
- Der Bake ist deterministisch (Godot-Generator mit festen Seeds).
- Der Builder aktualisiert Meshes und Materialien an Ort und Stelle; ihre GUIDs bleiben über Builds gleich.
- Nur die Datei-IDs der Szenenobjekte wechseln; das ist bei per Skript erzeugten Szenen unvermeidlich.
### Materialsystem
Alle Materialien verwenden `Luneraria/Surface` (URP, HLSL, SRP-Batcher-fähig) oder `Luneraria/Pad`. Portiert sind:
- das Gelände (`lune_terrain.gdshader`),
- die Kluft-Oberflächen (`sanctuary.gdshader`: Stein, Pflaster, Erde, Rinde, Laub mit Wind),
- der Fluss (`river.gdshader`),
- die Energieadern (`lune_vein.gdshader`),
- die Aufstellungsbereiche (`lune_pad.gdshader`).
Mit dabei sind jeweils triplanarer Kalkstein und Relief aus Bildschirmableitungen, wie in Godot.
Die G2-Oberflächen (`LN_*`) haben Werte und Teamvarianten aus `lune_materials.gd`: Kristall, Rune, Teammetall, Stoff; Blau, Rot und neutral. Vertex-AO wird mit verrechnet. `LuneMaterialSet.Dress` tauscht die Importmaterialien wie Godots `dress` gegen die geteilten Materialien.
Vereinfacht gegenüber Godot, bis U5 die Oberflächen neu macht:
- Quaderfugen der Steinshader, Runenglyphen (stattdessen ein schwach leuchtendes Band), Kristalltiefe, Stoffembleme.
- Beschädigungs- und Einsturzparameter der Materialien.
Befund beim Einrichten: Unity reicht `[HDR]`-Farben unverändert als linear weiter, gewöhnliche Farben konvertiert es aus Gamma. Das war am blassen Kristall zu sehen. Emission wird deshalb als linearer Wert gesetzt (`LuneMaterialFactory.Hdr`).
### Vergleich mit Godot
`LuneWorldCapture.CaptureAll` fotografiert die Szene mit den Kameraeinstellungen der Godot-G2-Aufnahmen (`g2_views.gd`: Fokus, Zoom, Drehung; Kamera wie `lune_camera.gd`). Die Bilder liegen unter `docs/port/screenshots/u2b/`, das Gegenstück unter `../saumwacht-godot/docs/luneraria/screenshots/g2/nachher/`.
| Bild | Befund |
| --- | --- |
| `15-gesamtkarte` | Anordnung gleich: Blau unten links, Rot oben rechts, Grotte links, Kluft von der Quelle unter der Plattform zum Delta mit Brücke; Waldinseln, Randsteine, Mauern, Runensteine an denselben Stellen |
| `01-ancient-100`, `07-basistuerme` | Ancient und Basistürme in gleicher Lage und Ausrichtung, Teamfarben richtig |
| `05-tower1-grenzwacht`, `06-tower2-bastion` | Turm, Tür, Wimpel, Randsteine und Plattformsteine decken sich im Bildausschnitt |
| `10-grotte` | Arena, Tor, Säulen und Treppen gleich; der schlafende Boss und die Grottenzustände kommen mit der Anbindung (U2C/U2D) |
Unterschiede:
- Godot zeigt in allen Bildern das HUD (U3) und in der Planungsphase die Aufstellungsbereiche.
- Die Materialien sind vereinfacht (siehe oben).
- Die Beleuchtung ist ähnlich, aber nicht identisch abgestimmt.
## U2C · Simulation und Darstellung
### Aufbau
| Teil | Aufgabe |
| --- | --- |
| `LuneClock` | fester Takt: Bildzeit geht in einen Akkumulator, heraus kommen ganze Schritte zu 0,05 s (`LuneBalance.SimStep`); `Alpha` ist der Bruchteil des nächsten Schritts |
| `LuneMatchRunner` | Host-Ablauf einer Bot-Partie: Planung (die sechs Bots planen über die Befehlsschnittstelle), Kampf in festen Schritten, Pause nach der Abrechnung, nächste Runde, bis ein Ancient fällt; reine Logik ohne Unity |
| `LuneMatchDriver` | MonoBehaviour: `Update` reicht nur Bildzeit × Spieltempo an den Takt weiter |
| `LuneMatchView` | liest den Kern (nach dem Driver): eine Einheitenansicht je Einheit und Kampf, Gebäudezustände aus den autoritativen HP, Turmblitze aus den Angriffsereignissen der Türme |
| `LuneUnitView` | Position zwischen den letzten beiden Schritten interpoliert, Blickrichtung, Clips aus echten Kampfereignissen, Teamring, Lebensbalken |
| `LuneBuildingView` | fünf Zustände wie in Godot (intakt, leicht, schwer beschädigt, Einsturz, Ruine); fällt ein Gebäude unter den Augen des Betrachters, stürzt es 1,7 s lang ein |
| `LuneTrace` | Fingerabdruck einer Partie (FNV-1a über jeden Schritt und jede Abrechnung) zum Vergleich mit dem reinen Kernlauf |
Regeln bleiben im Kern:
- Die Szene besitzt keine. `Update()` entscheidet nie über den Kampf; die Bildrate bestimmt nur, wie viele Schritte in ein Bild fallen, nie, was ein Schritt tut.
- Ansichten schreiben nichts zurück; eine Animation löst keine Regelwirkung aus.
- Umgekehrt führen Kernzustände zu Darstellung: Gebäude-HP 0 → Einsturz und Ruine; Angriffszähler → Clip und Turmblitz.
### Nachweis Bildraten-Unabhängigkeit
**Logisch** (EditMode, `FrameRateIndependenceTests`): Der Runner bekommt verschiedene Bildzeitfolgen. Jede muss Schritt für Schritt und Runde für Runde den Fingerabdruck des Kernlaufs ergeben (`LuneTrace.RunCore`).
| Bildfolge | Seed 2 (volle Partie) | Seed 9 ab Runde 21 |
| --- | --- | --- |
| 30 fps | 19.315 Bilder, gleich | 6.342 Bilder, gleich |
| 60 fps | 38.597 Bilder, gleich | 12.678 Bilder, gleich |
| 120 fps | 77.169 Bilder, gleich | 25.352 Bilder, gleich |
| unbegrenzt (0,4–3,9 ms, schwankend) | 569.627 Bilder, gleich | 99.090 Bilder, gleich |
| Ruckler (Stillstände bis 0,4 s) | 26.845 Bilder, gleich | 8.796 Bilder, gleich |
| Spieltempo 8× | gleich | – |
Seed 2 ergibt jedes Mal 12.690 Schritte mit Fingerabdruck `DCE95C96556F3A1A`, Seed 9 jedes Mal 4.195 Schritte mit `7B8E43AEFC0D60C5`.
**In der echten Szene** (PlayMode, `SceneFrameRateTests`): Die Partie mit Seed 2 läuft mit allen Ansichten bei `Application.targetFrameRate` 30, 60, 120 und unbegrenzt (VSync aus), bei Spieltempo 24×.
| Ziel | gemessen | Bilder | Schritte je Bild höchstens | Ergebnis |
| --- | ---: | ---: | ---: | --- |
| 30 fps | 30,0 fps | 816 | 17 | gleich dem Kern |
| 60 fps | 60,0 fps | 1.626 | 9 | gleich dem Kern |
| 120 fps | 120,0 fps | 3.227 | 6 | gleich dem Kern |
| unbegrenzt | 4.696 fps | 125.732 | 3 | gleich dem Kern |
Im Batchmodus zeichnet Unity kein Bild auf einen Bildschirm; daher die sehr hohe Rate bei „unbegrenzt“. Die Bildzeiten mit Grafik stehen bei U2D (Player). Protokoll: `logs/u2c/scene-frame-rates.json`.
### Bekannte Grenzen
- **Einheitenmodelle:** In U2C und U2D zeigten Einheiten die repräsentativen Modelle. Seit dem Batch-Import trägt jede Einheit ihr eigenes Modell (siehe U2A, Batch-Import). Teamringe (rund, Raute, Sechseck) und Balken unterscheiden die Seiten wie in Godot.
- **Animation:** Die starren Figuren haben nur Schrittwippen und Angriffsausfall; Godots prozedurale Gliederbewegung aus den benannten Teilen ist noch nicht portiert (die Teile kommen mit den FBX-Dateien, werden aber zur Laufzeit zusammengefasst). Figuren mit Skelett spielen die Blender-Clips (idle, walk, attack_a/b, ability, death).
- **Weitere Vereinfachungen:** Turmblitze sind kurze Linien. Die Einsturzsequenz ist vereinfacht (Kippen und Absinken). Der schlafende Boss zwischen den Kämpfen und die Planungsflächen fehlen noch.
- **Kamera:** wie `lune_camera.gd` (WASD, Mausrad, Q/E, R, O, mittlere Maustaste), vorerst mit dem klassischen Input Manager.
## U2D · Spielbare Unity-Partie
### Szene und Steuerung
`Assets/Luneraria/Scenes/G1_Map.unity`, Play im Editor oder der macOS-Player: Eine Bot-Partie läuft (Standard: Seed 2) mit 3 Lanes, 2 Basen, 16 Türmen, 2 Ancients, Grotte und Aufstellungsbereichen auf dem echten C#-Kern.
| Teil | Funktion |
| --- | --- |
| Kamera | wie Godot: WASD/Pfeile, Mausrad, Q/E, R, O, mittlere Maustaste |
| `LuneDebugOverlay` | Runde, Phase, Türme, Ancients, Einheiten, Helden, Boss, Sieger. Das ist eine vorläufige Anzeige, kein HUD; das G3-HUD wird in U3 portiert, nicht neu erfunden |
| `LuneMatchDriver` | Seed, Startrunde, Fixture-Gold und Spieltempo im Inspector |
Menschliche Befehle (Kaufen, Aufstellen, Bereit) kommen mit dem HUD (U3) und dem Netzwerk (U4); bis dahin planen alle sechs Plätze als Bots über dieselbe Befehlsschnittstelle.
### Vertical Function Test
`VerticalFunctionTests` (PlayMode, Test Runner) spielt beide Partien sichtbar in der Szene. Das Spieltempo ist 6×, die Zielbildrate 60. Ein Kameraregisseur fotografiert die Schlüsselmomente; jede Partie muss den Fingerabdruck des reinen Kernlaufs ergeben.
| Partie | Verlauf | Ergebnis |
| --- | --- | --- |
| Seed 2, ab Runde 1 | siehe unten | Rot gewinnt („Ancient von Blau zerstört.“); 12.690 Schritte, Fingerabdruck `DCE95C96556F3A1A` = Kern |
| Seed 9, ab Runde 21 (Fixture wie das Golden-Szenario `high_wave_21`: Runde 21, je 4.000 Gold) | Wellen über 20 mit bis zu 125 Einheiten und 6 Helden; Runde 21: 3 Rot-Türme fallen; Runde 22: Rot-Turm, beide Rot-Basistürme, ein Blau-Turm und der Rot-Ancient fallen | Blau gewinnt („Ancient von Rot zerstört.“); 4.195 Schritte, `7B8E43AEFC0D60C5` = Kern |
Verlauf der Partie mit Seed 2:
- 9 Runden, bis zu 84 Einheiten.
- Runde 7: erster Turm fällt (`blau_bot_outer`).
- Runde 8: Rot gewinnt den Grottenboss; `rot_top_outer` und `rot_top_inner` fallen.
- Runde 9: Der Boss zieht für Rot über die Bot-Lane. Bis zu 4 Helden kämpfen. Blaus Bot-Innenturm, beide Basistürme und der Ancient fallen.
Kein Bot-Spiel erreicht von Runde 1 aus die Welle 20: Ein Durchlauf über 200 Partien (5 Profile × 40 Seeds) endete immer nach 8 bis 12 Runden. Deshalb zeigt die zweite Partie die hohen Wellen mit derselben öffentlichen Fixture wie die Golden-Referenz.
Bilder (`docs/port/screenshots/u2d/`):
| Bild | zeigt |
| --- | --- |
| `01-runde1-aufmarsch`, `02-runde1-mid-kampf` | Aufmarsch auf drei Lanes, erster Kampf in der Mitte mit Turmblitz |
| `03-turm-faellt`, `04-turm-ruine` | Einsturz eines Turms und seine Ruine (Kamera von der Lane aus) |
| `05-grotte-bosskampf` | Schlundkrone kämpft in der Grotte |
| `06-held-kaempft` | Helden im Kampf |
| `07-boss-auf-der-lane` | eingesetzter Boss mit Rots Armee auf der Deltabrücke |
| `08-ancient-faellt`, `09-ancient-ruine`, `10-ende-uebersicht` | Fall des Blau-Ancient, Ruine, Endstand |
| `11-welle21-uebersicht`, `12-welle21-kampf`, `13-ancient-rot-faellt`, `14-welle-ende` | Welle 21: Übersicht, Massenkampf, Fall des Rot-Ancient |
Protokoll: `logs/u2d/vertical-function-test.json`, mit Runden, Ereignissen, Bosslog und Bildliste.
### Leistung
**Player** (macOS, Mono, Release; Apple M5, Metal; 1920 × 1080 im Fenster, VSync aus): Seed 9 in Runde 21, Spieltempo 1×, gemessen ab 10 s Kampf mit `LuneBenchmark`.
| Ansicht | Einheiten | fps (Mittel) | Bildzeit Median | p95 | Hauptthread (Mittel/p95) | Render-Thread | Warten auf Present (Mittel/p95) |
| --- | ---: | ---: | ---: | ---: | --- | --- | --- |
| ganze Karte (Zoom 64) | 116 | 161–199 | 2,3–2,9 ms | 18–26 ms | 1,3 / 2,0 ms | 1,3 ms | 4,9 / 24 ms |
| Mid-Lane nah (Zoom 22) | 63 | 274–354 | 1,9–2,5 ms | 9–13 ms | 0,9 / 1,3 ms | 0,9 ms | 2,7 / 13 ms |
Die Spannen stammen aus vier Läufen.
Was die Zahlen zeigen:
- Je Bild kosten die Simulation 0,02–0,07 ms und die Ansichten 0,12–0,14 ms.
- Die Ausreißer sind Wartezeit beim Present. Nur 4–27 der langsamen Bilder fielen mit einem Simulationsschritt zusammen.
- Garbage Collection war nicht die Ursache. Die Debug-Anzeige erzeugte zwar Müll in jedem Bild; das ist behoben (78 statt 111 Sammlungen in 20 s), änderte die Ausreißer aber nicht.
- Metal liefert in Unity keine GPU-Zeitstempel. Ob GPU oder Fenster-Compositor wartet, bleibt deshalb offen. Kandidaten:
- die Platzhaltermodelle (80.000 Dreiecke je Einheit, über 9 Millionen Dreiecke in der Übersicht),
- vier Schattenkaskaden bis 190 m,
- der Fenstermodus.
Das gehört zu U5 (LOD, Dreiecksbudget, Vollbild).
Rohdaten: `logs/u2d/player-benchmark-macos.json` (letzter Lauf) und `player-benchmark-macos-run1.json` bis `-run3.json` (die drei früheren Läufe; Lauf 2 vor, Lauf 3 nach der Korrektur der Debug-Anzeige). Die früheren Läufe waren im U2D-Commit als iCloud-Konfliktkopien `player-benchmark-macos 2.json` bis `4.json` gelandet (siehe Batch-Import); sie heißen jetzt nach ihrem Lauf. Windows- und Linux-Player brauchen die fehlenden Build-Module ([U1.5-UNITY.md](U1.5-UNITY.md#fehlende-build-module)).
```bash
/Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -executeMethod Luneraria.EditorTools.LunePlayerBuild.BuildMac -logFile -
```
```bash
Builds/macOS/Luneraria.app/Contents/MacOS/luneraria-unity -ApplePersistenceIgnoreState YES -screen-width 1920 -screen-height 1080 -screen-fullscreen 0 -luneBenchmark "$PWD/tools/out/player-benchmark.json"
```
`-ApplePersistenceIgnoreState YES` verhindert die Wiederherstellungsfrage von macOS nach einem Absturz (siehe Batch-Import).
### Abnahme U2
| Vorgabe | Stand |
| --- | --- |
| Reproduzierbarer Blender→FBX-Export mit Prüfung von Einheiten, Maßstab, Achsen, Vorderseite, Root Bone, Bone-Namen, Mesh-Namen, Materialslots, Normalen, Tangenten, Clips; keine Handarbeit | erfüllt (U2A) |
| Sechs repräsentative Assets vollständig in Unity geprüft, vor dem Batchlauf | erfüllt (U2A); Batchlauf erst nach diesem Test, also jetzt freigegeben |
| Erste spielbare Szene mit echter Karte (3 Lanes, 2 Basen, 16 Türme, 2 Ancients, Grotte, Aufstellungsbereiche), an den echten Kern gebunden | erfüllt (U2B, U2D) |
| Szene besitzt keine Regeln; Zustand → Darstellung, nie umgekehrt | erfüllt (U2C) |
| Fester Takt, `Update()` entscheidet nie über den Kampf, Darstellung interpoliert | erfüllt (U2C) |
| Gleiches Ergebnis bei 30, 60, 120 fps und unbegrenzt | erfüllt, logisch und in der echten Szene (U2C) |
| Vertical Function Test: vollständige Bot-Partie sichtbar, 3 Lanes, Wellen > 20, Türme fallen, Helden kämpfen, Boss nutzbar, Ancient fällt, Partie endet, gleich der Kernpartie | erfüllt; Wellen > 20 über die Golden-Fixture (siehe oben) |
### Bekannte Grenzen
- **Einheitenmodelle:** Der Vertical Function Test lief zuerst mit den repräsentativen Modellen. Nach dem Batch-Import zeigen die Einheiten ihre eigenen Modelle. Der Test lief damit erneut und ergab dieselben Fingerabdrücke; Bilder und Leistung stehen im Abschnitt Batch-Import.
- **Debug-Anzeige statt HUD:** Das G3-HUD folgt in U3. Es gibt noch keine menschlichen Befehle und kein Planungsbild für einen Spieler.
- **Materialien vereinfacht** (siehe U2B); keine Partikeleffekte; Turmblitze sind einfache Linien.
- **Leistungsspitzen beim Present** offen (siehe oben).
## Abnahme (gesamt)
- Die Partie läuft sichtbar.
- Die Kamera entspricht der Godot-Kamera.
- Screenshots neben den Godot-G2-Bildern mit vergleichbarer Kamera.
- Leistung gemessen, einschließlich Mono-Simulationszeit.
- Bekannte Unterschiede dokumentiert.
- Commits U2A bis U2D und der Batch-Import auf `luneraria/u2`.