diff --git a/HANDBUCH.md b/HANDBUCH.md index aea57ce..61c7cef 100644 --- a/HANDBUCH.md +++ b/HANDBUCH.md @@ -71,7 +71,7 @@ Eine vollständige, minimale Vorlage mit jeder Übungsart liegt in [`tooling/fix 1. **Lehrplan entwerfen.** Schreibe zuerst die Liste der Module auf: 10–16 Module in 2–4 Tracks, vom Einstieg bis zum Profi-Niveau. Frage dich bei jedem Modul: *Was kann ich danach, was ich vorher nicht konnte?* Und: *Würde ein Arbeitgeber das erwarten?* 2. **Ordner anlegen:** `content//` mit `area.yaml` (Abschnitt 4). Die Ordner-ID ist kurz, klein und kebab-case, zum Beispiel `go`, `typescript`, `devops`. -3. **Farbe wählen.** Eine kräftige Farbe, auf der schwarzer Text gut lesbar ist (Kontrast ≥ 4,5 : 1 zu `#0B0B0B`). Vorhandene Farben nicht wiederholen: `#A98BFF` Kotlin, `#FF6A3D` Rust, `#FFD400` Linux, `#4D8BFF` Python, `#2BD97C` Automation, `#FF5CA8` Git, `#22D3EE` Data Analysis, `#C6F432` Testing, `#FF9F1C` PostgreSQL, `#F4A3FF` Erzählhandwerk, `#FF4D6D` Combat Design. Gute freie Kandidaten: `#FFC6A8` (Apricot), `#B5E3FF` (Eisblau). +3. **Farbe wählen.** Eine kräftige Farbe, auf der schwarzer Text gut lesbar ist (Kontrast ≥ 4,5 : 1 zu `#0B0B0B`). Vorhandene Farben nicht wiederholen: `#A98BFF` Kotlin, `#FF6A3D` Rust, `#FFD400` Linux, `#4D8BFF` Python, `#2BD97C` Automation, `#FF5CA8` Git, `#22D3EE` Data Analysis, `#C6F432` Testing, `#FF9F1C` PostgreSQL, `#F4A3FF` Erzählhandwerk, `#FF4D6D` Combat Design, `#B5E3FF` Godot. Guter freier Kandidat: `#FFC6A8` (Apricot). 4. **Module schreiben** (Abschnitte 5–7). Ein Modul pro Datei, Dateiname = Modul-ID. 5. **Karten, Projekte, Glossar, Spickzettel, Berufsseite** ergänzen (Abschnitt 8). 6. **Prüfen:** `pnpm content:check `, dann `pnpm verify `, dann `pnpm test` und `pnpm build`. @@ -240,6 +240,20 @@ lesson: | Tabellen (GFM) sind erlaubt und im Spickzettel ausdrücklich erwünscht. +### Bilder + +Screenshots und Abbildungen gehören dorthin, wo Text nicht reicht: ein Editor-Dock, das man wiedererkennen soll, ein Spielbild mit einem Darstellungsfehler, den man diagnostiziert. Sie gehen in Lektionen und in allen Markdown-Feldern der Übungen. + +```markdown +![Inspektor eines Sprite2D mit Filter Linear: die Figur ist verwaschen](/media/godot/pixelraster/filter-linear@2x.png "Godot 4.7.2, Filter Linear, vierfach skaliert") +``` + +- **Ablage:** nur PNG unter `public/media///`. Der Service Worker legt alle PNG-Dateien aus `public/` beim Installieren ab, Bilder gehen also offline. Halte sie klein (Pixelart und Editor-Ausschnitte als PNG mit wenigen Farben, meist unter 100 KB). +- **Alt-Text ist Pflicht** und beschreibt, was zu sehen ist, auch das, worauf es ankommt. Der Titel in Anführungszeichen wird zur Bildunterschrift; nenne dort Quelle und Version. +- **Größe:** Der Build liest Breite und Höhe aus der Datei, damit die Seite beim Laden nicht springt. Eine Datei `…@2x.png` hat doppelt so viele Pixel, wie sie anzeigt, und bleibt auf hochauflösenden Displays scharf. Ein Bild ist nie breiter als die Textspalte; ein Tipp öffnet es in voller Größe zum Zoomen. Gestalte Bilder für Unterwegs-Module deshalb so, dass sie bei 330 px Breite lesbar bleiben: lieber einen Ausschnitt als den ganzen Bildschirm. +- **Prüfung:** `content:check` meldet fehlende Dateien und fehlende Alt-Texte als Fehler. Bilder von außerhalb (`https://…`) sind nicht erlaubt: Sie gingen offline nicht und könnten verschwinden. +- Ein Bild allein in einem Absatz wird eine Abbildung mit Unterschrift; mitten im Text erscheint es als Block ohne Unterschrift. + ## 7. Übungen Gemeinsame Felder aller Übungen: diff --git a/README.md b/README.md index 04f272a..6d0de29 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # learn.kiumu.app -Private Lernplattform für Kotlin & Android, Rust, Linux, Python, Automation mit Python, Git, Data Analysis mit Excel, SQL und Python, Testing & Softwarequalität, SQL & PostgreSQL für Anwendungsentwickler sowie Combat Design & Game Feel mit Godot 4. Live: [learn.kiumu.app](https://learn.kiumu.app) · Öffentliches Repository: [hernstev97/learning-platform](https://github.com/hernstev97/learning-platform) +Private Lernplattform für Kotlin & Android, Rust, Linux, Python, Automation mit Python, Git, Data Analysis mit Excel, SQL und Python, Testing & Softwarequalität, SQL & PostgreSQL für Anwendungsentwickler, Combat Design & Game Feel mit Godot 4 sowie Godot 4 für Gestaltung & 2D-Prototypen. Live: [learn.kiumu.app](https://learn.kiumu.app) · Öffentliches Repository: [hernstev97/learning-platform](https://github.com/hernstev97/learning-platform) Die Plattform ist kein Nachschlagewerk. Jedes Modul besteht aus einer Lektion, die ein Denkmodell aufbaut und auf die offizielle Dokumentation verweist, und aus 10–14 Übungen in fester Reihenfolge: verstehen → vorhersagen → schreiben → Fehler finden → erklären → anwenden. Jeder Bereich hat Interview-Karteikarten mit Wiederholung, Projekte und ein Abschlussprojekt, das wie eine Take-Home-Aufgabe im Bewerbungsprozess geschnitten ist. diff --git a/content/combat/modules/frust-diagnose.yaml b/content/combat/modules/frust-diagnose.yaml index d5a6e8a..1ced70d 100644 --- a/content/combat/modules/frust-diagnose.yaml +++ b/content/combat/modules/frust-diagnose.yaml @@ -117,7 +117,7 @@ lesson: | - **HitLog** mit Frame-Nummer für jeden Druck, jeden Zustandswechsel und jeden Treffer. - **Pause und Einzelbild**: F9 hält das Spiel im Game-Fenster an, F10 rückt einen Frame vor ([Game embedding](https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#frame-advance-f10)). - - **Kollisionsformen sichtbar** im Menü *Debug* ([Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes)). + - **Sichtbare Collision-Shapes** im Menü *Debuggen* ([Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes)). - **Movie Maker**: `--write-movie` nimmt mit fester Bildrate auf, als PNG wird jedes Bild eine Datei ([Creating movies](https://docs.godotengine.org/en/stable/tutorials/animation/creating_movies.html)). So siehst du einen Lauf später Bild für Bild an. Dein eigener Blick reicht nicht. Du kennst jeden Telegraph, also kannst du nicht prüfen, ob jemand ihn beim ersten Mal liest. Mike Ambinder rät für Valve, darauf zu achten, „what people do – not what they say“ ([Valve's Approach to Playtesting, PDF](https://cdn.fastly.steamstatic.com/apps/valve/2009/GDC2009_ValvesApproachToPlaytesting.pdf)). Eduardo Gorinstein, Combat Designer von Hades, sagt: „When I watch people play the game, they approach the fight in different ways“ ([DigiPen](https://digipen.edu/showcase/news/eduardo-gorinstein-delivers-challenge-and-fun-hades)). diff --git a/content/combat/modules/godot-start.yaml b/content/combat/modules/godot-start.yaml index b058c35..d33540b 100644 --- a/content/combat/modules/godot-start.yaml +++ b/content/combat/modules/godot-start.yaml @@ -104,7 +104,7 @@ lesson: | ## Eine Figur, die in alle Richtungen läuft - Zuerst die Aktionen. Unter *Projekteinstellungen → Eingabezuordnung* (Input Map) legst du `move_left`, `move_right`, `move_up`, `move_down`, `attack` und `dodge` an und gibst jeder Taste und Gamepad-Eingabe: `move_right` bekommt etwa `D`, die Pfeiltaste rechts und *Linker Stick, X-Achse +*. Der Code fragt nur Aktionen ab, nie Tasten. Jede Aktion hat eine **Deadzone**, Standard 0,2: Stick-Ausschläge darunter zählen als 0 ([InputMap](https://docs.godotengine.org/en/stable/classes/class_inputmap.html#class-inputmap-method-add-action), [Input examples](https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html)). + Zuerst die Aktionen. Unter *Projekteinstellungen → Eingabe-Zuordnung* (Input Map) legst du `move_left`, `move_right`, `move_up`, `move_down`, `attack` und `dodge` an und gibst jeder Taste und Gamepad-Eingabe: `move_right` bekommt etwa `D`, die Pfeiltaste rechts und *Linker Stick, X-Achse +*. Der Code fragt nur Aktionen ab, nie Tasten. Jede Aktion hat eine **Deadzone**, Standard 0,2: Stick-Ausschläge darunter zählen als 0 ([InputMap](https://docs.godotengine.org/en/stable/classes/class_inputmap.html#class-inputmap-method-add-action), [Input examples](https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html)). Dann das Skript der Figur: @@ -177,7 +177,7 @@ lesson: | ## Messen statt raten: Kollisionsformen, Einzelbild und Frame-Nummern - Drei Werkzeuge reichen für den Anfang. Im Menü *Debug* schaltest du **Kollisionsformen sichtbar** an; beim nächsten Start zeichnet Godot jede aktive Form farbig ([Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes)). Im laufenden Spiel hält **Pause** (F9) alles an, und **Nächster Frame** (F10) rückt genau einen Frame vor ([Game embedding](https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#frame-advance-f10)). So siehst du Bild für Bild, wann die Hitbox erscheint. Und du schreibst in die Ausgabe, in welchem Physik-Frame etwas passiert: + Drei Werkzeuge reichen für den Anfang. Im Menü *Debuggen* schaltest du **Sichtbare Collision-Shapes** an; beim nächsten Start zeichnet Godot jede aktive Form farbig ([Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes)). Im laufenden Spiel hält **Pause** (F9) alles an, und **Nächster Frame** (F10) rückt genau einen Frame vor ([Game embedding](https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#frame-advance-f10)). So siehst du Bild für Bild, wann die Hitbox erscheint. Und du schreibst in die Ausgabe, in welchem Physik-Frame etwas passiert: ```gdscript func _on_hitbox_area_entered(area: Area2D) -> void: diff --git a/content/combat/modules/hitboxen.yaml b/content/combat/modules/hitboxen.yaml index 25fb2d1..bb6f61e 100644 --- a/content/combat/modules/hitboxen.yaml +++ b/content/combat/modules/hitboxen.yaml @@ -148,9 +148,9 @@ lesson: | ## Kollisionsebenen und Debug-Anzeige - Damit eine Hitbox nur die richtigen Flächen findet, liegt jede Fläche auf einer Ebene und scannt bestimmte andere. Die Godot-Doku fasst die Regel so: „Object A can detect a contact with object B only if object B is in any of the layers that object A scans.“ ([Collision layers and masks](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks)). In der Testszene liegt die Hurtbox der Figur auf Ebene 2, die des Gegners auf Ebene 3. Die Hitbox der Figur scannt Ebene 3, die des Gegners Ebene 2. So trifft kein Angriff die eigene Figur. Die Einrichtung Schritt für Schritt steht in [Testszene: Angriff mit Frame-Daten](/combat/testszene-angriff). Dreamscapers getrennte Prüfung für Gegner, Umgebung und Zerstörbares ließe sich in Godot mit einer Fläche je Ebene nachbauen. + Damit eine Hitbox nur die richtigen Flächen findet, liegt jede Fläche auf einer Ebene und scannt bestimmte andere. Die Godot-Doku fasst die Regel so: „Object A can detect a contact with object B only if object B is in any of the layers that object A scans.“ ([CollisionObject2D.collision_layer](https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-property-collision-layer)). In der Testszene liegt die Hurtbox der Figur auf Ebene 2, die des Gegners auf Ebene 3. Die Hitbox der Figur scannt Ebene 3, die des Gegners Ebene 2. So trifft kein Angriff die eigene Figur. Die Einrichtung Schritt für Schritt steht in [Testszene: Angriff mit Frame-Daten](/combat/testszene-angriff). Dreamscapers getrennte Prüfung für Gegner, Umgebung und Zerstörbares ließe sich in Godot mit einer Fläche je Ebene nachbauen. - Sehen kannst du die Flächen im fertigen Spiel nicht. Manche Trainingsmodi von Kampfspielen zeigen sie, Hitboxen fast immer rot und Hurtboxen grün (Infil, [Hitbox](https://glossary.infil.net/?t=Hitbox)). In Godot schaltest du im Menü *Debug* **Kollisionsformen sichtbar** an ([Visible collision shapes](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes)). Mit Pause (F9) und Nächster Frame (F10) gehst du dann Bild für Bild durch einen strittigen Treffer und siehst gedrehte Hitboxen im aktiven Frame. Prüf am Frame-Zähler der Debug-Anzeige, wie weit ein Schritt geht. + Sehen kannst du die Flächen im fertigen Spiel nicht. Manche Trainingsmodi von Kampfspielen zeigen sie, Hitboxen fast immer rot und Hurtboxen grün (Infil, [Hitbox](https://glossary.infil.net/?t=Hitbox)). In Godot schaltest du im Menü *Debuggen* **Sichtbare Collision-Shapes** an ([Visible collision shapes](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes)). Mit Pause (F9) und Nächster Frame (F10) gehst du dann Bild für Bild durch einen strittigen Treffer und siehst gedrehte Hitboxen im aktiven Frame. Prüf am Frame-Zähler der Debug-Anzeige, wie weit ein Schritt geht. > [!PRAXIS] > Ist ein Treffer strittig, schau dir zuerst den aktiven Frame mit sichtbaren Formen an und ändere erst danach etwas. Ein Treffer ohne Kontakt im Bild kann gewollt sein: Dreamscaper nimmt ihn bei Spielerangriffen bewusst in Kauf und hält die Gegner-Hitboxen präzise. Schreib in jede Vorgabe für einen Coding-Agenten Form, Größe und Lage jeder Fläche relativ zur Figurenmitte und die Ebenen, die sie scannt. diff --git a/content/combat/modules/testszene-angriff.yaml b/content/combat/modules/testszene-angriff.yaml index 5fe1056..6c37203 100644 --- a/content/combat/modules/testszene-angriff.yaml +++ b/content/combat/modules/testszene-angriff.yaml @@ -188,7 +188,7 @@ lesson: | return get_parent().take_hit(attack, source, self) ``` - Damit nur gewollte Paare sich finden, benennst du unter *Projekteinstellungen → Ebenennamen → 2D-Physik* drei Ebenen und setzt Ebene (`collision_layer`) und Maske (`collision_mask`) so ([Collision layers and masks](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks)): + Damit nur gewollte Paare sich finden, benennst du unter *Projekteinstellungen → Ebenen-Namen → 2D-Physik* drei Ebenen und setzt Ebene (`collision_layer`) und Maske (`collision_mask`) so ([Collision layers and masks](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks)): | Node | Ebene | Maske | Einstellung | | --- | --- | --- | --- | @@ -383,7 +383,7 @@ lesson: | text = "%s Frame %d Puffer: %s\n%s" % [player.state_name(), player.frame, buffer, "\n".join(HitLog.lines.slice(-3))] ``` - Zusammen mit *Debug → Kollisionsformen sichtbar* und dem Einzelbild-Schritt (F10) siehst du jetzt bei jedem Treffer, welcher Zustand, welcher Frame und welcher Puffer im Spiel war. + Zusammen mit *Debuggen → Sichtbare Collision-Shapes* und dem Einzelbild-Schritt (F10) siehst du jetzt bei jedem Treffer, welcher Zustand, welcher Frame und welcher Puffer im Spiel war. ### Eingaben nach Drehbuch diff --git a/content/combat/projects.yaml b/content/combat/projects.yaml index 8b16ba0..682eaf5 100644 --- a/content/combat/projects.yaml +++ b/content/combat/projects.yaml @@ -58,7 +58,7 @@ projects: detail: Spielfigur mit `player.gd`, Dummy mit `enemy.gd`, drei benannte Ebenen, Hitbox und Hurtbox nach der Tabelle aus der Lektion. - id: log-und-anzeige title: HitLog und Debug-Anzeige - detail: Autoload `HitLog`, Debug-Label im `CanvasLayer`, Kollisionsformen sichtbar. Jeder Tastendruck und jeder Zustandswechsel steht mit Frame-Nummer in der Ausgabe. + detail: Autoload `HitLog`, Debug-Label im `CanvasLayer`, „Sichtbare Collision-Shapes“ an. Jeder Tastendruck und jeder Zustandswechsel steht mit Frame-Nummer in der Ausgabe. - id: referenz-angriff title: Referenzlauf Angriff detail: Mit den Platzhalterwerten einmal treffen. Die Abstände im Log müssen der Lektion entsprechen (8 Frames bis `ACTIVE`, Treffer einen Frame danach, `RECOVERY` 7 Frames nach `ACTIVE`, `MOVE` 14 danach). diff --git a/content/godot/area.yaml b/content/godot/area.yaml new file mode 100644 index 0000000..8335907 --- /dev/null +++ b/content/godot/area.yaml @@ -0,0 +1,34 @@ +id: godot +title: Godot 4 für Gestaltung & 2D-Prototypen +short: Godot +tagline: Ein Projekt, das eine KI gebaut hat, im Editor lesen, mit eigenen Assets füllen und abstimmen. +description: > + Editor, Szenen, Nodes, Ressourcen und Instanzen, eigene Sprites, Animationen, Tiles und Sounds, + Kollisionen, Eingabeaktionen und Parameter, Bildrate und Simulation, Debugger, Profiler und Export – + mit Godot 4.7. Code und technisches Gerüst darf eine KI liefern; du liest es, stellst es ein und + triffst jede gestalterische Entscheidung selbst. Wo es passt, knüpft der Kurs an deine + Erfahrung aus der Webentwicklung an. Module mit der Marke „Unterwegs“ gehen ganz am Handy, auch offline, mit Screenshots aus Editor + und Spiel. „Am PC“ braucht Godot am Rechner; lesen kannst du diese Lektionen auch unterwegs. +color: "#B5E3FF" +order: 12 +outcomes: + - "Du **liest ein fremdes Godot-Projekt**: Hauptszene, Szenenbaum, Instanzen, Ressourcen, Signale und Autoloads, im Editor und zur Laufzeit im Remote-Baum." + - Du **importierst eigene Pixelart** scharf und im richtigen Raster, zerlegst Spritesheets, setzt Ursprünge und ordnest Animationen und Töne zu. + - Du erklärst, was zwischen **Spielauflösung und Bildschirm** passiert, und diagnostizierst unscharfe, ungleichmäßige oder zitternde Pixel an Screenshots. + - Du stellst **Kollisionen, Layer und Masken, Eingabeaktionen und exportierte Parameter** ein und sagst vorher, was eine Änderung bewirkt. + - Du baust **2D-Level mit TileMapLayer** und Y-Sortierung aus eigenen Tiles. + - Du **grenzt Probleme ein** – Bildrate oder Simulation, Import, Kollision, Skalierung – mit Debugger, Monitoren und Profiler. + - Du **exportierst Testversionen** und sicherst Projekt und Originaldateien nachvollziehbar in Git. +tracks: + - title: Den Editor lesen + description: Docks und Projektordner, Szenenbäume, Instanzen und Ressourcen. Am PC erkundest du ein Projekt und baust das Testgelände auf, an dem der Kurs arbeitet. + modules: [editor-anatomie, szenenbaum, instanzen-ressourcen, projekt-erkunden] + - title: Eigene Assets + description: Pixelraster und Skalierung verstehen, dann eigene Sprites importieren, Animationen und Töne zuordnen. + modules: [pixelraster, sprites-importieren, animationen, audio] + - title: Welt und Systeme abstimmen + description: Kollisionen und Masken, Eingabeaktionen und Parameter, Level aus Tiles. + modules: [kollision-ebenen, aktionen-parameter, level-tilemap] + - title: Messen und ausliefern + description: Bildrate und Simulation, Fehler eingrenzen mit Debugger und Profiler, Testversionen exportieren und sichern, zum Schluss vorhersagen, ändern und messen im eigenen Testgelände. + modules: [bildrate-simulation, fehler-eingrenzen, export-sichern, werkstatt-messen] diff --git a/content/godot/career.md b/content/godot/career.md new file mode 100644 index 0000000..7c52f39 --- /dev/null +++ b/content/godot/career.md @@ -0,0 +1,94 @@ +## Godot-Wissen im Beruf + +Dein Anlass für diesen Kurs ist dein eigenes Spiel. Die Fertigkeit, um die es geht, ist trotzdem eine eigene Rolle in Teams: Systeme, die jemand anderes gebaut hat, im Editor lesen, mit Assets füllen, einstellen und Fehler eingrenzen. In großen Studios teilen sich das Technical Artists, Technical Designer und Level Designer. In kleinen Teams und bei Solo-Projekten macht es dieselbe Person, die auch zeichnet und entwirft. Mit Coding-Agenten verschiebt sich die Arbeit weiter in diese Richtung: Der Code entsteht schneller, aber jemand muss ihn im Editor prüfen, mit eigenen Assets verbinden und abstimmen. + +Diese Seite zeigt, wo das Wissen aus dem Kurs in der Praxis vorkommt und wie du es belegst. Sie ist keine Arbeitsmarktstudie und nennt keine Zahlen zu Stellen. + +## Rollen, in denen Editor-Arbeit zählt + +Titel bedeuten in jedem Studio etwas anderes. Lies die Aufgaben, nicht nur die Überschrift. + +| Rolle | Arbeit im Editor | Module, die dazu passen | +| --- | --- | --- | +| Technical Artist | Assets ins Spiel bringen: Import, Raster, Animation, Shader, Werkzeuge für Zeichner; Darstellungsfehler finden | Pixelraster, Sprites importieren, Animationen, Fehler eingrenzen | +| Technical Designer | Mechaniken im Editor zusammensetzen und abstimmen, exportierte Werte und Ressourcen pflegen, kleine Skripte lesen und ändern | Instanzen und Ressourcen, Eingabeaktionen und Parameter, Werkstatt | +| Level Designer | Level aus Tiles und Szenen bauen, Kollision, Zeichenreihenfolge, Laufwege messen | Kollisionen, Level aus Tiles, Szenenbäume | +| Game Designer in kleinen Teams | Prototypen bauen lassen und selbst prüfen, Werte ändern, Testversionen verteilen | Projekt erkunden, Werkstatt, Export | +| Gameplay Programmer | Code schreiben, aber auch Szenen und Ressourcen so anlegen, dass andere sie im Editor bedienen können | Szenenbaum, Instanzen und Ressourcen, Bildrate und Simulation | +| QA und Test | Fehler reproduzierbar machen, eingrenzen, mit Log und Screenshot beschreiben | Fehler eingrenzen, Bildrate und Simulation | + +## Spiele, die mit Godot entstanden sind + +Studios veröffentlichen kommerzielle Spiele mit Godot, viele davon in 2D und Pixelart. Eine Auswahl aus dem offiziellen [Showcase](https://godotengine.org/showcase/), jeweils mit der Seite dort: + +- [Slay the Spire 2](https://godotengine.org/showcase/slay-the-spire-2/) von Mega Crit, Early Access seit März 2026. +- [Dome Keeper](https://godotengine.org/showcase/dome-keeper/) von Bippinbits, Pixelart. +- [Brotato](https://godotengine.org/showcase/brotato/) von Blobfish. +- [Halls of Torment](https://godotengine.org/showcase/halls-of-torment/) von Chasing Carrots. +- [Cassette Beasts](https://godotengine.org/showcase/cassette-beasts/) von Bytten Studio. +- [The Case of the Golden Idol](https://godotengine.org/showcase/case-of-the-golden-idol/) von Color Gray Games. +- [Until Then](https://godotengine.org/showcase/until-then/) von Polychroma Games. +- [DOGWALK](https://godotengine.org/showcase/dogwalk/) vom Blender Studio. + +Spiel eines davon mit dem Blick aus diesem Kurs: Wo siehst du ganzzahlige Skalierung, wo Y-Sortierung, wo Ränder bei einem anderen Seitenverhältnis? + +## Was Teams erwarten + +Wer im Editor arbeitet, an dessen Arbeit hängen andere. Daraus folgen Erwartungen, die nicht an Godot gebunden sind: + +- **Fremde Projekte lesen.** Du findest dich in einer Szene zurecht, die du nicht gebaut hast, und änderst sie, ohne etwas anderes zu zerbrechen. +- **Saubere Assets.** Was du importierst, ist scharf, sitzt im Raster, hat den richtigen Ursprung und folgt den Einstellungen, die das Team festgelegt hat. +- **Werte an einer Stelle.** Du weißt, wo ein Wert lebt und welcher gilt, und du legst neue Werte so an, dass andere sie finden. +- **Reproduzierbare Fehlerberichte.** Symptom, Schritte, erwartetes und tatsächliches Verhalten, Version, Log. „Fühlt sich komisch an“ ist der Anfang einer Untersuchung, kein Bericht. +- **Saubere Historie.** Kleine Commits mit sprechender Nachricht, Originaldateien im Repository, keine Builds und kein Cache. Die Grundlagen dazu stehen im [Git-Bereich](/git). + +## Probeaufgaben und Gespräche + +Für Rollen nahe am Editor kann statt eines Programmiertests eine Probeaufgabe kommen: ein kleines Projekt, in das du Assets einbaust, ein Level, das du erweiterst, oder ein Prototyp mit Fehlern, die du finden sollst. Ob du den Weg dorthin nachvollziehbar aufschreibst, sieht man dabei sofort. Das [Abschlussprojekt](/godot/projekte) ist so geschnitten. + +Fragen, die sich aus dem Stoff dieses Kurses ergeben und die du in eigenen Worten beantworten können solltest: + +- Ein Sprite ist im Editor scharf und im Spiel unscharf. Wie gehst du vor? +- Zwei Gegner teilen sich eine Kollisionsform, und eine Änderung wirkt auf beide. Warum, und wie verhinderst du das? +- Ein Wert steht im Skript, in der Szene und in einer Ressource. Welcher gilt? +- Auf einem 144-Hz-Monitor ruckelt die Figur, auf 60 Hz nicht. Woran denkst du zuerst? +- Was gehört von einem Godot-Projekt ins Repository und was nicht? + +Die Interview-Karten dieses Bereichs üben solche Fragen mit Wiederholung. + +## Portfolio: was du zeigen kannst + +- **Ein kurzes Video** des Testgeländes mit eigenen Assets und sichtbaren Kollisionsformen. Zwei Minuten reichen. +- **`KORREKTUREN.md`** mit zwei ausgewählten Fehlern von Symptom bis Beleg. Das zeigt mehr als jede Liste von Werkzeugen. +- **`ASSETS.md`** mit deiner Pipeline und einem Commit, in dem eine Änderung am Original ohne Handarbeit im Spiel ankommt. +- **Eine Projektkarte** eines fremden Projekts, etwa der offiziellen JRPG-Demo. + +Zeig deine eigene Gestaltung, wenn du sie zeigen willst. Für die Arbeit, um die es hier geht, zählt der nachvollziehbare Weg. + +## Lernplan in Wochen + +Bei etwa fünf Stunden pro Woche, davon die Hälfte unterwegs: + +| Woche | Unterwegs | Am PC | +| --- | --- | --- | +| 1 | Der Editor als Werkstatt, Szenenbäume lesen | Testgelände aufbauen | +| 2 | Instanzen und Ressourcen | Projektkarte der JRPG-Demo | +| 3 | Pixelraster und Skalierung | Eigene Sprites importieren | +| 4 | Kollisionen und Trefferzonen | Animationen zuordnen | +| 5 | Eingabeaktionen und Parameter | Audio zuordnen | +| 6 | Bildrate und Simulation | Level aus Tiles | +| 7 | Fehler eingrenzen | Testversionen exportieren und sichern | +| 8 | Karten wiederholen | Werkstatt und Fehlerkiste | +| 9–12 | Karten wiederholen | Abschlussprojekt | + +Die Reihenfolge ist ein Vorschlag. Wenn dein Spiel gerade eine Frage aufwirft, geh zu dem Modul, das sie beantwortet. + +## Ressourcen + +- [Godot-Dokumentation: Step by step](https://docs.godotengine.org/en/stable/getting_started/step_by_step/index.html) – die offiziellen Grundlagen. +- [Godot-Dokumentation: Editor](https://docs.godotengine.org/en/stable/tutorials/editor/index.html) – Aufbau und Bedienung des Editors. +- [Godot-Dokumentation: Project organization](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html) und [Version control systems](https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html). +- [Godot-Demoprojekte](https://github.com/godotengine/godot-demo-projects) – offizielle kleine Projekte für Godot 4.7, gut zum Lesen fremder Projekte. +- [Godot Asset Store](https://store.godotengine.org/) – Erweiterungen und Vorlagen, im Editor über den Bildschirm „Asset Store“. +- [Art Design Deep Dive: Dead Cells](https://www.gamedeveloper.com/production/art-design-deep-dive-using-a-3d-pipeline-for-2d-animation-in-i-dead-cells-i-) – Thomas Vasseur (Motion Twin, 2018) beschreibt, wie das Studio Pixelart-Animationen aus 3D-Modellen rendert: eine andere Engine, dieselbe Frage nach Pipeline und Raster. +- [games-career.com](https://www.games-career.com/) – Jobportal der Spielebranche, vor allem mit Stellen in Deutschland. diff --git a/content/godot/cheatsheet.md b/content/godot/cheatsheet.md new file mode 100644 index 0000000..0b00606 --- /dev/null +++ b/content/godot/cheatsheet.md @@ -0,0 +1,153 @@ +## Projektordner (Godot 4.7) + +| Datei oder Ordner | Inhalt | In Git? | +| --- | --- | --- | +| `project.godot` | Projekteinstellungen, Hauptszene, Autoloads, Eingabeaktionen, Layer-Namen; nur Abweichungen vom Standard | ja | +| `*.tscn` | Szene als Text: Ressourcen, Nodes, Verbindungen | ja | +| `*.tres` | Ressource als Text, etwa Werte einer eigenen `Resource` | ja | +| `*.gd` | GDScript | ja | +| `*.import` | Import-Einstellungen einer Quelldatei (Bild, Ton) | ja | +| `*.uid` | eindeutige ID eines Skripts oder Shaders (seit 4.4) | ja | +| `export_presets.cfg` | Exportvorlagen-Einstellungen je Plattform | ja | +| `.godot/` | Cache, importierte Dateien; entsteht neu | nein | +| Originale (`.aseprite`, `.kra`, `.rpp`) | außerhalb von `res://` oder in einem Ordner mit `.gdignore` | ja | + +`res://` ist der Projektordner, `user://` der Ordner für Spielstände und Logs (unter Linux in `~/.local/share/godot/app_userdata//`). + +## Welches Dock beantwortet welche Frage? + +| Frage | Ort im Editor | +| --- | --- | +| Woraus besteht die Szene? | Dock **Szene** | +| Welche Werte hat dieser Node, welche weichen vom Standard ab? | **Inspektor**, Zurücksetzen-Pfeil | +| Wer reagiert auf dieses Ereignis? | Dock **Signale** | +| Wie wird diese Datei importiert? | Dock **Importieren** | +| Was läuft gerade im Spiel? | Dock Szene → **Remote** | +| Was ging schief? | unteres Panel **Ausgabe** und **Debugger** | + +## Wo ein Wert lebt + +Jede Stufe überschreibt die vorige; es gilt der Wert der untersten Stufe, die ihn setzt: + +1. Standard im Skript (`@export var max_speed := 90.0`) +2. Wert in der Szene des Nodes (`player.tscn`) +3. Override an der Instanz (`main.tscn`, Zurücksetzen-Pfeil im Inspektor) +4. Änderung zur Laufzeit (Code, Remote-Inspektor; nach dem Beenden weg) + +Werte in einer `.tres`-Ressource gelten für jeden Node, der dieselbe Ressource benutzt. + +## Pixelraster: ganzzahlige Faktoren + +Faktor = kleinere der beiden ganzzahligen Teilungen; der Rest wird Rand. + +| Bildschirm | 320 × 180 | 640 × 360 | +| --- | --- | --- | +| 1280 × 720 | 4×, kein Rand | 2×, kein Rand | +| 1280 × 800 (Steam Deck) | 4× = 1280 × 720, oben und unten je 40 px | 2× = 1280 × 720, je 40 px | +| 1366 × 768 | 4× = 1280 × 720, je 43 px links/rechts, 24 px oben/unten | 2×, gleiche Ränder | +| 1920 × 1080 | 6×, kein Rand | 3×, kein Rand | +| 2560 × 1440 | 8×, kein Rand | 4×, kein Rand | +| 3840 × 2160 | 12×, kein Rand | 6×, kein Rand | + +Die beiden Auflösungen sind Beispiele, keine Empfehlung. + +## Pixelart scharf halten + +| Einstellung | Wert | Wirkung | +| --- | --- | --- | +| `display/window/stretch/mode` | `viewport` | erst klein zeichnen, dann als Ganzes vergrößern | +| `display/window/stretch/scale_mode` | `integer` | nur ganze Faktoren, gleich große Pixel | +| `rendering/textures/canvas_textures/default_texture_filter` | `Nearest` (`0`) | kein Verwischen beim Vergrößern | +| `rendering/2d/snap/snap_2d_transforms_to_pixel` | an | Zeichnen auf ganzen Pixeln, Position bleibt Kommazahl | + +Einrichten Schritt für Schritt: [Godot-Start im Combat-Kurs](/combat/godot-start). + +## Layer und Masken + +Layer = wo ich bin. Maske = wen ich suche. Im Inspektor ab 1 gezählt, im Code als Bitwert. + +| Layer | 1 | 2 | 3 | 4 | 5 | +| --- | --- | --- | --- | --- | --- | +| Bitwert | 1 | 2 | 4 | 8 | 16 | + +Maske mit Layer 1 und 3 = 1 + 4 = `5`. Eine `Area2D` meldet einen Körper, wenn ihre Maske dessen Layer enthält und `monitoring` an ist. + +Testgelände: `world` = 1, `player` = 2, `pickups` = 3. + +## Bewegung im Testgelände rechnen + +Beschleunigung mit `move_toward`, 60 Physik-Frames pro Sekunde. Platzhalterwerte 90 / 600 / 900, gemessen mit Godot 4.7.2: + +| Größe | Rechnung | Ergebnis | +| --- | --- | --- | +| Tempozuwachs je Frame | `acceleration / 60` | 10 px/s | +| Frames bis Höchsttempo | `max_speed / (acceleration / 60)` | 9 | +| Tempoverlust je Frame | `friction / 60` | 15 px/s | +| Frames bis Stillstand | `max_speed / (friction / 60)` | 6 | +| Bremsweg | Summe der Tempi nach jedem Frame / 60 | 3,75 px | + +## Lautstärke + +| dB | Amplitude | +| --- | --- | +| 0 | 1 (unverändert) | +| −6 | ≈ 0,5 | +| −12 | ≈ 0,25 | +| −20 | 0,1 | +| −80 | 0,0001, praktisch still | + +`linear_to_db(0.5)` ≈ −6,02, `db_to_linear(-6)` ≈ 0,501. + +## Import für Pixelart (Standardwerte für PNG in 4.7) + +| Import-Dock | Englisch in der Doku | Standard | Wirkung | +| --- | --- | --- | --- | +| Komprimieren → Modus | Compress → Mode | Lossless | jedes Pixel exakt; VRAM Compressed macht Flecken und verschiebt Farben | +| Mipmaps → Erzeugen | Mipmaps → Generate | aus | an: rund 33 % mehr Speicher, nützt nur bei starkem Herauszoomen | +| Prozessierung → Alpharand beheben | Fix Alpha Border | an | gegen dunkle Säume an transparenten Kanten, wenn linear gefiltert wird | +| Prozessierung → Größenbeschränkung | Size Limit | 0 (aus) | größer als 0 verkleinert beim Import, die Pixel verwaschen | + +Eine Vorgabe „2D Pixel“ gibt es in Godot 4 nicht; der Texturfilter ist keine Importeinstellung mehr, sondern eine Projekt- und Node-Einstellung. + +Die Einstellungen stehen in `.png.import`; die Quelle bleibt unverändert. Ein `Sprite2D` setzt den Ursprung mit `centered` und `offset`; für Y-Sortierung gehört er an die Füße. + +## Zwei Takte + +| | `_process(delta)` | `_physics_process(delta)` | +| --- | --- | --- | +| Läuft | einmal pro gezeichnetem Bild | in festen Physik-Ticks | +| Takt | hängt an Monitor, VSync, Last | `physics/common/physics_ticks_per_second`, Standard 60 | +| `delta` | schwankt | fest, bei 60 Ticks 1/60 s | +| Gehört hinein | Darstellung, Kamera, UI | Bewegung, Kollision, Gameplay-Timing | + +- Frame-Budget: 1000 / 60 = 16,67 ms, bei 144 Hz 6,94 ms. +- `physics/common/max_physics_steps_per_frame` = 8: Unter 60 / 8 = 7,5 Bildern pro Sekunde holt die Physik nicht mehr auf, das ganze Spiel läuft langsamer. +- `Engine.time_scale` streckt `delta`, nicht die Zahl der Ticks. Zum Beobachten gut, zum Anhalten nicht. + +## Fehlermeldungen lesen + +| Meldung beginnt mit | Bedeutet | +| --- | --- | +| `ERROR:` | Die Engine meldet; die Zeile `at:` zeigt in Godots C++-Quelltext. | +| `SCRIPT ERROR:` | Eine Zeile deines GDScript ließ sich nicht ausführen. | +| `GDScript backtrace` | Aufrufkette; `[0]` ist die Zeile, an der es passiert ist. | + +Die erste Meldung zuerst beheben und neu starten; spätere sind oft Folgen. + +| Meldung (gekürzt) | Ursache | +| --- | --- | +| `The InputMap action "move_left" doesn't exist.` | Aktion fehlt in der Eingabe-Zuordnung | +| `Can't change this state while flushing queries.` | Form im Physik-Signal direkt abgeschaltet; `set_deferred()` nehmen | +| `… on a base object of type 'Nil'` | Variable ohne Wert, oft ein falscher Node-Pfad | + +## Exportieren + +```console +$ godot --headless --path . --export-release "Linux" build/linux/testgelaende.x86_64 +$ godot --headless --path . --export-debug "Linux" build/linux/testgelaende.x86_64 +``` + +- Exportvorlagen müssen genau zur Editor-Version passen (4.7.2 zu 4.7.2). +- Der Preset-Name muss exakt stimmen, der Zielordner muss existieren; relative Pfade gelten ab dem Projektordner. +- `export_presets.cfg` gehört ins Repository, Geheimnisse liegen in `.godot/export_credentials.cfg`. +- Ablauf und `.gitignore` im Detail: [Testversionen exportieren und sichern](/godot/export-sichern), Git-Grundlagen im [Git-Bereich](/git/git-einstieg). diff --git a/content/godot/glossary.yaml b/content/godot/glossary.yaml new file mode 100644 index 0000000..bc9c186 --- /dev/null +++ b/content/godot/glossary.yaml @@ -0,0 +1,150 @@ +terms: + - term: Node + definition: Baustein eines Godot-Spiels mit einer Aufgabe, etwa `Polygon2D` zum Zeichnen oder `Camera2D` für den Bildausschnitt. Nodes hängen in einem Baum, jeder hat höchstens einen Elternknoten. + - term: Szene + definition: Ein gespeicherter Baum aus Nodes mit genau einer Wurzel, als Datei `.tscn`. Eine Szene lässt sich in andere Szenen einsetzen, ähnlich einer Komponente im Web. + - term: Instanz + definition: Eine eingesetzte Szene. Sie übernimmt alles aus ihrer Vorlage und speichert in der Eltern-Szene nur ihre Abweichungen; im Szenen-Dock trägt sie eine Filmklappe. + - term: Geerbte Szene + definition: Eine Szene, deren Wurzel eine Instanz einer Basisszene ist, angelegt mit *Szene → Neue geerbte Szene …*. Sie übernimmt Änderungen der Basis und speichert nur ihre eigenen Abweichungen. + - term: Override + definition: "Ein Wert, den eine Instanz abweichend von ihrer Vorlage setzt. Er steht als Zeile unter dem Abschnitt der Instanz in der Eltern-Szene, etwa `value = 3`, und der Inspektor zeigt daneben den Zurücksetzen-Pfeil." + - term: Ressource + definition: Ein Datenobjekt wie eine Textur, eine Kollisionsform oder eigene Werte (`extends Resource`). Ressourcen werden per Verweis geteilt; eine Kopie entsteht erst mit Einzigartig machen, `resource_local_to_scene` oder `duplicate()`. + - term: ".tscn" + definition: Textformat für Szenen. Es enthält Kopf, externe Ressourcen (`[ext_resource]`), eingebettete Ressourcen (`[sub_resource]`), Nodes (`[node]`) und Signalverbindungen (`[connection]`) und speichert nur Werte, die vom Standard abweichen. + - term: ".tres" + definition: Textformat für eine einzelne Ressource, etwa `data/player_stats.tres`. Wie in `.tscn` stehen darin nur Werte, die vom Standard des Skripts oder der Klasse abweichen. + - term: "res://" + definition: Pfad zum Projektordner, also zum Ordner mit `project.godot`. Im exportierten Spiel ist er nur lesbar. + - term: "user://" + definition: Beschreibbarer Ordner für Spielstände, Einstellungen und Protokolle. Unter Linux standardmäßig `~/.local/share/godot/app_userdata//`. + - term: Remote-Baum + definition: Ansicht des Szenen-Docks, während das Spiel läuft (Knopf *Remote*). Sie zeigt alle Nodes des laufenden Spiels; Änderungen im Remote-Inspektor gehen beim Beenden verloren. + - term: Autoload + definition: "Ein Skript oder eine Szene, die Godot vor der Hauptszene als Node in den Baum hängt, eingetragen unter `[autoload]` in `project.godot`. Ist die Spalte Enable an, ist der Node im Code über seinen Namen erreichbar." + - term: Signal + definition: Ein Ereignis, das ein Node sendet (`collected.emit(total)`) und mit Methoden anderer Nodes verbunden wird, im Code oder als `[connection]` in der Szene. Das Signale-Dock zeigt die Verbindungen des gewählten Nodes. + - term: Gruppe + definition: Ein Etikett für Nodes, ähnlich einer CSS-Klasse. Code findet alle Mitglieder mit `get_tree().get_nodes_in_group()`; in der `.tscn` steht die Gruppe als `groups=[…]` in der Kopfzeile des Nodes. + - term: NodePath + definition: Pfad von einem Node zu einem anderen, relativ wie `"../HUD/Counter"` oder absolut ab `/root`. `$Counter` ist die Kurzform von `get_node("Counter")`. + - term: Eindeutiger Name (%) + definition: Markierung eines Nodes mit „Über einzigartigen Namen zugreifen“. Skripte derselben Szene erreichen ihn dann mit `%Name`, auch wenn sich sein Pfad innerhalb der Szene ändert. + - term: Spielauflösung + definition: Die Größe des Bildes, in dem das Spiel rechnet und zeichnet, in Godot `display/window/size/viewport_width` und `viewport_height`. Im Testgelände 320 × 180 als Platzhalter; welche Auflösung ein Spiel bekommt, hängt an seiner Pixelart. + - term: Stretch Mode + definition: "Projekteinstellung `display/window/stretch/mode`: wie das Spielbild an die Fenstergröße angepasst wird. `viewport` zeichnet klein und vergrößert das fertige Bild, `canvas_items` zeichnet jeden Node in Fenstergröße, `disabled` skaliert nicht. Neue Projekte starten in Godot 4.7 mit `canvas_items`." + - term: Ganzzahlige Skalierung + definition: "Scale Mode `integer`: Godot rundet den Vergrößerungsfaktor ab, damit jedes Spielpixel gleich viele Bildschirmpixel breit ist. Der Rest des Fensters bleibt schwarz, etwa 43 und 24 Pixel bei 320 × 180 auf 1366 × 768." + - term: Texturfilter + definition: "Bestimmt, welche Farbe beim Vergrößern zwischen Texturpixeln entsteht: `Nearest` nimmt das nächste Pixel, `Linear` mischt die vier nächsten. Projektweit in `rendering/textures/canvas_textures/default_texture_filter` (0 = Nearest), am Node in `texture_filter` (1 = Nearest, 2 = Linear)." + - term: Subpixel + definition: Eine Position zwischen zwei ganzen Pixeln, etwa x = 144,25. Ohne Pixel-Snap im Modus `canvas_items` zeichnet Godot Sprites an solchen Positionen, sie gleiten dann in Bruchteilen eines Spielpixels über das Raster. + - term: Mixel + definition: Pixel unterschiedlicher Größe im selben Bild, etwa ein Sprite mit `scale` 2 neben einem mit `scale` 1 oder gedrehte Pixelart, die feiner gerastert ist als ihre Umgebung. + - term: ".import-Datei" + definition: Textdatei neben jeder importierten Quelle, etwa `test_sheet.png.import`. Sie hält die Import-Einstellungen und den Pfad zum Ergebnis in `.godot/imported` und gehört ins Repository. + - term: Spritesheet + definition: Ein Bild mit mehreren gleich großen Feldern, meist den Frames einer Animation. Ein `Sprite2D` teilt es mit `hframes` und `vframes`, SpriteFrames holt Felder über den Dialog *Frames auswählen*. + - term: SpriteFrames + definition: Ressource mit benannten Animationen für `AnimatedSprite2D`. Jede Animation hat Frames, eine Geschwindigkeit in FPS (Standard 5), einen Loop-Modus und je Frame eine relative Dauer. + - term: AnimatedSprite2D + definition: Node, der Animationen aus SpriteFrames abspielt. `play()` mit demselben Namen lässt sie weiterlaufen, ein neuer Name beginnt bei Frame 0. Sendet `frame_changed`, `animation_looped` und, ohne Loop, `animation_finished`. + - term: AnimationPlayer + definition: Node, der Eigenschaften beliebiger Nodes zu festen Zeiten setzt, Methoden aufruft und Töne startet. Mit `callback_mode_process` auf Physics läuft er im Physik-Takt, nötig für Hitboxen und anderes spielmechanisches Timing. + - term: Spur (Track) + definition: Eine Zeile in einer Animation des `AnimationPlayer`, etwa Property Track, Call Method Track oder Audio Playback Track. Die Spurtypen heißen auch im deutschen Editor englisch. + - term: Audiobus + definition: Kanal, über den Ton zum Lautsprecher läuft, mit Lautstärkeregler, Effekten und Pegelanzeige im Panel *Audio*. Busse leiten nach links bis `Master`; Player finden ihren Bus über den Namen. + - term: Dezibel (dB) + definition: Logarithmisches Verhältnis. Für die Amplitude gilt dB = 20 × log10(Verhältnis); −6 dB halbieren die Amplitude, −20 dB teilen sie durch zehn. Umrechnen mit `linear_to_db()` und `db_to_linear()`. + - term: AudioStreamPlayer2D + definition: "Player mit Position, der mit dem Abstand zur Bildschirmmitte leiser wird: Faktor `(1 − Abstand / max_distance) ^ attenuation`, Standard 2000 Pixel und 1,0. Verteilt den Ton nach der Seite auf links und rechts." + - term: Collision Layer + definition: | + Die Ebenen, auf denen ein Kollisionsobjekt liegt (`collision_layer`), 32 Stück. Im Inspektor ab 1 gezählt, in `.tscn` und Code als Bitwert gespeichert: Ebene *n* hat den Wert 2^(*n*−1), Ebene 3 ist also 4. + - term: Collision Mask + definition: | + Die Ebenen, die ein Kollisionsobjekt absucht (`collision_mask`). Erkannt oder blockiert wird nur, wenn die Maske dessen, der sucht, den Layer des anderen enthält. Einzelne Ebenen setzt `set_collision_mask_value(nummer, true)`. + - term: CharacterBody2D + definition: | + Körper, den dein Code bewegt, meist über `velocity` und `move_and_slide()`. Er bleibt an Körpern hängen, deren Layer in seiner Maske liegt; in der Draufsicht steht `motion_mode` auf Floating. + - term: StaticBody2D + definition: | + Körper, den die Physik nicht bewegt, etwa Wände und Kisten. Versetzt ihn Code, springt er an die neue Stelle, ohne andere Körper mitzuschieben; dafür gibt es `AnimatableBody2D`. + - term: Area2D + definition: | + Fläche, die Überlappungen meldet und nichts blockiert, über `body_entered` und `area_entered`. `monitoring` heißt „ich melde“, `monitorable` „andere Areas dürfen mich finden“. + - term: Aktion (Input Action) + definition: | + Benannte Eingabe wie `move_right`, die der Code statt einer Taste abfragt, etwa mit `Input.is_action_just_pressed()`. Welche Tasten und Achsen sie auslösen, legt die Input Map fest. + - term: Physical Keycode + definition: | + Tastenposition auf einer US-QWERTY-Tastatur (`physical_keycode`), laut Doku für Spieleingaben wie WASD gedacht. In der Eingabe-Zuordnung mit „(Physisch)“ markiert; auf QWERTZ liegt das physische Z dort, wo Y aufgedruckt ist. + - term: "`@export`" + definition: | + GDScript-Annotation, die eine Variable im Inspektor zeigt und ihren Wert in Szene oder Ressource speichert. Godot setzt gespeicherte Werte nach `_init()` und vor `_ready()`; Werte gleich dem Standard stehen nicht in der Datei. + - term: Export-Hint + definition: | + Zusatz zu einem Export, der die Anzeige im Inspektor ändert, etwa `@export_range(0, 400, 1, "suffix:px/s")`, `@export_enum`, `@export_group` oder `@export_file`. Hints begrenzen nur die Eingabe im Editor, nicht den Code. + - term: Eigene Ressource (Custom Resource) + definition: | + Skript mit `extends Resource` und `class_name`, dessen exportierte Werte als `.tres` gespeichert werden, im Testgelände `MovementStats` in `player_stats.tres`. Alle Nodes, die dieselbe Datei benutzen, teilen die Werte. + - term: TileSet + definition: | + Ressource, die Tiles beschreibt: Tilegröße, Quellen wie ein Atlas aus einem Bild, Physik-, Terrain- und Navigationsebenen. Als `.tres` gespeichert teilen es mehrere `TileMapLayer` und Level. + - term: TileMapLayer + definition: | + Node, der in jeder Zelle seines Rasters höchstens eine Tile aus einem TileSet zeigt. Seit Godot 4.3 nimmt man je Ebene einen eigenen Layer statt des veralteten `TileMap`; die Zellen stehen binär in `tile_map_data`. + - term: Terrain + definition: | + Zuordnung im TileSet, welche Ecken und Kanten einer Tile zu welchem Untergrund gehören. Damit wählt der TileMap-Editor beim Malen passende Übergänge selbst aus (Autotiling). + - term: Y-Sortierung + definition: | + Zeichenreihenfolge nach der y-Position bei Kindern eines Nodes mit `y_sort_enabled`; weiter unten wird später, also obendrauf gezeichnet. Figuren werden an ihrem Ursprung sortiert, Tiles an Zellenmitte plus `y_sort_origin`. + - term: Physik-Tick + definition: | + Ein Schritt der Simulation im festen Takt, standardmäßig 60 pro Sekunde (`physics/common/physics_ticks_per_second`). Pro Tick ruft Godot `_physics_process` aller Nodes auf und rechnet danach die Physik; wie viele Ticks auf ein gezeichnetes Bild fallen, hängt von der Bildrate ab. + - term: "`delta`" + definition: | + Der Parameter von `_process` und `_physics_process`: die Spielzeit seit dem letzten Aufruf in Sekunden. In `_process` schwankt er mit der Bildrate, in `_physics_process` ist er fest, standardmäßig 1/60. `Engine.time_scale` streckt ihn. + - term: "`_process`" + definition: | + Virtuelle Methode, die Godot einmal pro gezeichnetem Bild aufruft. Für alles, was nur die Darstellung betrifft; Gameplay-Timing hängt dort an der Bildrate. Entspricht grob `requestAnimationFrame` im Browser. + - term: "`_physics_process`" + definition: | + Virtuelle Methode, die Godot in jedem Physik-Tick vor dem Physik-Schritt aufruft. Für Bewegung mit Kollision, Frame-Zähler und alles, was auf jedem Rechner gleich lange dauern muss. + - term: Physik-Interpolation + definition: | + Zeichnet Objekte zwischen den Ständen der letzten beiden Ticks, damit Bewegung bei Bildraten über dem Physik-Takt gleichmäßig wirkt; in 2D seit Godot 4.3. Kostet ein bis zwei Ticks Rückstand und wirkt nur auf Bewegung im Physik-Takt. Nach einem Teleport `reset_physics_interpolation()` aufrufen. + - term: Frame-Zeit + definition: | + Die Zeit für ein gezeichnetes Bild. Das Budget ist 1000 / Bildrate Millisekunden, also 16,67 ms bei 60 und 6,94 ms bei 144 Bildern pro Sekunde. Der Monitor „Prozessierung“ zeigt den längsten Durchlauf der letzten Sekunde, mit VSync einschließlich der Wartezeit auf den Monitor. + - term: VSync + definition: | + Vertikale Synchronisation: Godot wartet mit jedem Bild auf den Monitor, die Bildrate übersteigt dessen Bildwiederholrate nicht. Einstellung *Anzeige → Fenster → V-Sync-Modus* (`display/window/vsync/vsync_mode`), Standard `Enabled`; `--disable-vsync` schaltet sie für einen Lauf ab. + - term: Haltepunkt + definition: | + Stelle im Code, an der das Spiel anhält, damit du Aufrufkette und Variablen im Stacktrace prüfen kannst. Gesetzt per Klick links neben die Zeilennummer oder mit dem Schlüsselwort `breakpoint`, das mit dem Skript gespeichert wird. + - term: Monitor + definition: | + Messwert im Reiter Monitore des Debuggers, etwa FPS, Prozessierung, Nodes oder Verwaiste Nodes, als Graph über die Zeit. Der Editor fragt die Werte einmal pro Sekunde ab. Eigene Monitore legt `Performance.add_custom_monitor()` an. + - term: Profiler + definition: | + Reiter im Debugger, der nach *Start* die Zeit je Funktion und Frame misst. „Gesamt“ zählt aufgerufene Funktionen mit, „Eigen“ nur den eigenen Code. Der Visuelle Profiler misst dagegen nur das Zeichnen auf CPU und GPU. + - term: Orphan Node + definition: | + Deutsch im Editor „verwaister Node“: ein Node, der existiert, aber in keinem Szenenbaum hängt, etwa nach `remove_child()` ohne `queue_free()`. Steigt der Monitor „Verwaiste Nodes“, verliert ein Skript Nodes; `Node.print_orphan_nodes()` listet sie. + - term: Exportvorlage + definition: | + Vorgebautes Godot ohne Editor für eine Zielplattform, das mit der PCK eines Projekts zum Spiel wird. Muss exakt zur Editor-Version passen und liegt unter `export_templates//` in den Editordaten; installiert über *Editor → Exportvorlagen verwalten …*. + - term: Export-Preset + definition: | + Gespeicherte Export-Einstellung für eine Plattform, etwa „Linux“ oder „Windows Desktop“, mit Exportpfad, Optionen und Ressourcenauswahl. Steht in `export_presets.cfg` im Projektordner und gehört ins Repository; Geheimnisse liegen getrennt in `.godot/export_credentials.cfg`. + - term: PCK + definition: | + Godots Paketformat für die Daten eines exportierten Projekts: Szenen, Ressourcen, vorverarbeitete Skripte. Liegt neben dem Programm oder ist mit *PCK einbetten* in ihm enthalten; fehlt sie, startet das Programm mit „Is the .pck file missing?“ nicht. + - term: "`.uid`-Datei" + definition: | + Datei neben einem Skript oder Shader (seit Godot 4.4) mit einer eindeutigen ID wie `uid://4pgkkb82h0e6`, über die Szenen auf die Datei verweisen, auch wenn sie verschoben wird. Muss ins Repository, sonst brechen die Verweise auf anderen Rechnern. diff --git a/content/godot/interview.yaml b/content/godot/interview.yaml new file mode 100644 index 0000000..35a2244 --- /dev/null +++ b/content/godot/interview.yaml @@ -0,0 +1,337 @@ +cards: + - id: editor-anatomie-szenen-in-git + module: editor-anatomie + q: Warum lassen sich Godot-Projekte gut mit Git versionieren, und was gehört nicht hinein? + a: | + Szenen (`.tscn`), Ressourcen (`.tres`), Skripte und `project.godot` sind Textdateien, und Godot speichert darin nur Werte, die vom Standard abweichen. Stellt jemand im Inspektor `max_speed` auf 120, zeigt `git diff` genau eine neue Zeile `max_speed = 120.0`. Die `.uid`-Dateien neben Skripten gehören mit ins Repository, `.godot/` nicht: Das ist ein Cache, den Godot beim Öffnen neu erzeugt. + tags: [editor, git] + level: 1 + - id: editor-anatomie-res-user + module: editor-anatomie + q: Was ist der Unterschied zwischen `res://` und `user://`? + a: | + `res://` ist der Projektordner mit allen Szenen, Skripten und Assets; im exportierten Spiel ist er nur lesbar. `user://` ist ein beschreibbarer Ordner für Spielstände, Einstellungen und Protokolle, unter Linux `~/.local/share/godot/app_userdata//`. Wer einen Spielstand nach `res://` schreibt, merkt den Fehler erst im exportierten Spiel. + tags: [editor, dateien] + level: 1 + - id: editor-anatomie-abweichungen-finden + module: editor-anatomie + q: Wie erkennst du in einer fremden Szene, welche Werte jemand bewusst eingestellt hat? + a: | + Am Zurücksetzen-Pfeil im Inspektor: Er steht nur neben Werten, die vom Standard abweichen. Standard ist der Wert der Klasse oder des Skripts, bei einer Instanz der Wert aus ihrer Vorlage. Das Werkzeugmenü bietet „Nicht-Default expandieren“, das nur die Abschnitte mit Abweichungen aufklappt. Im Text sehe ich dasselbe: Jede Eigenschaftszeile in der `.tscn` ist eine Abweichung, alles andere ist Standard. + tags: [editor, inspektor] + level: 1 + - id: szenenbaum-zeichenreihenfolge + module: szenenbaum + q: Wie entscheidet Godot in 2D, was vorn gezeichnet wird? + a: | + Grundregel ist die Baumreihenfolge: Spätere Geschwister liegen über früheren, Kinder über ihren Eltern. `z_index` verschiebt Nodes in höhere oder tiefere Ebenen. Ist an einem Node `y_sort_enabled` an, sortiert Godot seine Kinder mit gleichem `z_index` nach der y-Position ihres Ursprungs, so wie es eine Draufsicht braucht. Ein `CanvasLayer` mit höherem `layer` liegt über allem und folgt der Kamera nicht, deshalb kommt das HUD dort hinein. + tags: [szenenbaum, zeichnen] + level: 1 + - id: szenenbaum-pfade-brechen + module: szenenbaum + q: Ein Kollege benennt einen Node um, und das Spiel meldet „Node not found“. Was ist passiert, und wie verhinderst du das? + a: | + Ein Skript greift mit einem festen Pfad wie `$Counter` auf den Node zu, und Godot passt Skripte beim Umbenennen nicht an. Robuster ist ein szeneneindeutiger Name: Der Node wird als „Über einzigartigen Namen zugreifen“ markiert, und das Skript schreibt `%Counter`; der Pfad darf sich dann innerhalb der Szene ändern. Signalverbindungen schreibt der Editor beim Speichern selbst neu, solange man im Szenen-Dock umbaut. Wer die `.tscn` von Hand ändert, verliert sie dagegen ohne Meldung. + tags: [szenenbaum, fehlersuche] + level: 2 + - id: szenenbaum-ready-reihenfolge + module: szenenbaum + q: In welcher Reihenfolge laufen `_ready()` und `_process()` in einem Szenenbaum? + a: | + `_ready()` läuft bei den Kindern zuerst und beim Elternknoten zuletzt, damit der Eltern-Node in `_ready()` auf fertige Kinder zugreifen kann. `_process()` läuft dagegen in Baumreihenfolge von oben nach unten, bei gleicher `process_priority`. Im Testgelände heißt das: `_ready` bei Player, Crate, Objects, HUD, Main, `_process` bei Main, Objects, Player, Crate, HUD. + tags: [szenenbaum, lebenszyklus] + level: 2 + - id: instanzen-override-speichern + module: instanzen-ressourcen + q: Was speichert Godot, wenn du eine Eigenschaft an einer Instanz änderst, und welcher Wert gilt am Ende? + a: | + Die Eltern-Szene bekommt unter dem Abschnitt der Instanz eine Zeile mit der Abweichung, etwa `value = 3`; die Vorlage bleibt unverändert. Es gilt der Standard im Skript, darüber der Wert der Vorlage, darüber der Override der Instanz und zuletzt, was Code zur Laufzeit zuweist. Eine Änderung an der Vorlage erreicht deshalb nur Instanzen ohne eigenen Override für diese Eigenschaft. + tags: [instanzen, szenen] + level: 2 + - id: instanzen-geteilte-form + module: instanzen-ressourcen + q: Ein Designer macht die Kollisionsform eines Sammelstücks größer, und alle Sammelstücke wachsen mit. Woran liegt das? + a: | + Ressourcen werden geteilt, nicht kopiert. Alle Instanzen einer Szene teilen ihre Unter-Ressourcen, und ein duplizierter Node teilt seine Form mit dem Original; der Inspektor zeigt das seit Godot 4.6 mit einem Kettensymbol. Soll nur ein Objekt abweichen, macht man die Form im Editor einzigartig. Ändert Code die Form zur Laufzeit, braucht es `duplicate()` vor der Änderung oder `resource_local_to_scene` an der Ressource. + tags: [ressourcen, fehlersuche] + level: 2 + - id: instanzen-varianten-waehlen + module: instanzen-ressourcen + q: Wann nimmst du eine geerbte Szene, wann Bearbeitbare Kinder und wann Lokal machen? + a: | + Eine geerbte Szene nehme ich für eine Variante, die ich an mehreren Stellen einsetze; sie folgt Änderungen an der Basis weiter. Bearbeitbare Kinder nutze ich, wenn eine einzelne Verwendung an einem Kind etwas anders braucht, auch hier bleibt die Verbindung zur Vorlage. Lokal machen kappt die Verbindung und kopiert alles in die Eltern-Szene; das ist für Einzelstücke, die sich bewusst von der Vorlage lösen sollen. In der `.tscn` erkenne ich die Wege an `instance=` an der Wurzel, an `[editable path=…]` oder an einem Node ohne Instanz. + tags: [instanzen, szenen] + level: 2 + - id: projekt-erkunden-erste-stunde + module: projekt-erkunden + q: Du übernimmst ein fremdes Godot-Projekt. Was machst du, bevor du den ersten Wert änderst? + a: | + Ich lese `project.godot` auf Hauptszene, Autoloads, Eingabeaktionen und Ebenen-Namen, starte das Spiel und beobachte Ausgabe und Debugger. Dann folge ich dem Baum der Hauptszene in die Instanzen, überfliege die Skripte nach `class_name`, `signal` und `@export` und prüfe Verbindungen im Signale-Dock und per `grep` in den Szenen, weil die eingebaute Suche standardmäßig nur Skripte durchsucht. Am Ende schaue ich in den Remote-Baum und schreibe eine Projektkarte. + tags: [projekt, vorgehen] + level: 1 + - id: projekt-erkunden-remote + module: projekt-erkunden + q: Wofür nutzt du den Remote-Baum und den Remote-Inspektor, und was musst du dabei beachten? + a: | + Sie zeigen das laufende Spiel: alle Nodes, auch die in Instanzen, und im Inspektor auch nicht exportierte Variablen wie einen Zähler. Ich prüfe dort, was zur Laufzeit existiert, und probiere Werte aus. Änderungen gehen beim Beenden verloren, also übertrage ich einen guten Wert in den lokalen Inspektor oder die `.tres`. Und weil der Remote-Baum das Spiel laut Editor zum Ruckeln bringen kann, schalte ich vor Messungen zurück auf Lokal. + tags: [projekt, laufzeit] + level: 1 + - id: projekt-erkunden-fehlermeldung-lesen + module: projekt-erkunden + q: Wie liest du eine Godot-Fehlermeldung mit Backtrace? + a: | + Die `ERROR`-Zeile sagt, was schiefging, etwa dass eine Eingabeaktion fehlt. Die `at:`-Zeile zeigt Godots eigenen C++-Quelltext und hilft selten. Der GDScript-Backtrace nennt meine Datei und Zeile; Eintrag `[0]` ist die Stelle des Fehlers, die Einträge darunter zeigen, wer sie aufgerufen hat. „Did you mean“ schlägt einen ähnlichen Namen vor und verrät so oft einen Tippfehler. + tags: [projekt, fehlersuche] + level: 1 + - id: pixelraster-viewport-canvas-items + module: pixelraster + q: Was ist der Unterschied zwischen den Stretch Modes `viewport` und `canvas_items` für ein Pixelart-Spiel? + a: | + `viewport` zeichnet die Szene in der Spielauflösung, etwa 320 × 180, und vergrößert dann das fertige Bild. Jedes Spielpixel wird ein gleich großer Block, nichts liegt zwischen zwei Pixeln. `canvas_items` zeichnet jeden Node direkt in Fenstergröße; dann entscheidet der Texturfilter, ob ein vergrößertes Sprite scharf bleibt, und Figuren können sich in Bruchteilen eines Spielpixels bewegen. Für strenge Pixelart nehme ich `viewport`; `canvas_items` passt, wenn Schrift oder Drehungen in voller Auflösung gewünscht sind. Neue Projekte starten in Godot 4.7 mit `canvas_items`, das stelle ich bewusst um. + tags: [pixelraster, skalierung] + level: 1 + - id: pixelraster-faktor-raender + module: pixelraster + q: Ein Spiel mit 320 × 180 läuft auf einem Laptop mit 1366 × 768. Was passiert mit ganzzahliger Skalierung? + a: | + Godot nimmt die kleinere Division und rundet ab: 1366 / 320 und 768 / 180 sind beide 4,27, also Faktor 4. Das Bild wird 1280 × 720 groß, links und rechts bleiben je 43 Pixel schwarz, oben und unten je 24. Mit `fractional` wäre die Fläche voll, aber die Pixel wären abwechselnd 4 und 5 Bildschirmpixel breit. Den Rand nehme ich für gleich große Pixel in Kauf; `expand` füllt ihn nicht, weil er vom Abrunden kommt, nicht vom Seitenverhältnis. + tags: [pixelraster, skalierung] + level: 2 + - id: pixelraster-diagnose + module: pixelraster + q: Ein Tester meldet, die Pixelart sehe „komisch“ aus. Wie grenzt du das ein? + a: | + Ich lasse mir einen Screenshot mit einem Prüfmuster geben: Schachbrett und Streifen aus einzelnen Pixeln. Grau statt Schwarz und Weiß heißt Filter `Linear` oder ein Sprite, das im Modus `canvas_items` vergrößert wird. Abwechselnd breite und schmale Streifen heißen nicht ganzzahliger Faktor, Kamera-Zoom oder `scale`. Unterschiedlich große Pixel nebeneinander sind Mixels. Danach prüfe ich die passende Stelle in `project.godot` oder in der Szene, statt an allen gleichzeitig zu drehen. + tags: [pixelraster, fehlersuche] + level: 2 + - id: pixelraster-kamera-zittern + module: pixelraster + q: Warum zittert eine Figur um einen Pixel, wenn die Kamera geglättet folgt? + a: | + Mit Pixel-Snap rundet Godot Figur und Kamera getrennt auf ganze Pixel. Folgt die Kamera mit Glättung, ist der Abstand zwischen beiden eine Kommazahl, die sich jedes Bild ändert, und gerundet springt die Figur zwischen zwei Positionen hin und her. Gemessen bei 90 px/s: ohne Glättung steht die Figur bei 154, mit Glättung wechselt sie zwischen 170 und 171. Ob man Glättung will, ist eine Designfrage; beurteilen muss man sie in Bewegung. + tags: [pixelraster, kamera] + level: 3 + - id: sprites-import-pipeline + module: sprites-importieren + q: Was passiert, wenn du eine PNG in ein Godot-Projekt legst, und was davon gehört ins Repository? + a: | + Godot importiert sie: Die PNG bleibt unverändert, daneben entsteht eine `.import`-Datei mit den Einstellungen, und das Ergebnis liegt als `.ctex` im Cache `.godot/imported`. Ändert sich die PNG, importiert Godot neu. Ins Repository gehören PNG und `.import`, der Ordner `.godot/` nicht. Das ist wie bei einem Bundler, nur dass die Konfiguration pro Datei neben der Quelle steht. + tags: [import, git] + level: 1 + - id: sprites-pixelart-import + module: sprites-importieren + q: Welche Import-Einstellungen verderben Pixelart, und wo stellst du den Filter ein? + a: | + VRAM-Kompression verschiebt Farben in Blöcken, eine Größenbeschränkung verkleinert mit kubischer Interpolation und macht das Bild weich, Mipmaps kosten in 2D meist nur Speicher. Für Pixelart bleibt es bei Lossless, ohne Mipmaps, Größenbeschränkung 0. Der Filter ist seit Godot 4 keine Import-Einstellung mehr, sondern eine Zeicheneinstellung: projektweit Nearest und an den Nodes Inherit. Änderungen an `.import`-Dateien sehe ich mir im Diff an, weil sie im Spiel leicht übersehen werden. + tags: [import, pixelart] + level: 2 + - id: sprites-ursprung-fuesse + module: sprites-importieren + q: Warum gehört der Ursprung einer Figur in der Draufsicht an die Füße? + a: | + Y-Sortierung zeichnet Nodes nach ihrer Position: Wer weiter unten steht, wird später gezeichnet. Liegt der Ursprung in der Bildmitte, sortiert Godot nach dem Bauch der Figur, und sie steht mit den Füßen vor einer Kiste, aber halb dahinter. Mit dem Ursprung an den Füßen, etwa per `offset` um die halbe Feldhöhe nach oben, stimmt die Reihenfolge mit dem Boden überein, auf dem die Figur steht. + tags: [sprites, y-sortierung] + level: 2 + - id: animationen-sprite-oder-player + module: animationen + q: Wann nimmst du `AnimatedSprite2D` und wann einen `AnimationPlayer`? + a: | + `AnimatedSprite2D` mit SpriteFrames für reine Bildfolgen wie Laufen und Stehen. Sobald im selben Takt noch etwas anderes passieren muss, eine Hitbox, ein Ton, ein Methodenaufruf, nehme ich einen `AnimationPlayer` mit Spuren für alles. Er kann außerdem im Physik-Takt laufen, was für spielmechanisches Timing zählt; das `AnimatedSprite2D` zählt im Bildtakt. Oft kombiniere ich beides. + tags: [animation] + level: 1 + - id: animationen-richtung-360 + module: animationen + q: Deine Figur läuft analog in 360°, gezeichnet sind vier Richtungen. Wie wählst du die Animation? + a: | + Ich nehme den Winkel des letzten Eingabevektors, teile durch 90° und runde; `wrapi` faltet das Ergebnis auf 0 bis 3. Beim Wechsel der Richtung während des Laufens übernehme ich Frame und Fortschritt mit `set_frame_and_progress()`, sonst beginnt der Zyklus neu und die Figur stolpert. Tastatur-Diagonalen liegen genau auf der Sektorgrenze; was dort gilt, lege ich bewusst fest, statt es der Rundung zu überlassen. + tags: [animation, eingabe] + level: 2 + - id: animationen-fussrutschen + module: animationen + q: Die Füße deiner Figur rutschen beim Laufen. Wie gehst du vor? + a: | + Ich rechne nach: Zyklusdauer ist Summe der Frame-Dauern durch FPS, die Strecke pro Zyklus Tempo mal Zyklusdauer. Passt die nicht zum Schrittweg in der Zeichnung, rutscht der Fuß. Bei 90 px/s, vier Frames und 30 Pixeln Schrittweg passen 12 FPS. Ändert sich das Tempo, koppel ich `speed_scale` daran. Ob ein leichtes Gleiten zum Stil passt, entscheide ich danach im Spiel. + tags: [animation, tempo] + level: 2 + - id: audio-queue-free + module: audio + q: Ein Sammelgeräusch spielt nicht, obwohl `play()` aufgerufen wird. Woran liegt es? + a: | + Häufig hängt der Player am Pickup, und das Pickup ruft gleich danach `queue_free()` auf; mit ihm verschwindet der Player, und der Ton bricht ab. Ich lege den Ton auf einen Node, der weiterlebt, und löse ihn über ein Signal aus, oder hänge den Player vor dem Löschen um. Danach prüfe ich den Bus mit Solo und Mute und den Stream selbst, etwa Stille am Anfang oder eine Startposition hinter dem Ende. + tags: [audio, fehlersuche] + level: 2 + - id: audio-dezibel-busse + module: audio + q: Wie ist die Lautstärke in Godot organisiert, und was heißt −6 dB? + a: | + Jeder Player hat `volume_db`, und jeder Ton läuft über einen Bus mit eigenem Regler und Effekten bis zum Master-Bus. Die Lautstärke ist die Summe aller Stufen in Dezibel. −6 dB halbieren die Amplitude, −20 dB teilen sie durch zehn; `linear_to_db()` rechnet um. Busse findet ein Player über den Namen, deshalb bricht das Umbenennen eines Busses die Zuordnung, ohne Fehlermeldung. + tags: [audio, mischung] + level: 1 + - id: kollision-ebenen-layer-maske + module: kollision-ebenen + q: Was ist der Unterschied zwischen Collision Layer und Collision Mask in Godot? + a: | + Der Layer sagt, auf welchen Ebenen ein Objekt liegt, die Maske, welche Ebenen es absucht. Erkannt oder blockiert wird nur, wenn die Maske dessen, der sucht, den Layer des anderen enthält; die Maske des anderen spielt dabei keine Rolle. Ein Sammelstück auf „pickups“ mit Maske „player“ meldet die Figur, auch wenn die Figur selbst nur „world“ sucht. Im Inspektor zählt man Ebenen ab 1, in der `.tscn` steht ein Bitwert: Ebene 3 ist 4. + tags: [kollision, layer, maske] + level: 1 + - id: kollision-ebenen-area-oder-koerper + module: kollision-ebenen + q: Wann nimmst du eine Area2D und wann einen Körper wie StaticBody2D oder AnimatableBody2D? + a: | + Eine `Area2D` meldet Überlappungen über Signale und blockiert nichts, also für Sammelstücke, Trigger und Trefferflächen. Körper blockieren. Eine Wand, die sich nie bewegt, ist ein `StaticBody2D`. Eine Tür, die eine Animation verschiebt und die Figur dabei mitschieben soll, ist ein `AnimatableBody2D`; ein versetzter `StaticBody2D` würde laut Doku nur springen. Die Figur selbst ist ein `CharacterBody2D`, den der Code mit `move_and_slide()` bewegt. + tags: [kollision, nodes] + level: 1 + - id: kollision-ebenen-signal-kommt-nicht + module: kollision-ebenen + q: Ein Sammelstück liegt sichtbar auf der Figur, aber body_entered kommt nie. Wie gehst du vor? + a: | + Ich prüfe von der Seite des Suchenden aus. Hat das Sammelstück eine Form, oder zeigt das Szenen-Dock ein Warndreieck? Ist `monitoring` an? Enthält seine Maske den Layer der Figur? Bei Area gegen Area zusätzlich: Ist beim Ziel `monitorable` an? Dazu starte ich mit sichtbaren Kollisionsformen, damit ich sehe, ob sich die Formen wirklich überlappen und nicht nur die Bilder. Häufigster Fund: Nummer und Bitwert verwechselt, etwa `set_collision_mask_value(4, true)` für Ebene 3. + tags: [kollision, fehlersuche, szenario] + level: 2 + - id: kollision-ebenen-fussform + module: kollision-ebenen + q: Was bewirkt es, wenn eine Figur in einem Top-down-Spiel nur mit einer flachen Form an den Füßen kollidiert? + a: | + Die Figur kann vor und hinter Objekte treten, weil ihr Kopf im Bild über andere Objekte ragen darf, ohne anzustoßen. Mit dem Ursprung an den Füßen zeichnet die Y-Sortierung sie dann richtig davor oder dahinter. Eine Form über die ganze Figur hält sie dagegen schon mit dem Kopf auf; im Testgelände bleibt sie so 10 Pixel früher vor einer Kiste stehen. Welche Variante ein Spiel will, ist eine Designentscheidung. Treffer im Kampf prüfen ohnehin eigene Areas, die anders sitzen können. + tags: [kollision, top-down] + level: 2 + - id: aktionen-parameter-aktionen-statt-tasten + module: aktionen-parameter + q: Warum fragt Spielcode Aktionen ab statt Tasten, und was bedeutet „physisch“ in der Eingabe-Zuordnung? + a: | + Eine Aktion wie `move_right` entkoppelt den Code von der Belegung: Tastatur, Pfeiltasten und Stick hängen an derselben Aktion, und Spieler oder Designer ändern die Belegung, ohne Code anzufassen. Ein physischer Tastencode ist die Position auf einer US-QWERTY-Tastatur; die Doku empfiehlt ihn für Spieleingaben wie WASD. Auf QWERTZ liegt die physische Taste Z dort, wo Y aufgedruckt ist, links neben X. Für Kürzel wie Ctrl+S nimmt man den Tastencode, für Anzeigen das Label. + tags: [eingabe, input-map] + level: 1 + - id: aktionen-parameter-just-pressed + module: aktionen-parameter + q: Was ist der Unterschied zwischen is_action_pressed und is_action_just_pressed? + a: | + `is_action_pressed()` ist in jedem Frame wahr, solange die Aktion gehalten wird, `is_action_just_pressed()` nur im ersten Frame des Drucks. Drei Frames gehalten heißt dreimal wahr gegen einmal. Laufen und Zielen fragen den Zustand ab, Angriff und Ausweichen den Beginn. Wer das verwechselt, bekommt ein Ausweichen, das beim Halten in jedem Frame neu auslöst. + tags: [eingabe, gdscript] + level: 1 + - id: aktionen-parameter-wert-ohne-wirkung + module: aktionen-parameter + q: Ein Designer ändert einen exportierten Wert im Inspektor, und im Spiel passiert nichts. Woran kann das liegen? + a: | + An der Frage, wer den Wert zuletzt setzt. Die Reihenfolge ist: Standard im Skript, Wert in der eigenen Szene, Override an der Instanz in der Elternszene, dann Code in `_ready()`. Eine Zuweisung in `_ready()` schlägt jeden Inspektor-Wert. Außerdem prüfe ich, ob er die richtige Instanz bearbeitet hat, ob der Wert in einer geteilten Ressource liegt, und ob er im Remote-Inspektor des laufenden Spiels geändert hat. Dann zeige ich ihm die Stelle, an der der Wert dauerhaft wirkt. + tags: [parameter, export, szenario] + level: 2 + - id: aktionen-parameter-live-tuning + module: aktionen-parameter + q: Wie stimmst du Werte im laufenden Spiel ab, ohne sie beim Beenden zu verlieren? + a: | + Der Remote-Inspektor ändert nur das laufende Spiel, die Doku sagt ausdrücklich, dass das nach dem Stoppen nicht erhalten bleibt. Zum Ausprobieren ist er gut. Was bleiben soll, ändere ich im Modus Lokal an der Szene oder der `.tres`, während das Spiel läuft; mit „Szenenänderungen synchronisieren“ kommt das sofort im Spiel an. Danach speichere ich und notiere die Änderung, eine Größe pro Runde. + tags: [parameter, debugger, tuning] + level: 2 + - id: level-tilemap-tileset-layer + module: level-tilemap + q: Was ist der Unterschied zwischen TileSet und TileMapLayer? + a: | + Das TileSet ist eine Ressource, die beschreibt, was eine Tile ist: Bildausschnitt, Kollision, Sortierpunkt, Terrain. Der `TileMapLayer` ist ein Node, der in jeder Zelle seines Rasters höchstens eine Tile zeigt. Mehrere Ebenen sind mehrere Layer, seit Godot 4.3 statt des veralteten `TileMap`. Das TileSet speichere ich als `.tres`, damit alle Layer und Level dasselbe benutzen und eine geänderte Kollision überall gilt. + tags: [tilemap, grundlagen] + level: 1 + - id: level-tilemap-y-sort + module: level-tilemap + q: Wie bringst du eine Figur dazu, vor und hinter hohen Tiles richtig gezeichnet zu werden? + a: | + Figur und Tile-Layer hängen unter einem gemeinsamen Node mit Y-Sortierung, und der Tile-Layer hat selbst `y_sort_enabled`. Godot sortiert die Tiles dann an Zellenmitte plus `y_sort_origin`, die Figur an ihrem Ursprung an den Füßen. Den Sortierpunkt einer hohen Tile lege ich auf ihren Fuß; soll die Figur hinter das Objekt treten können, liegt die Kollision nur auf der Grundfläche. Ohne `y_sort_enabled` am Layer sortiert sich der ganze Layer als ein Stück, und die Figur liegt immer davor oder dahinter. + tags: [tilemap, y-sort, top-down] + level: 2 + - id: level-tilemap-naehte + module: level-tilemap + q: Zwischen deinen Tiles erscheinen dünne farbige Linien, vor allem beim Zoomen. Woher kommen sie? + a: | + Der Filter mischt am Rand einer Tile Pixel aus dem Nachbarfeld im Atlas mit, wenn Godot die Tile nicht pixelgenau zeichnet, etwa mit Filter Linear und nicht ganzzahligem Zoom. Texture Padding im Atlas legt einen zusätzlichen Pixel um jede Tile und verhindert das; es ist standardmäßig an. Ich prüfe also Padding, dann Filter und Skalierung, dann ob Abstände, Trennung und Regionsgröße zum Bild passen. + tags: [tilemap, rendering, fehlersuche] + level: 2 + - id: level-tilemap-review + module: level-tilemap + q: Wie gehst du im Code-Review mit Level-Änderungen in Godot um? + a: | + Die gemalten Zellen speichert ein `TileMapLayer` als `tile_map_data`, binär in Base64; ein Diff davon sagt nichts. Ich commite Level-Änderungen deshalb einzeln mit einer Nachricht, die beschreibt, was gemalt wurde, und hänge bei größeren Umbauten einen Screenshot an. Das TileSet als `.tres` ist dagegen lesbar: Kollisionspolygone, Sortierpunkte und Physik-Ebenen sehe ich dort Zeile für Zeile. + tags: [tilemap, git, praxis] + level: 2 + - id: bildrate-simulation-zwei-takte + module: bildrate-simulation + q: Was ist der Unterschied zwischen _process und _physics_process? + a: | + `_process` läuft einmal pro gezeichnetem Bild, mit einem `delta`, das mit der Bildrate schwankt. `_physics_process` läuft in festen Physik-Ticks, standardmäßig 60 pro Sekunde, jeder mit `delta` 1/60. Bei 30 Bildern pro Sekunde laufen vor jedem Bild zwei Ticks, bei 144 mal einer, mal keiner. Bewegung mit Kollision, Frame-Zähler und Eingabepuffer gehören deshalb in `_physics_process`, die Text-Anzeige einer Debug-Ausgabe in `_process`. + tags: [takt, physik] + level: 1 + - id: bildrate-simulation-144hz-ruckeln + module: bildrate-simulation + q: Auf einem 144-Hz-Monitor wirkt die Bewegung unruhig, auf 60 Hz nicht. Woran liegt das, und was tust du? + a: | + Die Physik liefert 60 neue Stände pro Sekunde, der Monitor zeigt 144 Bilder. 84 davon wiederholen die Position, danach springt die Figur. Ich stelle das mit `--fixed-fps 144` nach und zähle die Ticks pro Bild. Physik-Interpolation zeichnet Zwischenstände und glättet das, zeigt aber laut Doku einen Stand ein bis zwei Ticks in der Vergangenheit, und Positionen dürfen dann nur noch im Physik-Takt gesetzt werden. Ob das Spiel sie bekommt, entscheide ich nach einem Test auf dem Zielmonitor. + tags: [takt, interpolation] + level: 2 + - id: bildrate-simulation-zeitlupe + module: bildrate-simulation + q: Bei einer schweren Szene läuft das ganze Spiel in Zeitlupe, obwohl alle Skripte mit delta rechnen. Warum? + a: | + Godot holt pro Bild höchstens `max_physics_steps_per_frame` Ticks nach, standardmäßig 8. Dauert ein Bild länger als 8 Ticks, also 133 ms, fallen Ticks weg, und die Spielzeit läuft langsamer als die Echtzeit. Gemessen mit Godot 4.7.2: Bei 250 ms pro Bild vergingen in 3 Sekunden nur 1,467 Sekunden Spielzeit. Die Ursache liegt also bei der Bildrate, nicht im Bewegungscode. + tags: [takt, performance] + level: 2 + - id: bildrate-simulation-time-scale + module: bildrate-simulation + q: Taugt Engine.time_scale als Pause oder für Hitstop? + a: | + Zum Zusehen ja, zum Anhalten nicht. `time_scale` streckt `delta`, die Zahl der Ticks pro Sekunde bleibt gleich. Code, der Ticks zählt, zählt weiter, und der Ton läuft im normalen Tempo. Bei 0.5 braucht die Figur im Testgelände 18 statt 9 Ticks bis zum Höchsttempo, `velocity` erreicht aber denselben Wert. Für Hitstop hält man gezielt die beteiligten Objekte an, für Einzelbilder nimmt man Pause und Nächster Frame. + tags: [takt, debugging] + level: 2 + - id: fehler-eingrenzen-methode + module: fehler-eingrenzen + q: Ein Kollege meldet, nach dem letzten Umbau zähle der Zähler nicht mehr. Wie gehst du vor? + a: | + Erst reproduzieren: Schritte aufschreiben, bis es jedes Mal passiert. Dann verkleinern, eine Vermutung notieren, genau eine Sache ändern und mit derselben Probe vergleichen. In der Ausgabe lese ich von oben, denn spätere Meldungen sind oft Folgen der ersten. Lief es gestern noch, findet `git bisect` den Commit. Wächter wie `if counter:` lehne ich ab, weil sie die Meldung entfernen, aber nicht die Ursache. + tags: [debugging, methode] + level: 1 + - id: fehler-eingrenzen-backtrace + module: fehler-eingrenzen + q: Wie liest du eine Godot-Fehlermeldung mit GDScript backtrace? + a: | + `[0]` ist die Zeile, an der es scheitert, darunter stehen die Aufrufer. Die Zeile `at:` zeigt in den C++-Quelltext der Engine und hilft selten. Bei „Invalid assignment … on a base object of type 'null instance'“ suche ich, wo die Variable belegt wird, meist ein `@onready`-Pfad; oft nennt eine frühere Meldung wie „Node not found“ genau diese Stelle. Den Backtrace gibt es seit 4.5 im Editor und in Debug-Exporten, in Release-Exporten nur, wenn man ihn einschaltet. + tags: [debugging, fehlermeldungen] + level: 2 + - id: fehler-eingrenzen-verwaiste-nodes + module: fehler-eingrenzen + q: Der Monitor „Verwaiste Nodes“ steigt während des Spiels. Was bedeutet das? + a: | + Es gibt Nodes, die in keinem Baum hängen und nie freigegeben werden, also ein Speicherleck. Typischer Fall: `remove_child()` statt `queue_free()`. Gemessen mit Godot 4.7.2 blieben pro eingesammeltem Pickup drei Nodes übrig, eins pro Node der Szene, und beim Beenden meldete Godot geleakte ObjectDB-Instanzen. `Node.print_orphan_nodes()` listet die betroffenen Nodes, der ObjectDB-Profiler vergleicht Schnappschüsse. + tags: [debugging, speicher] + level: 2 + - id: fehler-eingrenzen-profiler-eigen + module: fehler-eingrenzen + q: Im Profiler stehen _physics_process und count_tiles beide bei knapp 3 ms. Welche Funktion ist langsam? + a: | + Mit der Einstellung „Gesamt“ enthält die Zeit einer Funktion alles, was sie aufruft. `_physics_process` ruft `count_tiles` auf, deshalb stehen beide gleich hoch. Ich schalte auf „Eigen“: Dort fiel `_physics_process` im Test auf 0,00 ms, `count_tiles` blieb bei 2,70 ms. Die Zeit steckt also im Code von `count_tiles`. Vorher und nachher messe ich mit derselben Szene. + tags: [debugging, profiler] + level: 2 + - id: export-sichern-vorlagen + module: export-sichern + q: Der Export-Dialog meldet, es sei keine Exportvorlage unter dem erwarteten Pfad. Was ist los? + a: | + Eine Exportvorlage ist ein vorgebautes Godot ohne Editor, das mit der PCK des Projekts zum Spiel wird. Sie muss exakt zur Editor-Version passen; Godot sucht sie in `export_templates/4.7.2.stable/`, und der Dialog nennt den Pfad. Ich installiere sie unter *Editor → Exportvorlagen verwalten* für die nötigen Plattformen oder aus der TPZ-Datei. Vorlagen einer anderen Version helfen nicht. + tags: [export] + level: 1 + - id: export-sichern-debug-release + module: export-sichern + q: Welche Testversion schickst du Testern, Debug oder Release? + a: | + Meist Debug. Nur dort steht bei einem Skriptfehler der GDScript-Backtrace im Log, also die Zeile im eigenen Code. Der Release-Build meldete im Test denselben Fehler nur mit der Stelle in der Engine. Das Log liegt unter `user://logs/godot.log`, unter Linux in `~/.local/share/godot/app_userdata//logs/`. Ich bette die PCK ein, damit niemand die zweite Datei vergisst, und notiere den Git-Tag der Version. + tags: [export, debugging] + level: 2 + - id: export-sichern-repository + module: export-sichern + q: Welche Dateien eines Godot-4-Projekts gehören ins Repository? + a: | + Alles, was sich nicht verlustfrei neu erzeugen lässt: `project.godot`, Szenen, Ressourcen, Skripte, die `.import`-Dateien mit den Import-Einstellungen, die `.uid`-Dateien seit 4.4 und `export_presets.cfg`. Nicht hinein gehören `.godot/` mit Cache, Importergebnissen und `export_credentials.cfg` sowie die Exporte. Die `.gitignore` erzeugt Godot selbst unter Projekt → Versionsverwaltung. Große Binärdateien kommen bei Bedarf in Git LFS, eingerichtet vor dem ersten Commit. + tags: [git, export] + level: 1 + - id: werkstatt-messen-diff-als-beleg + module: werkstatt-messen + q: Du übernimmst einen Prototyp, der sich nach einem Umbau anders verhält. Warum schaust du nicht zuerst in git diff? + a: | + Weil der Diff mir die Antwort zeigt, aber nicht, wie ich das Symptom im Editor finde, und bei einem Kunden-Projekt gibt es oft keinen sauberen letzten Stand. Ich beschreibe das Symptom ohne Vermutung, prüfe gezielt die Stellen, an denen es entstehen kann, und bestätige die Ursache erst dann im Diff gegen den letzten guten Commit. Jede Korrektur bekommt einen Eintrag mit Symptom, Ursache und Beleg und einen eigenen Commit. + tags: [debugging, methode] + level: 3 diff --git a/content/godot/modules/aktionen-parameter.yaml b/content/godot/modules/aktionen-parameter.yaml new file mode 100644 index 0000000..3929bb9 --- /dev/null +++ b/content/godot/modules/aktionen-parameter.yaml @@ -0,0 +1,559 @@ +id: aktionen-parameter +title: Eingabeaktionen und exportierte Parameter abstimmen +level: 2 +minutes: 130 +summary: Tasten über Aktionen zuordnen, auch auf QWERTZ, Aktionen im Code richtig abfragen, exportierte Parameter mit Hints lesen, herausfinden, welcher Wert gilt, Werte im laufenden Spiel ändern, ohne sie zu verlieren, und ihre Wirkung in Frames vorhersagen. +place: unterwegs +goals: + - Du liest die Eingabe-Zuordnung im Editor und in `project.godot` und erklärst, warum WASD als physische Tasten auf QWERTZ und QWERTY gleich liegt. + - Du unterscheidest `is_action_pressed()`, `is_action_just_pressed()` und `get_action_strength()` und erkennst Fehler, die aus der Verwechslung entstehen. + - Du liest exportierte Parameter mit Bereich, Einheit, Auswahl, Gruppe, Dateipfad und Node-Verweis im Inspektor und im Skript. + - Du sagst vorher, welcher Wert gilt, wenn Skript, Szene, Instanz und `_ready()` verschiedene Werte setzen. + - Du änderst Werte im laufenden Spiel so, dass sie nach dem Beenden erhalten bleiben, und sagst ihre Wirkung in Frames vorher. +resources: + - title: Godot Docs · Input examples · InputMap + url: https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html#inputmap + - title: Godot Docs · GDScript exported properties + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html + - title: Godot Docs · Game embedding · Interaction mode + url: https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#interaction-mode + - title: Godot Docs · Input + url: https://docs.godotengine.org/en/stable/classes/class_input.html +lesson: | + Die Figur im Testgelände ist dir zu träge. Du öffnest `player_stats.tres`, ziehst `acceleration` hoch, startest, und nichts ändert sich. Oder es ändert sich, aber nach dem nächsten Start ist der alte Wert zurück, weil du ihn im laufenden Spiel im falschen Inspektor geändert hast. Oder ein Ausweichen löst beim Halten der Taste dreißigmal aus. Keiner dieser Fehler steckt in der Bewegungsrechnung. Sie stecken in der Frage, wo ein Wert lebt, wer ihn zuletzt setzt und wie der Code eine Taste abfragt. + + Hier geht es um die zwei Stellen, an denen du ein Spiel abstimmst, ohne Code zu schreiben: die Eingabe-Zuordnung (Input Map) für Tasten und Sticks und die exportierten Parameter im Inspektor. Ein Coding-Agent baut beides als Gerüst; welche Taste was tut und welche Werte gut sind, entscheidest du. Alle Werte hier sind Platzhalter des Testgeländes, alle Ausgaben stammen aus Läufen mit Godot 4.7.2. + + ## Aktionen statt Tasten: die Eingabe-Zuordnung + + Der Code fragt nie eine Taste ab, sondern eine Aktion (Input Action) mit einem Namen wie `move_right`. Welche Tasten, Stick-Achsen oder Knöpfe eine Aktion auslösen, steht unter *Projekt → Projekteinstellungen → Eingabe-Zuordnung* ([InputMap](https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html#inputmap)). Jede Aktion hat dort eine Liste von Ereignissen und eine Totzone (Deadzone), Standard 0,2: + + ![Eingabe-Zuordnung des Testgeländes: Aktionen move_left und move_right mit je A oder D (Physisch), Left oder Right (Physisch) und einer Zeile Joypad Achse 0 für den linken Stick, deren Text abgeschnitten ist; rechts die Spalte Totzone mit 0.2](/media/godot/aktionen-parameter/eingabe-zuordnung.png "Godot 4.7.2, Projekteinstellungen → Eingabe-Zuordnung, Ausschnitt; die Joypad-Zeilen gehen im Editor weiter") + + Gespeichert wird das in `project.godot`, als lange Zeilen mit `Object(…)`. Gekürzt sieht `move_right` so aus: + + ```ini title=project.godot + [input] + + move_right={ + "deadzone": 0.2, + "events": [Object(InputEventKey, … "keycode":0,"physical_keycode":68, …) + , Object(InputEventKey, … "keycode":0,"physical_keycode":4194321, …) + , Object(InputEventJoypadMotion, … "axis":0,"axis_value":1.0, …) + ] + } + ``` + + 68 ist die Taste D, 4194321 die Pfeiltaste rechts, Achse 0 mit Wert 1,0 der linke Stick nach rechts. Aus dem Web kennst du das Muster von Tastenkürzeln in einem Editor: Eine Taste ruft einen Befehl mit Namen auf, und der Code reagiert auf den Befehl. Anders als ein `keydown`-Listener fragt `player.gd` den Zustand in jedem Frame ab, statt auf ein Ereignis zu warten; beides geht in Godot ([Events versus polling](https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html#events-versus-polling)). Die Deadzone-Rechnung für Sticks steht in [Godot-Start](/combat/godot-start). + + ### Physisch, Tastencode, Label: QWERTZ und QWERTY + + Eine Taste kann Godot auf drei Arten speichern. Im Dialog, mit dem du ein Ereignis hinzufügst, heißen sie in Godot 4.7.2 „Physischer Tastencode (Position auf der US-QWERTY-Tastatur)“, „Tastencode (Lateinäquivalent)“ und „Tasten-Label (Unicode, unabh. Groß-/Kleinschreibung)“. Die Liste zeigt physische Tasten mit dem Zusatz „(Physisch)“. + + - Der **physische Tastencode** (`physical_keycode`) ist eine Position auf einer US-Tastatur. Die Doku nennt ihn ausdrücklich für Spieleingaben wie WASD, „where only the location of the keys is important“ ([InputEventKey.physical_keycode](https://docs.godotengine.org/en/stable/classes/class_inputeventkey.html#class-inputeventkey-property-physical-keycode)). + - Der **Tastencode** (`keycode`) ist das lateinische Zeichen, das im aktuellen Layout auf der Taste steht, gedacht für Kürzel wie Ctrl+S. + - Das **Tasten-Label** (`key_label`) ist das Zeichen im aktuellen Layout, auch kyrillisch oder arabisch, gedacht für Anzeigen. + + W, A, S und D liegen auf QWERTZ und QWERTY an denselben Stellen, deshalb fällt der Unterschied dort nicht auf. Bei Z und Y schon: Die physische Taste Z sitzt ganz links in der unteren Buchstabenreihe; auf deiner deutschen Tastatur steht dort Y, links neben X. Eine französische AZERTY-Tastatur vertauscht A mit Q und W mit Z. Mit physischen Tasten bleibt die Bewegung für alle an derselben Stelle der Hand. Willst du im Spiel anzeigen, welche Taste das auf dem Gerät des Spielers ist, hilft `DisplayServer.keyboard_get_label_from_physical()`, wie die Doku an derselben Stelle zeigt. + + ## Aktionen im Code abfragen + + Für jede Aktion fragt der Code einen von drei Zuständen ab ([Input](https://docs.godotengine.org/en/stable/classes/class_input.html#class-input-method-is-action-just-pressed)): + + | Abfrage | ist wahr | passt für | + | --- | --- | --- | + | `is_action_pressed()` | in jedem Frame, solange gedrückt | Laufen, Halten, Zielen | + | `is_action_just_pressed()` | nur im ersten Frame des Drucks | Angriff, Ausweichen, Menü | + | `is_action_just_released()` | nur im ersten Frame nach dem Loslassen | Aufladen beenden | + + Mit Godot 4.7.2 gezählt: Hält ein Testskript eine Aktion drei Physik-Frames lang, ist `is_action_pressed()` in `_physics_process` dreimal wahr, `is_action_just_pressed()` einmal. Für Sticks liefert `get_action_strength()` einen Wert zwischen 0 und 1, mit herausgerechneter Deadzone: Ein Stick bei 0,6 mit Deadzone 0,2 ergibt 0,5. `Input.get_vector()` baut daraus die Richtung, die `player.gd` benutzt: + + ```gdscript title=player.gd + var input := Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down") + ``` + + Ein Tippfehler im Aktionsnamen bricht das Spiel nicht ab. Die Ausgabe meldet in Godot 4.7.2 bei jedem Aufruf, mit Vorschlag: + + ```text + ERROR: The InputMap action "move_rigth" doesn't exist. Did you mean "move_right"? + ``` + + Die Abfrage liefert dann 0, und die Figur läuft nicht nach rechts. Fehlt eine Aktion ganz in der Eingabe-Zuordnung, etwa `attack`, steht dort nur `The InputMap action "attack" doesn't exist.`, ohne Vorschlag. Einen Druck aufheben, der ein paar Frames zu früh kommt (Eingabepuffer), behandelt [Eingabe: Reaktionszeit, Latenz und Puffer](/combat/eingabe). + + ## Exportierte Parameter und ihre Hints + + `@export` macht eine Variable im Inspektor sichtbar und speichert ihren Wert in der Szene oder Ressource. Hints ändern, wie der Inspektor sie zeigt ([GDScript exported properties](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html)). Die Bewegungswerte des Testgeländes liegen in einer eigenen Ressource: + + ```gdscript title=movement_stats.gd + class_name MovementStats + extends Resource + + ## Bewegungswerte der Figur. Platzhalter: Die echten Werte bestimmst du. + + @export_range(0, 400, 1, "suffix:px/s") var max_speed := 90.0 + @export_range(0, 5000, 10, "suffix:px/s²") var acceleration := 600.0 + @export_range(0, 5000, 10, "suffix:px/s²") var friction := 900.0 + ``` + + ![Inspektor von player_stats.tres: Abschnitt MovementStats mit Max Speed 90 px/s, Acceleration 600 px/s² und Friction 900 px/s², jeweils mit Schieberegler](/media/godot/aktionen-parameter/stats-inspektor.png "Godot 4.7.2, Inspektor der Ressource player_stats.tres") + + `export_range` setzt Grenzen und Schrittweite für Regler und Eingabe, `suffix:` hängt die Einheit an ([Adding suffixes](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#adding-suffixes-and-handling-degrees-radians)). Die Grenzen gelten nur im Editor: Im Code kannst du `radius = 500.0` setzen, und der Wert bleibt 500. Ein Agent baut dir oft Testwerkzeuge mit mehr Hints. Dieses Skript verteilt Sammelstücke für Testläufe, es ist kein Spielinhalt. Gezeigt ist der Kopf mit den Exports, ohne `_ready()`: + + ```gdscript title=pickup_spawner.gd + class_name PickupSpawner + extends Node2D + + ## Testwerkzeug: verteilt Sammelstücke für Testläufe. Kein Spielinhalt, + ## alle Werte sind Platzhalter. + + ## Szene, die verteilt wird. + @export_file("*.tscn") var pickup_path := "res://pickup.tscn" + ## Node, unter den die Sammelstücke gehängt werden. + @export var target: Node2D + ## Wie viele Sammelstücke entstehen. + @export_range(0, 50) var count := 5 + + @export_group("Anordnung") + ## Kreis um den Spawner oder Raster nach rechts unten. + @export_enum("Kreis", "Raster") var layout := 0 + @export_subgroup("Kreis") + ## Abstand vom Spawner. + @export_range(0, 200, 1, "suffix:px") var radius := 60.0 + @export_subgroup("Raster") + ## Abstand zwischen zwei Sammelstücken. + @export_range(4, 64, 1, "suffix:px") var spacing := 24.0 + ``` + + ![Inspektor des Spawner: Pickup Path mit Datei-Knopf, Target zeigt Objects, Count 8, Gruppe Anordnung mit Layout Raster als Auswahlliste, Untergruppen Kreis mit Radius 60 px und Raster mit Spacing 24 px](/media/godot/aktionen-parameter/spawner-inspektor.png "Godot 4.7.2, Inspektor eines Spawner-Nodes in einer Testszene") + + - `@export_file("*.tscn")` zeigt einen Dateiknopf, der nur `.tscn` anbietet. Wählst du die Datei dort aus, speichert Godot 4.7.2 eine UID wie `uid://d1p2sp4rba6w6`, wenn die Datei eine hat; so bleibt der Verweis gültig, wenn du die Datei verschiebst ([@export_file](https://docs.godotengine.org/en/stable/classes/class_@gdscript.html#class-gdscript-annotation-export-file)). `load()` versteht UID und Pfad. Soll immer der Pfad als Text gespeichert werden, nimmst du `@export_file_path`. + - `@export var target: Node2D` ist ein Node-Verweis. In der `.tscn` steht er als Pfad, `target = NodePath("../Objects")`. + - `@export_enum` zeigt eine Auswahlliste, speichert aber die Nummer: Raster ist `layout = 1`. + - `@export_group` und `@export_subgroup` ordnen nur die Anzeige ([Grouping exports](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#grouping-exports)). + - Ein `##`-Kommentar direkt über der Variable wird zum Tooltip im Inspektor ([Documenting script members](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_documentation_comments.html#documenting-script-members)). + + Im Inspektor heißt `max_speed` „Max Speed“; Godot macht aus dem Bezeichner einen Titel. Suchst du den Wert im Code, nimm den Bezeichner mit Unterstrich. + + ## Wann Godot einen Wert setzt + + Wo ein exportierter Wert gespeichert sein kann, im Skript, in der Vorlage oder als Override an der Instanz, und welcher dann gilt, steht im [Modul über Instanzen und Ressourcen](/godot/instanzen-ressourcen): Jede Stufe überschreibt die vorige, und Werte, die dem Standard entsprechen, stehen nicht in der Datei. Für das Abstimmen kommt eine Frage dazu: wann Godot diese Werte setzt und was danach noch kommt. + + Godot setzt die gespeicherten Werte, nachdem das Objekt erzeugt ist und bevor `_ready()` läuft. In `_init()` siehst du deshalb noch den Standard aus dem Skript ([Reading an exported variable's value early on](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#reading-an-exported-variable-s-value-early-on)). Steht in `_ready()` eine Zuweisung an eine exportierte Variable, gewinnt sie gegen jeden Inspektor-Wert. Das ist die häufigste Ursache für „ich ändere den Wert, und nichts passiert“. + + Ressourcen folgen einer eigenen Regel. `stats` der Figur ist ein Verweis auf `player_stats.tres`. Die Werte darin gehören der Datei, nicht der Figur: Jeder Node, der dieselbe `.tres` benutzt, sieht dieselben Werte, und eine Änderung im Inspektor landet beim Speichern in dieser Datei. Wie du eine Ressource für eine Instanz einzigartig machst, steht im selben Modul. + + Im Web entspricht das Props mit Standardwerten in einer Komponente, die die Elternkomponente überschreibt. Der Vergleich bricht an einer Stelle: Godot setzt die Werte einmal beim Laden. Es rendert nichts neu, wenn sich danach etwas ändert. + + ## Live abstimmen, ohne den Wert zu verlieren + + Während das Spiel läuft, zeigt das Szenen-Dock oben zwei Knöpfe, „Remote“ und „Lokal“. Remote zeigt den Baum des laufenden Spiels, und der Inspektor zeigt den Node von dort, mit dem Zusatz „Remote-“ im Namen. Ganz oben stehen dort unter „Members“ auch Variablen, die nicht exportiert sind, etwa `total` (im Bild weggeschnitten). Ressourcen wie `stats` klappst du mit einem Klick auf ihr Feld auf ([Remote in scene dock](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#remote-in-scene-dock)): + + ![Szenen-Dock im Modus Remote mit root, Main, Objects und ausgewähltem Player, darunter der Inspektor Remote-Player: Abschnitt Player mit aufgeklapptem Stats (Max Speed 90 px/s, Acceleration 600 px/s², Friction 900 px/s²) und Debug Log](/media/godot/aktionen-parameter/remote-player.png "Godot 4.7.2, Remote-Baum und Remote-Inspektor des laufenden Testgeländes") + + Was du dort änderst, wirkt sofort und ist beim Beenden weg. Die Doku sagt es ausdrücklich: Änderungen im Remote-Baum und im Spiel-Bildschirm bleiben nach dem Stoppen nicht erhalten. Ihr Weg, Werte zu behalten: im Modus Lokal den Node wählen und dort ändern, während das Spiel läuft, mit *Debuggen → Szenenänderungen synchronisieren* ([Game embedding](https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#interaction-mode), [Synchronize Scene Changes](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#synchronize-scene-changes)). + + Mit Godot 4.7.2 geprüft: Der Haken ist in einem neuen Editor gesetzt. Eine Änderung von `max_speed` im Inspektor von `player_stats.tres` kam im laufenden Spiel innerhalb einer Sekunde an, ebenso `debug_log` an der Figur in `main.tscn`. Beide Werte blieben nach dem Stoppen im Editor stehen. Auf der Platte stehen sie erst nach dem Speichern: *Szene → Szene speichern* (`Ctrl+S`) schrieb auch die geänderte `player_stats.tres`. + + > [!PRAXIS] + > Nimm Remote zum Nachsehen und Ausprobieren, Lokal für alles, was bleiben soll. Notier jeden Wert, den du im Remote-Inspektor gut findest, bevor du das Spiel beendest, und trag ihn dann lokal ein. Wie du dabei vorgehst, damit du weißt, welche Änderung was bewirkt hat, steht in [Tuning als Experiment](/combat/tuning): eine Hypothese, eine Größe pro Runde, ein Protokoll. + + ## Wirkung vorhersagen: Frames bis zum Höchsttempo + + Die Figur rechnet in jedem Physik-Frame: + + ```gdscript title=player.gd + var rate := stats.acceleration if input != Vector2.ZERO else stats.friction + velocity = velocity.move_toward(input * stats.max_speed, rate * delta) + ``` + + `move_toward` ändert die Geschwindigkeit pro Frame höchstens um `rate * delta`. Bei 60 Frames pro Sekunde ist `delta` 1/60, also ändern sich bei `acceleration` 600 höchstens 10 px/s pro Frame, bei `friction` 900 höchstens 15. Mit *Debug Log* an der Figur schreibt sie das selbst in die Ausgabe. Ein Testlauf mit Godot 4.7.2, Taste in den Frames 1 bis 12 gedrückt: + + ```text + 1 speed 10.0 + 2 speed 20.0 + … + 8 speed 80.0 + 9 speed 90.0 + 10 speed 90.0 + … + 12 speed 90.0 + 13 speed 75.0 + 14 speed 60.0 + … + 17 speed 15.0 + 18 speed 0.0 + ``` + + Nach 9 Frames erreicht die Figur 90 px/s, nach dem Loslassen steht sie 6 Frames später. Den Bremsweg rechnest du aus den Geschwindigkeiten nach dem Loslassen: (75 + 60 + 45 + 30 + 15 + 0) / 60 = 3,75 Pixel. Allgemein: + + ```text + Frames bis Höchsttempo = max_speed / (acceleration / 60), aufgerundet + Frames bis Stillstand = max_speed / (friction / 60), aufgerundet + ``` + + > [!MERKE] + > Ein Wert in px/s² sagt dir erst etwas, wenn du ihn in px/s pro Frame umrechnest: geteilt durch die Physik-Ticks pro Sekunde, im Testgelände 60. + + Ob sich 9 Frames gut anfühlen, sagt keine Formel. Die Rechnung sagt dir vorher, was eine Änderung bewirkt, damit du beim Spielen weißt, was du prüfst. + + > [!INTERVIEW] + > „Ein Designer ändert einen Wert im Inspektor, und im Spiel passiert nichts. Wie gehst du vor?“ Gute Antwort: Ich prüfe, wo der Wert lebt und wer ihn zuletzt setzt. Steht er in der Instanz und wird in der Szene überschrieben? Ist er Teil einer geteilten Ressource? Setzt `_ready()` ihn im Code? Läuft das Spiel noch, und hat er im Remote-Inspektor geändert? Dann zeige ich, wo er den Wert dauerhaft einträgt. + + ## Häufige Fehler + + - **Taste statt Aktion.** Code fragt `KEY_D` ab statt `move_right`. Dann hilft die Eingabe-Zuordnung nicht, und Gamepad oder anderes Layout gehen nicht. + - **Tastencode statt physischer Taste.** Eine Aktion auf Z liegt mit Tastencode auf deiner Tastatur oben, für Spieler mit QWERTY unten links. Für Bewegung und Aktionen nach Position nimmst du physische Tasten. + - **`is_action_pressed()` für eine einmalige Aktion.** Ausweichen oder Angriff lösen dann in jedem Frame aus, solange die Taste unten ist. + - **Tippfehler im Aktionsnamen.** Godot läuft weiter und meldet `The InputMap action "…" doesn't exist.` in der Ausgabe. + - **Zuweisung in `_ready()`.** Sie überschreibt jeden Inspektor-Wert. Such in `_ready()` nach dem Bezeichner, bevor du am Inspektor zweifelst. + - **Im Remote-Inspektor abgestimmt.** Nach dem Beenden ist der Wert weg. Lokal ändern und speichern. + - **Geteilte Ressource geändert.** Du wolltest eine Figur schneller machen, und alle mit derselben `.tres` werden schneller. + - **Grenzen von `export_range` für Sicherheit gehalten.** Sie gelten nur im Editor, Code kann jeden Wert setzen. + + ## Zusammenfassung + + - Der Code fragt Aktionen ab, die Eingabe-Zuordnung verbindet sie mit Tasten, Achsen und Knöpfen. Gespeichert wird alles unter `[input]` in `project.godot`. + - Physische Tasten sind Positionen auf einer US-Tastatur und für Spieleingaben gedacht; „(Physisch)“ in der Liste zeigt sie an. + - `is_action_pressed()` ist in jedem gedrückten Frame wahr, `is_action_just_pressed()` nur im ersten; `get_action_strength()` liefert 0 bis 1 nach der Deadzone. + - Hints wie `export_range` mit `suffix:`, `export_enum`, `export_group`, `export_file` und Node-Exports ändern die Anzeige, `##`-Kommentare werden zu Tooltips. + - Es gilt der zuletzt gesetzte Wert: Skript, dann Vorlage, dann Instanz, dann Code in `_ready()`. In `_init()` siehst du nur den Standard. + - Ressourcen gehören ihrer Datei und werden geteilt. + - Remote-Änderungen sind beim Beenden weg. Lokal mit „Szenenänderungen synchronisieren“ ändern und speichern. + - Beschleunigung und Reibung in px/s² geteilt durch die Physik-Ticks pro Sekunde (Standard 60) ergeben die Änderung pro Frame; daraus folgen Frames bis zum Höchsttempo und der Bremsweg. +exercises: + - id: physisch-qwertz + type: choice + title: Welche Taste ist physisch Z? + prompt: | + Ein Agent hat die Aktion `dodge` auf die **physische** Taste Z gelegt. Du spielst mit einer deutschen QWERTZ-Tastatur. Welche Taste löst bei dir `dodge` aus? + options: + - text: Die Taste, auf der Y steht, ganz links in der unteren Buchstabenreihe, links neben X + correct: true + why: Physische Tasten sind Positionen auf einer US-QWERTY-Tastatur. Dort sitzt Z ganz links in der unteren Buchstabenreihe, auf QWERTZ steht an dieser Stelle Y. + - text: Die Taste, auf der Z steht, oben zwischen T und U + correct: false + why: So wäre es mit einem Tastencode (Lateinäquivalent). Der richtet sich nach der Beschriftung im aktuellen Layout, nicht nach der Position. + - text: Beide Tasten, Godot wählt je nach Layout + correct: false + why: Ein Ereignis speichert genau eine Art. Ein physisches Ereignis kennt nur die Position. + - text: Keine, weil physische Tasten nur mit US-Tastaturen funktionieren + correct: false + why: Physische Tasten funktionieren mit jedem Layout. Sie beziehen sich nur auf die Position der US-Tastatur. + explanation: | + Der physische Tastencode beschreibt laut Doku die Position auf einer 101/102-Tasten-US-QWERTY-Tastatur. Für WASD fällt das auf QWERTZ nicht auf, für Z und Y schon. Für Bewegung und Aktionen, bei denen die Lage der Hand zählt, ist das gewollt. Willst du dem Spieler anzeigen, welche Beschriftung das auf seiner Tastatur ist, hilft `DisplayServer.keyboard_get_label_from_physical()`. + resources: + - title: Godot Docs · InputEventKey.physical_keycode + url: https://docs.godotengine.org/en/stable/classes/class_inputeventkey.html#class-inputeventkey-property-physical-keycode + + - id: wert-kette + type: order + title: Wer setzt den Wert zuletzt? + prompt: | + Ein Sammelstück `Pickup2` in `main.tscn` hat die exportierte Variable `value`. Bring die Stellen in die Reihenfolge, in der Godot sie beim Starten und Spielen anwendet. Die oberste setzt den Wert zuerst, die unterste zuletzt. + lang: prose + lines: | + Standardwert im Skript pickup.gd + Zeile value in pickup.tscn + Zeile value unter Pickup2 in main.tscn + Zuweisung an value in _ready + Änderung im Remote-Inspektor während das Spiel läuft + explanation: | + Godot erzeugt das Objekt mit dem Standardwert aus dem Skript, setzt dann die Werte aus der eigenen Szene und danach die Overrides der Instanz aus der Elternszene. Erst dann läuft `_ready()`, und eine Zuweisung dort überschreibt alles davor. Der Remote-Inspektor wirkt noch später, im laufenden Spiel, und verschwindet beim Beenden. Mit Godot 4.7.2 nachgespielt: Standard 1, `pickup.tscn` 5 und `main.tscn` 3 ergeben für `Pickup2` den Wert 3, und eine Zuweisung in `_ready()` schlägt alle drei. + resources: + - title: Godot Docs · Reading an exported variable's value early on + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#reading-an-exported-variable-s-value-early-on + + - id: gedrueckt-zaehlen + type: output + title: Gedrückt oder gerade gedrückt? + prompt: | + Ein Testskript hält die Aktion `dodge` genau drei Physik-Frames lang gedrückt und lässt dann los. Danach gibt es `print(pressed_frames, " ", just_frames)` aus. Was steht in der Ausgabe? + lang: gdscript + code: | + var pressed_frames := 0 + var just_frames := 0 + + + func _physics_process(_delta: float) -> void: + if Input.is_action_pressed(&"dodge"): + pressed_frames += 1 + if Input.is_action_just_pressed(&"dodge"): + just_frames += 1 + expected: | + 3 1 + hints: + - "`is_action_pressed()` fragt den Zustand ab, in jedem Frame neu." + - "`is_action_just_pressed()` ist nur in dem Frame wahr, in dem der Druck beginnt." + explanation: | + Solange die Aktion gedrückt ist, ist `is_action_pressed()` in jedem Physik-Frame wahr, hier dreimal. `is_action_just_pressed()` ist nur im ersten Frame des Drucks wahr. Gemessen mit Godot 4.7.2: `3 1`. Daraus folgt die Regel: Für alles, was einmal pro Druck passieren soll, nimmst du `just_pressed`, für alles, was beim Halten anhält, `pressed`. + resources: + - title: Godot Docs · Input.is_action_just_pressed + url: https://docs.godotengine.org/en/stable/classes/class_input.html#class-input-method-is-action-just-pressed + + - id: init-oder-ready + type: output + title: Zu früh gefragt + prompt: | + Dieses Skript hängt an einem Node, bei dem im Inspektor Value 3 eingetragen ist; in der `.tscn` steht `value = 3`. Was gibt es beim Laden der Szene aus? + lang: gdscript + code: | + extends Area2D + + @export var value := 1 + + + func _init() -> void: + print("_init: ", value) + + + func _ready() -> void: + print("_ready: ", value) + expected: | + _init: 1 + _ready: 3 + hints: + - "`_init()` läuft, wenn das Objekt erzeugt wird." + - Wann setzt Godot die Werte aus der Datei? + explanation: | + Godot erzeugt das Objekt zuerst mit dem Standardwert aus dem Skript, dabei läuft `_init()`. Danach setzt es die Werte aus der Szene, hier `value = 3`, und erst dann kommt `_ready()`. Die Doku beschreibt genau diesen Ablauf. Gemessen mit Godot 4.7.2. Wer in `_init()` mit exportierten Werten rechnet, rechnet mit dem Standard und wundert sich über den Inspektor. + resources: + - title: Godot Docs · Reading an exported variable's value early on + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#reading-an-exported-variable-s-value-early-on + + - id: welcher-wert + type: output + title: Wer zählt wie viel? + prompt: | + Angenommen, im Testgelände steht in `pickup.tscn` am Wurzel-Node `value = 5`, und `Pickup2` hat in `main.tscn` den Override `value = 3`, wie im Versuch des Moduls über Instanzen und Ressourcen. Jetzt ändert jemand in `pickup.gd` den Standard von 1 auf 2. Die Figur sammelt `Pickup` und `Pickup2` ein. Welche Zahl zeigt der Zähler? + lang: text + code: | + pickup.gd @export var value := 2 (vorher 1) + pickup.tscn value = 5 + main.tscn Pickup2: value = 3 + expected: | + 8 + accept: + - 8 Punkte + - 5 + 3 = 8 + hints: + - Für `Pickup` gibt es eine Zeile in der Vorlage, für `Pickup2` zusätzlich einen Override. + - Kommt der Standard aus dem Skript hier noch irgendwo zum Zug? + explanation: | + `Pickup` hat keinen Override und nimmt die 5 aus der Vorlage `pickup.tscn`. `Pickup2` hat seinen Override und zählt 3. Zusammen 8, gemessen mit Godot 4.7.2, mit Standard 1 wie mit Standard 2. Der geänderte Standard kommt nicht zum Zug, weil beide Sammelstücke einen gespeicherten Wert haben. Er wirkt nur bei Nodes, die den Wert nirgends speichern; deshalb ist eine Änderung am Skript-Standard oft wirkungslos, ohne dass etwas meldet. + resources: + - title: Godot Docs · GDScript exported properties · Basic use + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#basic-use + + - id: frames-bis-tempo + type: output + title: Langsamer anlaufen + prompt: | + In `player_stats.tres` stellst du `acceleration` von 600 auf 450, `max_speed` bleibt 90. Nach wie vielen Physik-Frames mit gedrückter Taste erreicht die Figur aus dem Stand 90 px/s? + lang: text + code: | + max_speed 90 px/s + acceleration 450 px/s² (vorher 600) + Physik 60 Frames pro Sekunde + expected: | + 12 + accept: + - 12 Frames + hints: + - Rechne die Beschleunigung in px/s pro Frame um. + - 450 / 60 = 7,5 px/s pro Frame. + explanation: | + `move_toward` ändert die Geschwindigkeit pro Frame höchstens um `acceleration / 60`, hier 7,5 px/s. 90 / 7,5 = 12 Frames. Gemessen mit Godot 4.7.2: 12 Frames, mit 600 waren es 9. Geht die Division nicht auf, rundest du auf: Bei 700 sind es 90 / 11,67 = 7,7, also 8 Frames. + resources: + - title: Godot Docs · Vector2.move_toward + url: https://docs.godotengine.org/en/stable/classes/class_vector2.html#class-vector2-method-move-toward + + - id: bremsweg + type: output + title: Wie weit rutscht die Figur? + prompt: | + `friction` steht jetzt auf 1800, `max_speed` auf 90. Die Figur läuft mit 90 px/s nach rechts, dann lässt du los. Wie viele Pixel legt sie noch zurück, bis sie steht? + lang: text + code: | + max_speed 90 px/s + friction 1800 px/s² + Physik 60 Frames pro Sekunde + expected: | + 1.5 + accept: + - "1,5" + - 1.5 px + - "1,5 px" + - 1.5 Pixel + - "1,5 Pixel" + hints: + - Pro Frame sinkt die Geschwindigkeit um 1800 / 60 = 30 px/s. + - In jedem Frame bewegt sich die Figur um ihre neue Geschwindigkeit geteilt durch 60. + explanation: | + Nach dem Loslassen sinkt das Tempo pro Frame um 30 px/s: 60, 30, 0. In jedem dieser Frames bewegt sich die Figur um Tempo / 60 Pixel, also (60 + 30 + 0) / 60 = 1,5 Pixel. Gemessen mit Godot 4.7.2: 3 Frames, 1,5 Pixel. Mit dem Platzhalter 900 waren es 6 Frames und 3,75 Pixel. Doppelte Reibung halbiert hier die Frames und mehr als halbiert den Weg. + resources: + - title: Godot Docs · CharacterBody2D.move_and_slide + url: https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html#class-characterbody2d-method-move-and-slide + + - id: export-hints + type: gap + title: Hints für den Inspektor + prompt: | + Ergänze die Annotationen, damit der Inspektor einen Zähler von 0 bis 50, eine Gruppe „Anordnung“, eine Auswahlliste und einen Regler mit der Einheit px zeigt. + lang: gdscript + code: | + @⟦export_range⟧(0, 50) var count := 5 + + @⟦export_group⟧("Anordnung") + @⟦export_enum⟧("Kreis", "Raster") var layout := 0 + @export_range(0, 200, 1, "⟦suffix:px⟧") var radius := 60.0 + gaps: + - label: Bereich + hint: Die Annotation für Grenzen und Schrittweite. + - label: Gruppe + hint: Alle folgenden Variablen landen in einem aufklappbaren Abschnitt. + - label: Auswahlliste + hint: Zeigt Namen, speichert eine Zahl. + - label: Einheit + hint: Ein Hint-Text mit Doppelpunkt, gefolgt von der Einheit. + explanation: | + `@export_range` begrenzt Regler und Eingabe im Editor, `@export_group` ordnet die folgenden Variablen in einen Abschnitt, `@export_enum` zeigt eine Liste und speichert den Index (Raster ist 1), und `"suffix:px"` hängt die Einheit an. Alle vier ändern nur die Anzeige im Editor; der Code kann jeden Wert setzen. + resources: + - title: Godot Docs · Adding suffixes + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#adding-suffixes-and-handling-degrees-radians + - title: Godot Docs · Exporting enums + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#exporting-enums + + - id: inspektor-lesen + type: choice + title: Was der Inspektor verrät + prompt: | + So zeigt der Inspektor den Spawner aus der Lektion. Welche Aussagen stimmen? + + ![Inspektor des Spawner: Pickup Path, Target Objects mit Zurücksetzen-Pfeil, Count 8 mit Zurücksetzen-Pfeil, Gruppe Anordnung mit Layout Raster, Untergruppe Kreis mit Radius 60 px, Untergruppe Raster mit Spacing 24 px](/media/godot/aktionen-parameter/spawner-inspektor.png "Godot 4.7.2, Inspektor des Spawner") + options: + - text: In der `.tscn` steht `layout = 1`, nicht der Text „Raster“. + correct: true + why: "`@export_enum` zeigt Namen, speichert aber den Index. Raster ist der zweite Eintrag, Index 1." + - text: Target und Count stehen als eigene Zeilen in der `.tscn`, Radius und Spacing nicht. + correct: true + why: Der Zurücksetzen-Pfeil zeigt Abweichungen vom Standard. Nur sie schreibt Godot in die Datei. + - text: Kreis und Raster sind zwei Ressourcen, die sich alle Spawner teilen. + correct: false + why: "`@export_subgroup` ordnet nur die Anzeige. Die Werte gehören dem Node." + - text: Radius kann zur Laufzeit nie größer als 200 werden. + correct: false + why: Die Grenzen von `export_range` gelten nur im Editor. Mit Godot 4.7.2 geprüft, `radius = 500.0` im Code ergibt 500. + explanation: | + Den Inspektor liest du wie eine Datei: Zurücksetzen-Pfeile markieren gespeicherte Abweichungen, Auswahllisten speichern Zahlen, Gruppen und Untergruppen sind nur Ordnung. In der gespeicherten Testszene stehen unter dem Spawner `target = NodePath("../Objects")`, `count = 8` und `layout = 1`, aber weder `radius` noch `spacing`. Grenzen und Einheiten sind Hilfen für die Eingabe, keine Prüfung zur Laufzeit. + resources: + - title: Godot Docs · Grouping exports + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#grouping-exports + + - id: aktion-tippfehler + type: bug + title: Nach rechts geht nichts + prompt: | + Die Figur läuft nach links, oben und unten, aber nicht nach rechts, weder mit D noch mit dem Stick. Die Ausgabe zeigt in jedem Frame: + + ```text + ERROR: The InputMap action "move_rigth" doesn't exist. Did you mean "move_right"? + ``` + + Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + func _physics_process(delta: float) -> void: + var input := Input.get_vector( + &"move_left", &"move_rigth", + &"move_up", &"move_down") + var rate := stats.acceleration if input != Vector2.ZERO else stats.friction + velocity = velocity.move_toward(input * stats.max_speed, rate * delta) + move_and_slide() + lines: [3] + fix: + - line: 3 + answers: + - '&"move_left", &"move_right",' + - '"move_left", "move_right",' + - '&"move_left", "move_right",' + - '"move_left", &"move_right",' + explanation: | + Die Aktion heißt `move_right`. Mit einem Namen, den die Eingabe-Zuordnung nicht kennt, liefert die Abfrage 0, und Godot meldet den Fehler, statt das Spiel anzuhalten. Die Meldung ist mit Godot 4.7.2 aufgenommen und nennt den richtigen Namen gleich mit. Prüf bei Eingabefehlern zuerst die Ausgabe, dann die Eingabe-Zuordnung. + resources: + - title: Godot Docs · Input.get_vector + url: https://docs.godotengine.org/en/stable/classes/class_input.html#class-input-method-get-vector + + - id: ausweichen-dauerfeuer + type: bug + title: Ein Druck, dreißig Ausweichschritte + prompt: | + Ein Testskript soll zählen, wie oft du ausweichst. Hältst du die Taste eine halbe Sekunde, steigt der Zähler aber um etwa 30. Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + extends Node + + ## Testwerkzeug: zählt, wie oft die Aktion dodge auslöst. + + var dodges := 0 + + + func _physics_process(_delta: float) -> void: + if Input.is_action_pressed(&"dodge"): + dodges += 1 + print("dodge ", dodges) + lines: [9] + fix: + - line: 9 + answers: + - 'if Input.is_action_just_pressed(&"dodge"):' + - 'if Input.is_action_just_pressed("dodge"):' + explanation: | + `is_action_pressed()` ist in jedem Physik-Frame wahr, in dem die Taste unten ist. Eine halbe Sekunde sind 30 Frames bei 60 pro Sekunde. `is_action_just_pressed()` ist nur im ersten Frame wahr, gemessen mit Godot 4.7.2: drei Frames gehalten, einmal wahr. Derselbe Fehler steckt oft in Angriffen und Menüs, die beim Halten mehrfach auslösen. + resources: + - title: Godot Docs · Input.is_action_just_pressed + url: https://docs.godotengine.org/en/stable/classes/class_input.html#class-input-method-is-action-just-pressed + + - id: remote-oder-lokal + type: explain + title: Warum der Wert weg ist + prompt: | + Du hast im laufenden Spiel im Inspektor aus dem Bild unter Stats `Max Speed` der Figur von 90 auf 120 gestellt, das fühlte sich gut an. Nach dem Beenden steht wieder 90 da. Erkläre in drei bis fünf Sätzen, warum, und wie du den Wert beim nächsten Mal behältst. + + ![Szenen-Dock im Modus Remote mit ausgewähltem Player, darunter der Inspektor Remote-Player mit aufgeklapptem Stats und Max Speed 90 px/s](/media/godot/aktionen-parameter/remote-player.png "Godot 4.7.2, Remote-Baum und Remote-Inspektor") + lang: text + code: | + Szenen-Dock Remote | Lokal + Inspektor Remote-Player: … + Debuggen Szenenänderungen synchronisieren + points: + - Im Modus Remote ändert der Inspektor das laufende Spiel, keine Datei; beim Beenden ist die Änderung weg. + - Der Name „Remote-Player“ im Inspektor zeigt, dass du am laufenden Spiel arbeitest. + - Bleiben soll ein Wert, wenn du ihn im Modus Lokal (hier in `player_stats.tres`) änderst; mit „Szenenänderungen synchronisieren“ wirkt das sofort im laufenden Spiel. + - Auf der Platte steht der Wert erst nach dem Speichern, etwa mit Szene speichern. + explanation: | + Der Name „Remote-Player“ im Inspektor und der gedrückte Knopf „Remote“ zeigen es: Du hast das laufende Spiel bearbeitet, nicht die Datei. Der Remote-Inspektor zeigt Objekte im laufenden Spielprozess. Was du dort änderst, lebt nur in diesem Prozess, die Doku warnt ausdrücklich, dass es nach dem Stoppen nicht erhalten bleibt. Willst du live abstimmen und den Wert behalten, wählst du im Modus Lokal die Datei oder den Node, hier `player_stats.tres`, und änderst dort. Mit Godot 4.7.2 geprüft: Mit „Szenenänderungen synchronisieren“, das in einem neuen Editor an ist, kam die Änderung innerhalb einer Sekunde im Spiel an, und nach Szene speichern stand sie in der `.tres`. + resources: + - title: Godot Docs · Game embedding · Interaction mode + url: https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#interaction-mode + - title: Godot Docs · Synchronize Scene Changes + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#synchronize-scene-changes + + - id: parameter-steckbrief + type: practice + title: Steckbrief für einen Parameter + prompt: | + Such dir einen exportierten Parameter des Testgeländes aus, etwa `acceleration`, `friction` oder `value`, und schreib mit den Dateien aus der Lektion einen kurzen Steckbrief: wo er lebt, in welcher Einheit, was er pro Frame bewirkt, wie du die Wirkung misst und welcher Wert gerade eingetragen ist. Ob der Wert gut ist, gehört nicht in den Steckbrief; das entscheidest du später beim Spielen. + lang: prose + checklist: + - Datei und Bezeichner stehen da, etwa `player_stats.tres` und `acceleration`, und ob eine Instanz oder `_ready()` den Wert überschreibt. + - Die Einheit steht da und, wo es passt, die Umrechnung pro Physik-Frame. + - Du hast eine Größe notiert, an der du die Wirkung misst, zum Beispiel Frames bis zum Höchsttempo oder Bremsweg in Pixeln. + - Der aktuelle Wert ist als Platzhalter oder als deine Entscheidung gekennzeichnet. + - Du hast notiert, wo du den Wert änderst, damit er nach dem Beenden erhalten bleibt. + hints: + - Im Testgelände eignen sich `acceleration` und `friction` gut, weil `debug_log` ihre Wirkung pro Frame zeigt. + explanation: | + Ein Steckbrief trennt Fakten von Geschmack. Wo der Wert lebt, wer ihn überschreibt und was er pro Frame tut, lässt sich prüfen; ob er sich gut anfühlt, prüfst du beim Spielen. Mit dem Steckbrief kannst du einem Coding-Agenten genau sagen, welchen Wert er nicht anfassen soll, und vor einer Änderung vorhersagen, was du messen wirst. + resources: + - title: Godot Docs · Resources + url: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html diff --git a/content/godot/modules/animationen.yaml b/content/godot/modules/animationen.yaml new file mode 100644 index 0000000..6c77811 --- /dev/null +++ b/content/godot/modules/animationen.yaml @@ -0,0 +1,548 @@ +id: animationen +title: "Animationen zuordnen: SpriteFrames, AnimationPlayer und Blickrichtung" +level: 2 +minutes: 150 +summary: Wie du Frames aus einem Spritesheet in SpriteFrames legst, Takt und Loop einstellst, wann ein AnimationPlayer die bessere Wahl ist und wie ein kleines Skript bei analoger 360°-Bewegung die passende Richtung wählt, ohne dass der Laufzyklus bei jedem Richtungswechsel neu startet. +place: pc +goals: + - Du legst in SpriteFrames Animationen aus einem Spritesheet an und stellst FPS, Loop und Frame-Dauer ein. + - Du rechnest aus FPS, Frame-Dauer und `speed_scale` aus, wie lange ein Frame und ein Zyklus dauern. + - Du entscheidest begründet zwischen `AnimatedSprite2D` und `AnimationPlayer`. + - Du erklärst, wie `player_animation.gd` aus `facing` und `velocity` eine von vier oder acht Richtungen wählt und warum Diagonalen auf der Grenze landen. + - Du rechnest nach, bei welcher FPS ein Laufzyklus zum Tempo der Figur passt, und erkennst Fußrutschen. +resources: + - title: Godot Docs · 2D sprite animation + url: https://docs.godotengine.org/en/stable/tutorials/2d/2d_sprite_animation.html + - title: Godot Docs · AnimatedSprite2D + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html + - title: Godot Docs · SpriteFrames + url: https://docs.godotengine.org/en/stable/classes/class_spriteframes.html + - title: Godot Docs · Animation Track types + url: https://docs.godotengine.org/en/stable/tutorials/animation/animation_track_types.html +lesson: | + Die Figur im Testgelände läuft analog in jede Richtung. Gezeichnet hast du aber nur vier Laufzyklen, vielleicht acht. Läuft die Figur im Winkel von 30°, muss ein Skript entscheiden, welcher Zyklus gilt. Wechselt die Richtung mitten im Schritt, soll der Zyklus weiterlaufen, statt neu anzufangen. Und läuft die Figur schneller, als die Beine in der Zeichnung ausgreifen, rutschen die Füße über den Boden. + + Dieses Modul baut die Brücke zwischen deinen Frames und der Bewegung: SpriteFrames und `AnimatedSprite2D` für die Bilder, `AnimationPlayer` für alles, was im selben Takt passieren muss, und ein kleines Skript für die Richtung. Statt einer Figur nimmst du einen Richtungsbogen: 64 × 64 Pixel, vier Zeilen zu je vier Feldern, jedes Feld mit Rasterlinie, Richtungsbuchstabe (R, D, L, U), Frame-Nummer, wanderndem Block und Fußmarke. Den legst du wie den Testbogen aus [Eigene Sprites importieren](/godot/sprites-importieren) selbst an. Namen, Richtungszahl, FPS und Schwellen hier sind Platzhalter. + + ## SpriteFrames: Bilder, Takt und Loop + + Ein `AnimatedSprite2D` zeigt Animationen aus einer Ressource vom Typ `SpriteFrames`. Darin stehen benannte Animationen, jede mit ihren Frames, einer Geschwindigkeit in FPS und einem Loop-Modus ([SpriteFrames](https://docs.godotengine.org/en/stable/classes/class_spriteframes.html)). Im Inspektor wählst du bei *Sprite Frames* eine neue SpriteFrames-Ressource; ein Klick darauf öffnet unten das Panel *SpriteFrames*. + + ![SpriteFrames-Panel: links die Liste der Animationen idle_down bis walk_up mit walk_down markiert, darüber 8.0 FPS und der Loop-Schalter; rechts die Frames 0 und 1 mit D1 und D2, darunter angeschnitten D4, und das Feld Frame-Dauer × 1.0](/media/godot/animationen/spriteframes-panel.png "Godot 4.7.2, SpriteFrames-Panel mit den Platzhalter-Animationen aus dem Richtungsbogen") + + Die Frames holst du mit *Frames aus Sprite-Sheet hinzufügen* (Kürzel Ctrl+Shift+O, auf deutscher Tastatur Strg+Umschalt+O). Im Dialog *Frames auswählen* stellst du Spalten und Zeilen ein und klickst die Felder an, die zur Animation gehören ([Sprite sheet with AnimatedSprite2D](https://docs.godotengine.org/en/stable/tutorials/2d/2d_sprite_animation.html#sprite-sheet-with-animatedsprite2d)). Anders als `hframes` am `Sprite2D` kennt der Dialog *Trennung* und *Versatz*, also Abstände zwischen Feldern und einen Rand. Jedes Feld wird dabei eine `AtlasTexture`. + + ![Einstellungen im Dialog Frames auswählen: Horizontal 4, Vertikal 4, Größe 16 px mal 16 px, Trennung 0 px, Versatz 0 px, Knopf Autoschnitt](/media/godot/animationen/frames-auswaehlen.png "Godot 4.7.2, Dialog Frames auswählen, Einstellungen für den Richtungsbogen") + + Drei Werte bestimmen den Takt. Die **FPS** der Animation, Standard 5. Die **Frame-Dauer** je Frame, ein Faktor, Standard 1,0. Und `speed_scale` am Node. Die Doku nennt die Formel ([get_frame_duration](https://docs.godotengine.org/en/stable/classes/class_spriteframes.html#class-spriteframes-method-get-frame-duration)): + + ```text + Dauer in Sekunden = Frame-Dauer / (FPS × |Abspieltempo|) + ``` + + Gemessen mit Godot 4.7.2 bei 60 Bildern pro Sekunde, vier Frames, `play()` im ersten Bild: + + | Einstellung | Frame wechselt bei Bild | Zyklus | + | --- | --- | --- | + | 8 FPS | 8, 15, 23, 30 | 0,5 s | + | 8 FPS, dritter Frame Dauer 2,0 | 8, 15, 30, 38 | 0,625 s | + | 8 FPS, `speed_scale` 0,5 | 15, 30, 45, 60 | 1,0 s | + + Bei 8 FPS steht ein Frame 7,5 Bilder lang, also abwechselnd 7 und 8. Der Loop-Modus heißt in Godot 4.7 `LoopMode`: aus, wiederholen oder Ping-Pong ([LoopMode](https://docs.godotengine.org/en/stable/classes/class_spriteframes.html#enum-spriteframes-loopmode)). In der Szene steht er als Zahl: + + ```ini + [sub_resource type="SpriteFrames" id="SpriteFrames_7ahtn"] + animations = [{ + "frames": [{ + "duration": 1.0, + "texture": SubResource("AtlasTexture_2hs0m") + }, …], + "loop": 1, + "name": &"walk_down", + "speed": 8.0 + }, …] + ``` + + `"loop": 0` heißt aus, `1` wiederholen, `2` Ping-Pong. Das `AnimatedSprite2D` zählt seine Frames im normalen Bildtakt (`_process`), nicht im Physik-Takt. Für Bilder reicht das. Was spielmechanisch auf den Frame genau sitzen muss, gehört in einen `AnimationPlayer` mit Physik-Takt. + + ## Abspielen aus Code: play, Signale und Phase + + `play(&"walk_down")` startet eine Animation ([play](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-method-play)). Drei Regeln musst du kennen, alle mit Godot 4.7.2 nachgemessen: + + - Ruft der Code jedes Bild `play()` mit demselben Namen auf, läuft die Animation weiter. Sie startet nicht neu. + - Wechselt der Name, springt das Sprite auf Frame 0. Das gilt auch, wenn du `animation` direkt setzt. + - `stop()` setzt auf Frame 0 zurück, `pause()` behält die Stelle. + + Die zweite Regel ist beim Richtungswechsel ein Problem: Aus `walk_right` in Frame 2 wird `walk_down` in Frame 0, die Figur stolpert. Die Lösung zeigt die Doku selbst: Stelle und Fortschritt merken, wechseln, zurücksetzen mit `set_frame_and_progress()` ([set_frame_and_progress](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-method-set-frame-and-progress)). Gemessen: vorher `walk_right frame 2 progress 0.53`, danach `walk_down frame 2 progress 0.53` statt Frame 0. + + > [!MERKE] + > Gleicher Name: läuft weiter. Neuer Name: Frame 0. Wer die Phase behalten will, setzt sie nach dem Wechsel mit `set_frame_and_progress()` zurück. + + Signale melden den Ablauf. `animation_finished` kommt nur bei Animationen ohne Loop, am Ende; bei einer Schleife kommt stattdessen `animation_looped` ([animation_finished](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-signal-animation-finished)). `frame_changed` kommt bei jedem neuen Frame; daran hängst du später Schrittgeräusche ([Audio zuordnen](/godot/audio)). Mit *Autoplay beim Laden* im Panel (Eigenschaft `autoplay`) startet eine Animation, sobald die Szene geladen ist. + + ## AnimationPlayer: wenn mehr als das Bild wechselt + + Ein `AnimatedSprite2D` kann eines: Bilder im Takt wechseln. Ein `AnimationPlayer` setzt beliebige Eigenschaften beliebiger Nodes zu festen Zeiten ([Introduction to the animation features](https://docs.godotengine.org/en/stable/tutorials/animation/introduction.html)). Im Animations-Panel fügst du mit *Track hinzufügen* Spuren hinzu; die Spurtypen heißen auch im deutschen Editor englisch ([Animation Track types](https://docs.godotengine.org/en/stable/tutorials/animation/animation_track_types.html)): + + | Spur | Was sie tut | Beispiel im Testgelände | + | --- | --- | --- | + | Property Track | setzt eine Eigenschaft | `frame` eines `Sprite2D`, `disabled` einer Form | + | Call Method Track | ruft eine Methode zu einem Zeitpunkt | `queue_free()` am Ende | + | Audio Playback Track | spielt einen Ton an einem `AudioStreamPlayer` | Schritt bei Frame 2 | + | Animation Playback Track | startet eine Animation eines anderen Players | verschachtelte Abläufe | + + Faustregel: Reine Bildfolgen (Laufen, Stehen) laufen im `AnimatedSprite2D`. Muss etwas anderes im selben Takt passieren, etwa eine Hitbox, ein Ton oder ein Methodenaufruf, nimm einen `AnimationPlayer`. Er kann den `frame` eines `Sprite2D` mitschalten, wie die Doku zeigt ([Sprite sheet with AnimationPlayer](https://docs.godotengine.org/en/stable/tutorials/2d/2d_sprite_animation.html#sprite-sheet-with-animationplayer)). Wie du ihn auf den Physik-Takt stellst und Keys aufs 1/60-Raster legst, steht in [Godot-Start](/combat/godot-start). Methodenspuren laufen in der Vorschau im Editor nicht, nur im Spiel ([Call Method Track](https://docs.godotengine.org/en/stable/tutorials/animation/animation_track_types.html#call-method-track)). + + Für Webentwickler: `AnimatedSprite2D` ist eine CSS-Sprite-Animation mit `steps()`. `AnimationPlayer` gleicht einer Zeitleiste mit Keyframes auf beliebigen Eigenschaften, wie die Web Animations API. Der Vergleich endet bei Methoden- und Tonspuren, dafür hat CSS nichts. + + ## Blickrichtung bei 360°-Bewegung + + `player.gd` merkt sich in `facing` die letzte Eingaberichtung. Sie bleibt stehen, wenn du loslässt; deshalb schaut die Figur im Stand dorthin, wohin sie zuletzt lief. Aus dem Winkel des Vektors wird eine Richtung: `Vector2.angle()` liefert den Winkel im Bogenmaß, 0 nach rechts, positiv im Uhrzeigersinn, weil y in Godot nach unten zeigt ([angle](https://docs.godotengine.org/en/stable/classes/class_vector2.html#class-vector2-method-angle)). Teilst du durch ein Viertel des Vollkreises und rundest, bekommst du 0 bis 3 für rechts, unten, links, oben. + + Ein eigenes Skript am `AnimatedSprite2D` erledigt das. Es liest nur, was der Player schon hat, und ändert `player.gd` nicht. Hänge es an den Node `Sprite` unter dem Player. Aus dem `Sprite2D` des letzten Moduls machst du mit Rechtsklick → *Typ ändern...* ein `AnimatedSprite2D`; der Name bleibt. Prüf danach im Inspektor, ob `offset` noch auf (0, −8) steht. + + ```gdscript title=player_animation.gd + extends AnimatedSprite2D + + ## Wählt die Animation aus Tempo und Blickrichtung des Players (Elternknoten). + ## Namen, Schwelle und Zahl der Richtungen sind Platzhalter: Du legst sie fest. + + @export var eight_directions := false ## aus: 4 Richtungen, an: 8 + @export var mirror_left := false ## an: "left" entsteht gespiegelt aus "right" + @export_range(0, 100, 1, "suffix:px/s") var walk_threshold := 5.0 ## darunter gilt "idle" + + const NAMES_4: Array[String] = ["right", "down", "left", "up"] + const NAMES_8: Array[String] = ["right", "down_right", "down", "down_left", "left", "up_left", "up", "up_right"] + + @onready var player: Player = get_parent() + + + func _process(_delta: float) -> void: + var state := "walk" if player.velocity.length() > walk_threshold else "idle" + var dir_name := direction_name(player.facing) + flip_h = mirror_left and dir_name.contains("left") + if flip_h: + dir_name = dir_name.replace("left", "right") + var next := StringName(state + "_" + dir_name) + if next == animation and is_playing(): + return + var same_state := String(animation).begins_with(state) + var keep_frame := frame + var keep_progress := frame_progress + play(next) + if same_state: + set_frame_and_progress(keep_frame, keep_progress) + + + func direction_name(dir: Vector2) -> String: + var names := NAMES_8 if eight_directions else NAMES_4 + var step := TAU / names.size() + return names[wrapi(roundi(dir.angle() / step), 0, names.size())] + ``` + + Bei „oben“ ergibt die Rundung −1. `wrapi()` faltet das auf 3 ([wrapi](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-wrapi)). GDScript liest einen negativen Index zwar vom Ende des Arrays und träfe hier dasselbe, gemessen `names[-1]` = `up`; mit `wrapi()` steht die Absicht im Code, statt an einer Spracheigenheit zu hängen. Läuft die Figur von rechts nach unten weiter, behält das Skript die Phase; wechselt sie von `idle` zu `walk`, beginnt der Zyklus bei Frame 0. + + Die Tastatur liefert Diagonalen mit genau 45°. Das ist genau die Grenze zwischen zwei Richtungen, und `roundi()` rundet ,5 von der Null weg. Gemessen mit vier Richtungen: + + | Tasten | `facing` | Animation | + | --- | --- | --- | + | rechts + unten | (0.707107, 0.707107) | `walk_down` | + | links + unten | (-0.707107, 0.707107) | `walk_left` | + | links + oben | (-0.707107, -0.707107) | `walk_left` | + | rechts + oben | (0.707107, -0.707107) | `walk_up` | + + Zwei Diagonalen landen senkrecht, zwei waagrecht. Mit dem Stick trifft man die Grenze selten genau. Was auf der Diagonale gelten soll, ist eine Entscheidung für dein Spiel: acht Richtungen zeichnen, eine Achse bevorzugen oder die alte Richtung behalten. Mit `eight_directions` an braucht das Skript acht Animationen je Zustand. Fehlen sie, meldet Godot jedes Bild: + + ```text + ERROR: There is no animation with name 'walk_down_right'. + at: play (scene/2d/animated_sprite_2d.cpp:505) + GDScript backtrace (most recent call first): + [0] _process (res://player_animation.gd:28) + ``` + + `flip_h` spiegelt das ganze Feld. Am Richtungsbogen siehst du sofort, was das heißt: Aus dem R wird ein gespiegeltes R. + + ![Drei Felder: walk_right mit R1, dasselbe Feld mit flip_h und gespiegelter Schrift, walk_left mit eigenem L1](/media/godot/animationen/flip-h@2x.png "Godot 4.7.2, Richtungsbogen mit und ohne flip_h, achtfach vergrößert") + + Bei einer Figur spiegeln sich genauso Seitenscheitel, Waffenhand und Licht. Ob dich das stört oder ob du links eigene Frames zeichnest, entscheidest du; `mirror_left` macht beides möglich. + + > [!INTERVIEW] + > „Wie wählst du in einem Top-down-Spiel mit Analogstick die Animation aus?“ Gute Antwort, etwa als Gameplay Programmer: Ich trenne Zustand und Richtung. Der Zustand kommt aus dem Tempo mit einer kleinen Schwelle, die Richtung aus dem Winkel des letzten Eingabevektors, auf vier oder acht Sektoren gerundet. Beim Richtungswechsel übernehme ich Frame und Fortschritt, sonst stolpert der Zyklus. Auf den Sektorgrenzen, etwa bei Tastatur-Diagonalen, lege ich bewusst fest, welche Richtung gewinnt, und spiegle nur, wenn die Grafik es verträgt. + + ## Laufzyklus und Tempo: Fußrutschen ausrechnen + + Ein Laufzyklus zeigt eine Strecke: In deiner Zeichnung kommt die Figur pro Zyklus um eine bestimmte Zahl Pixel voran, gemessen am Fuß, der gerade am Boden steht. Das Spiel bewegt sie unabhängig davon mit `velocity`. Passen beide nicht zusammen, rutscht der Fuß über den Boden. + + ```text + Zyklusdauer = Summe der Frame-Dauern / FPS 4 / 8 = 0,5 s + Strecke = Tempo × Zyklusdauer 90 × 0,5 = 45 px + passende FPS = Tempo × Summe der Frame-Dauern / Schrittweg + ``` + + Zeigt deine Zeichnung 32 Pixel Weg pro Zyklus, passen bei 90 px/s und vier Frames 90 × 4 / 32 = 11,25 FPS. Bei 8 FPS legt die Figur 45 Pixel zurück, während die Füße nur 32 zeigen: Sie gleitet vorwärts. Bei 15 FPS wären es 24 Pixel, die Füße rutschen nach hinten. + + Ändert sich das Tempo, etwa beim Anlaufen, kannst du den Zyklus mitziehen: `speed_scale = player.velocity.length() / reference_speed`, wobei `reference_speed` das Tempo ist, für das der Zyklus gezeichnet wurde ([speed_scale](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-property-speed-scale)). Ob die Figur leicht gleiten darf, entscheidest du beim Zusehen. Die Rechnung sagt dir nur, wo du anfängst. Zum Beobachten Bild für Bild nimmst du die Werkzeuge aus [Tuning als Experiment](/combat/tuning). + + > [!PRAXIS] + > Animationsnamen sind ein Vertrag zwischen Code und Grafik. Schreib die Liste (`idle_down`, `walk_down`, …) in die Aufgabe für den Coding-Agenten und neben deine Quelldateien, bevor du zeichnest. Ein Tippfehler im Namen fällt sonst erst als Fehlermeldung im laufenden Spiel auf. + + ## Häufige Fehler + + - **Zyklus startet bei jedem Richtungswechsel neu.** Der Name wechselt, das Sprite springt auf Frame 0. Phase mit `set_frame_and_progress()` übernehmen. + - **Figur hängt auf Frame 0.** Der Code ruft jedes Bild `stop()` und `play()` oder setzt `frame = 0`. Nur bei einem neuen Namen etwas tun. + - **`animation_finished` kommt nie.** Die Animation hat Loop an. Für einmalige Abläufe Loop aus (`"loop": 0`). + - **Fehlermeldung „There is no animation with name …“.** Name falsch geschrieben, oder das Skript erwartet acht Richtungen und SpriteFrames hat vier. + - **Diagonalen zeigen unerwartete Richtungen.** Tastatur-Diagonalen liegen genau auf der Sektorgrenze. Regel festlegen statt der Rundung zu überlassen. + - **Füße rutschen.** FPS und Tempo passen nicht zum Schrittweg der Zeichnung. Nachrechnen. + - **Gameplay hängt am `AnimatedSprite2D`.** Es zählt im Bildtakt. Hitboxen und Ähnliches gehören in einen `AnimationPlayer` mit Physik-Takt. + + ## Zusammenfassung + + - SpriteFrames hält benannte Animationen mit FPS (Standard 5), Loop-Modus (0 aus, 1 wiederholen, 2 Ping-Pong) und Frame-Dauer je Frame. + - Dauer eines Frames = Frame-Dauer / (FPS × Abspieltempo). Bei 8 FPS und 60 Bildern pro Sekunde steht ein Frame 7 oder 8 Bilder. + - Gleicher Name läuft weiter, neuer Name beginnt bei Frame 0; `set_frame_and_progress()` behält die Phase. + - `animation_finished` gibt es nur ohne Loop; `frame_changed` kommt bei jedem Frame. + - `AnimationPlayer` für alles, was neben dem Bild im Takt passieren muss. + - Richtung = Winkel von `facing`, gerundet auf vier oder acht Sektoren. Tastatur-Diagonalen liegen auf der Grenze. + - Passende FPS = Tempo × Frames / Schrittweg. Namen, Richtungszahl, Spiegeln und FPS entscheidest du. +exercises: + - id: sprite-oder-player + type: choice + title: Bildfolge oder Zeitleiste? + prompt: | + Welche Aufgaben gehören in einen `AnimationPlayer` statt in ein `AnimatedSprite2D`? Mehrere Antworten richtig. + options: + - text: Eine Hitbox genau in Frame 9 bis 11 eines Angriffs einschalten + correct: true + why: Das ist eine Eigenschaft eines anderen Nodes, die im Physik-Takt auf den Frame genau schalten muss. Das kann nur der `AnimationPlayer`. + - text: Einen Ton genau in dem Frame abspielen, in dem auch die Hitbox angeht + correct: true + why: Ton und Hitbox müssen im selben Takt sitzen. In einer Zeitleiste liegen beide als Spuren nebeneinander, die Audio Playback Track spielt den Ton. + - text: Vier Frames eines Laufzyklus in Schleife zeigen + correct: false + why: Eine reine Bildfolge ist genau das, wofür `AnimatedSprite2D` und SpriteFrames da sind. + - text: Im Physik-Takt genau in Frame 9 eines Angriffs eine Methode aufrufen, die ein Projektil erzeugt + correct: true + why: Eine Call Method Track ruft die Methode zu einem festen Zeitpunkt, und mit `callback_mode_process = Physics` liegt er auf dem Physik-Frame. Das `AnimatedSprite2D` zählt im Bildtakt. + explanation: | + `AnimatedSprite2D` wechselt Bilder, sonst nichts. Sobald neben dem Bild etwas anderes passieren muss, ein Ton, eine Form, ein Methodenaufruf, legst du es in eine Zeitleiste. Der `AnimationPlayer` kann zusätzlich im Physik-Takt laufen, was für spielmechanisches Timing zählt. Viele Projekte kombinieren beides: SpriteFrames für Laufen und Stehen, einen `AnimationPlayer` für Angriffe. + resources: + - title: Godot Docs · Animation Track types + url: https://docs.godotengine.org/en/stable/tutorials/animation/animation_track_types.html + + - id: frames-aus-sheet + type: order + title: Vom Richtungsbogen zur Laufanimation + prompt: | + Bring die Schritte in eine sinnvolle Reihenfolge, um `walk_down` aus dem Richtungsbogen anzulegen. + lang: prose + lines: | + Am Node Sprite (AnimatedSprite2D) bei Sprite Frames eine neue SpriteFrames-Ressource anlegen + Die Ressource anklicken, damit unten das Panel SpriteFrames erscheint + Eine Animation anlegen und in walk_down umbenennen + Frames aus Sprite-Sheet hinzufügen wählen und den Richtungsbogen öffnen + Horizontal 4 und Vertikal 4 einstellen und die vier Felder der Zeile D anklicken + Die Frames hinzufügen, dann FPS und Loop der Animation einstellen + Im laufenden Spiel nach unten laufen und die Nummern 1 bis 4 nacheinander sehen + explanation: | + Ohne SpriteFrames-Ressource gibt es kein Panel, ohne Animation keinen Ort für die Frames. Im Dialog bestimmen Spalten und Zeilen das Raster; die gewählten Felder werden zu AtlasTextures. FPS und Loop gehören zur Animation, nicht zum Frame, deshalb kommen sie danach. Der Test im Spiel zeigt, ob das Skript die Animation auch auswählt. + resources: + - title: Godot Docs · 2D sprite animation · Sprite sheet with AnimatedSprite2D + url: https://docs.godotengine.org/en/stable/tutorials/2d/2d_sprite_animation.html#sprite-sheet-with-animatedsprite2d + + - id: diagonalen-vorhersagen + type: output + title: Welche Richtung gewinnt? + prompt: | + `direction_name()` aus `player_animation.gd`, mit `eight_directions = false`. Was geben die drei Zeilen aus? + lang: gdscript + code: | + print(direction_name(Vector2(1, 1).normalized())) + print(direction_name(Vector2(-1, -1).normalized())) + print(direction_name(Vector2(0.9, 0.3))) + expected: | + down + left + right + hints: + - Rechne den Winkel in Vierteln des Vollkreises aus und runde. 0 ist rechts, 1 unten, 2 links, 3 oben. + - "`roundi(0.5)` ist 1, `roundi(-1.5)` ist −2." + explanation: | + Rechts unten hat 45°, also 0,5 Viertel; `roundi()` rundet auf 1, das ist `down`. Links oben hat −135°, also −1,5 Viertel, gerundet −2, und `wrapi()` macht daraus 2, `left`. `Vector2(0.9, 0.3)` hat gut 18°, gerundet 0, `right`. Gemessen mit Godot 4.7.2. Mit acht Richtungen gibt die erste Zeile `down_right` aus. + resources: + - title: Godot Docs · Vector2.angle + url: https://docs.godotengine.org/en/stable/classes/class_vector2.html#class-vector2-method-angle + - title: Godot Docs · @GlobalScope.roundi + url: https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-roundi + + - id: phase-beim-wechsel + type: output + title: Wo steht der Zyklus nach dem Wechsel? + prompt: | + Das Sprite spielt `walk_right` und steht gerade in Frame 2. Dann läuft dieser Code. Was gibt er aus? + lang: gdscript + code: | + sprite.play(&"walk_down") + print(sprite.animation, " ", sprite.frame) + sprite.play(&"walk_down") + print(sprite.animation, " ", sprite.frame) + expected: | + walk_down 0 + walk_down 0 + hints: + - Was passiert mit dem Frame, wenn sich der Name ändert? + - Und was, wenn `play()` mit dem Namen kommt, der schon läuft? + explanation: | + Der erste Aufruf wechselt den Namen, das Sprite springt auf Frame 0. Der zweite nennt denselben Namen; die Animation läuft weiter und bleibt in diesem Moment bei Frame 0. Gemessen mit Godot 4.7.2. Soll der Laufzyklus beim Richtungswechsel nicht neu beginnen, merkst du dir vorher `frame` und `frame_progress` und setzt beides mit `set_frame_and_progress()` zurück; dann stünde in beiden Zeilen Frame 2. + resources: + - title: Godot Docs · AnimatedSprite2D.play + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-method-play + + - id: zyklus-dauer + type: output + title: Wie lang ist der Zyklus? + prompt: | + Eine Animation hat vier Frames, 8 FPS und Loop an. Der dritte Frame hat die Frame-Dauer 2,0, alle anderen 1,0. `speed_scale` ist 1. Wie viele Sekunden dauert ein Durchlauf? + lang: text + code: | + FPS: 8 + Frame-Dauern: 1,0 1,0 2,0 1,0 + expected: | + 0,625 + accept: + - 0,625 s + - 0,625s + - 0.625 + - 0.625 s + - 0.625s + - 0,625 Sekunden + hints: + - Jeder Frame dauert Frame-Dauer / FPS Sekunden. + explanation: | + Die Frame-Dauern addieren sich zu 5,0. Geteilt durch 8 FPS ergibt das 0,625 s. Gemessen mit Godot 4.7.2 bei 60 Bildern pro Sekunde: Der dritte Frame steht 15 statt 7 oder 8 Bilder, der Zyklus dauert 37 bis 38 Bilder. Die Frame-Dauer ist das Werkzeug für Haltebilder, etwa einen Kontaktmoment, den du länger zeigen willst. + resources: + - title: Godot Docs · SpriteFrames.get_frame_duration + url: https://docs.godotengine.org/en/stable/classes/class_spriteframes.html#class-spriteframes-method-get-frame-duration + + - id: passende-fps + type: output + title: Bei welcher FPS rutscht nichts? + prompt: | + Ein Laufzyklus mit vier Frames gleicher Dauer zeigt 30 Pixel Weg. Die Figur läuft mit 90 px/s (Platzhalter). Bei welcher FPS passt der Zyklus zum Tempo? Antworte mit der Zahl. + lang: text + code: | + Frames: 4 + Schrittweg: 30 px pro Zyklus + Tempo: 90 px/s + expected: | + 12 + accept: + - 12 FPS + - 12 fps + - 12FPS + - "12,0" + - "12.0" + hints: + - In der passenden Zyklusdauer legt die Figur genau 30 Pixel zurück. + explanation: | + 90 px/s brauchen für 30 Pixel genau 1/3 Sekunde. Vier Frames in 1/3 Sekunde sind 12 FPS, kurz: 90 × 4 / 30 = 12. Mit der Standard-FPS von 5 dauerte der Zyklus 0,8 s, die Figur käme 72 Pixel weit, während die Füße 30 zeigen. Ob du die FPS anpasst, das Tempo oder die Zeichnung, entscheidest du. + resources: + - title: Godot Docs · AnimatedSprite2D.speed_scale + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-property-speed-scale + + - id: richtung-ergaenzen + type: gap + title: Vom Winkel zum Namen + prompt: | + Ergänze `direction_name()`, so dass sie für vier oder acht Namen funktioniert: Der Vollkreis wird in gleich große Sektoren geteilt, der Winkel gerundet und in den gültigen Bereich gefaltet. + lang: gdscript + code: | + func direction_name(dir: Vector2) -> String: + var names := NAMES_8 if eight_directions else NAMES_4 + var step := ⟦TAU⟧ / names.size() + return names[⟦wrapi⟧(roundi(dir.⟦angle⟧() / step), 0, names.size())] + gaps: + - label: Vollkreis + hint: Die Konstante für 2π. + accept: ["2 * PI", "PI * 2"] + - label: In den Bereich falten + hint: Eine Funktion mit Wert, Untergrenze und Obergrenze, die −1 auf das Ende des Bereichs zurückführt. + - label: Winkel des Vektors + hint: Liefert den Winkel zur x-Achse im Bogenmaß. + explanation: | + `TAU` ist der Vollkreis im Bogenmaß; geteilt durch vier ergibt sich ein Sektor von 90°, durch acht einer von 45°. `angle()` liefert −π bis π, also auch negative Werte; `wrapi()` macht aus −1 die 3 und aus −2 die 2. GDScript würde negative Indizes vom Ende des Arrays lesen und hier dasselbe treffen, aber das wäre ein Zufall der Sprache, keine Absicht im Code. + resources: + - title: Godot Docs · @GlobalScope.wrapi + url: https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-wrapi + + - id: haengt-auf-frame-null + type: bug + title: Die Figur läuft auf der Stelle + prompt: | + Ein Coding-Agent hat diese Fassung geschrieben. Die Figur zeigt beim Laufen nur das Feld mit der Nummer 1, der Block auf dem Richtungsbogen rückt nie weiter. Fehlermeldungen gibt es keine. Finde die Zeile, die das verursacht. + lang: gdscript + code: | + func _process(_delta: float) -> void: + var state := "walk" if player.velocity.length() > walk_threshold else "idle" + var next := StringName(state + "_" + direction_name(player.facing)) + stop() + play(next) + lines: [4] + explanation: | + `stop()` setzt die Animation auf Frame 0 zurück, und `play()` startet sie neu. Weil das jedes Bild passiert, kommt sie nie über Frame 0 hinaus. Gemessen mit Godot 4.7.2: Mit `stop()` steht nach 10, 20 und 30 Bildern Frame 0, ohne `stop()` Frame 1, 2 und 3. Die Zeile muss weg; `play()` mit demselben Namen lässt die Animation weiterlaufen. Ein ähnliches Muster ist `frame = 0` in `_process`. + resources: + - title: Godot Docs · AnimatedSprite2D.stop + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-method-stop + + - id: acht-ohne-acht + type: bug + title: Fehlermeldung bei jeder Diagonale + prompt: | + Läuft die Figur diagonal, füllt sich die Ausgabe mit `ERROR: There is no animation with name 'walk_down_right'.`, und die Figur behält das alte Bild. SpriteFrames enthält `idle_*` und `walk_*` für right, down, left und up. Hier ist der Node aus `player.tscn`. Finde die Zeile und korrigiere sie, ohne neue Animationen anzulegen. + lang: ini + code: | + [node name="Sprite" type="AnimatedSprite2D" parent="." unique_id=942859769] + sprite_frames = SubResource("SpriteFrames_7ahtn") + animation = &"idle_down" + offset = Vector2(0, -8) + script = ExtResource("4_sweqy") + eight_directions = true + lines: [6] + fix: + - line: 6 + answers: + - eight_directions = false + explanation: | + Mit `eight_directions = true` rundet das Skript auf acht Sektoren und verlangt Namen wie `walk_down_right`. Die gibt es nicht, `play()` meldet den Fehler jedes Bild und lässt die alte Animation stehen. Mit `false` (oder ohne die Zeile, weil das der Standard ist) rundet das Skript auf vier Richtungen. Die Meldung ist mit Godot 4.7.2 nachgestellt. Ob du acht Richtungen zeichnest, entscheidest du; dann muss SpriteFrames sie auch enthalten. + resources: + - title: Godot Docs · AnimatedSprite2D.play + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-method-play + + - id: finished-kommt-nie + type: bug + title: Das Signal kommt nie + prompt: | + Ein Skript spielt die Platzhalter-Animation `test_once` und wartet mit `await sprite.animation_finished` auf ihr Ende. Die Animation läuft, aber der Code danach wird nie ausgeführt. Hier ist der Ausschnitt aus der SpriteFrames-Ressource. Finde die Zeile und korrigiere sie. + lang: ini + code: | + }, { + "frames": [{ + "duration": 1.0, + "texture": SubResource("AtlasTexture_2hs0m") + }], + "loop": 1, + "name": &"test_once", + "speed": 8.0 + }, { + lines: [6] + fix: + - line: 6 + answers: + - '"loop": 0,' + - '"loop": 0' + explanation: | + `"loop": 1` heißt wiederholen. Eine Schleife hat kein Ende, also gibt es kein `animation_finished`, sondern bei jedem Durchlauf `animation_looped`. Mit `"loop": 0` läuft die Animation einmal und meldet sich am Ende. Gemessen mit Godot 4.7.2: vier Frames bei 8 FPS ohne Loop melden `animation_finished` nach 30 Bildern, also 0,5 s; mit Loop kommt nur `animation_looped`. Im Panel ist das der Schalter *Animations-Looping*. + resources: + - title: Godot Docs · AnimatedSprite2D.animation_finished + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-signal-animation-finished + + - id: phase-erklaeren + type: explain + title: Warum merkt sich das Skript den Frame? + prompt: | + Erkläre in wenigen Sätzen, was diese Zeilen aus `player_animation.gd` bewirken und was ohne die letzten drei Zeilen passieren würde. + lang: gdscript + code: | + var next := StringName(state + "_" + dir_name) + if next == animation and is_playing(): + return + var same_state := String(animation).begins_with(state) + var keep_frame := frame + var keep_progress := frame_progress + play(next) + if same_state: + set_frame_and_progress(keep_frame, keep_progress) + points: + - Läuft schon die richtige Animation, kehrt das Skript sofort zurück und spart sich `play()` und `set_frame_and_progress()`; neu starten würde sie auch ohne diese Abfrage nicht. + - Bei einem neuen Namen setzt `play()` das Sprite auf Frame 0. + - Bleibt der Zustand gleich (etwa `walk` zu `walk`), übernimmt das Skript Frame und Fortschritt; der Laufzyklus geht beim Richtungswechsel nahtlos weiter. + - Wechselt der Zustand (etwa `idle` zu `walk`), beginnt die neue Animation bewusst bei Frame 0. + explanation: | + Die erste Abfrage spart Aufrufe, wenn sich nichts ändert; `play()` mit demselben Namen ließe die Animation ohnehin weiterlaufen. Danach merkt sich das Skript, wo der alte Zyklus stand. `play()` mit neuem Namen springt auf Frame 0; `set_frame_and_progress()` setzt Frame und Fortschritt zurück, aber nur, wenn die Figur weiterläuft. Ohne die drei Zeilen begänne der Zyklus bei jedem Richtungswechsel neu, die Figur stolperte bei jeder Kurve. Das funktioniert, solange alle Laufanimationen gleich viele Frames haben; das ist eine Vorgabe an die Zeichnungen. + resources: + - title: Godot Docs · AnimatedSprite2D.set_frame_and_progress + url: https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-method-set-frame-and-progress + + - id: richtungsbogen-einbauen + type: practice + title: Den Richtungsbogen ins Testgelände bringen + prompt: | + Leg den Richtungsbogen an (64 × 64, vier Zeilen R, D, L, U zu je vier Feldern mit Nummer, Block und Fußmarke) und importiere ihn wie den Testbogen. Mach aus dem Node `Sprite` unter dem Player ein `AnimatedSprite2D`, leg die Animationen `idle_right` … `walk_up` an und häng `player_animation.gd` an. Prüfe alle vier Richtungen, die vier Diagonalen und das Stehen. + lang: ini + solution: | + ; player.tscn nach dem Speichern in Godot 4.7.2 (Auszug, ids weichen ab) + + [ext_resource type="Texture2D" uid="uid://blhbfbwn481j2" path="res://test_assets/test_sheet_directions.png" id="3_hqtel"] + [ext_resource type="Script" uid="uid://dyqm1r18nljfv" path="res://player_animation.gd" id="4_sweqy"] + + [sub_resource type="AtlasTexture" id="AtlasTexture_2hs0m"] + atlas = ExtResource("3_hqtel") + region = Rect2(0, 16, 16, 16) + + ; … 15 weitere AtlasTextures, eine je Feld + + [sub_resource type="SpriteFrames" id="SpriteFrames_7ahtn"] + animations = [{ + "frames": [{ + "duration": 1.0, + "texture": SubResource("AtlasTexture_2hs0m") + }], + "loop": 1, + "name": &"idle_down", + "speed": 5.0 + }, …, { + "frames": [ … vier Frames der Zeile D … ], + "loop": 1, + "name": &"walk_down", + "speed": 8.0 + }, …] + + [node name="Sprite" type="AnimatedSprite2D" parent="." unique_id=942859769] + sprite_frames = SubResource("SpriteFrames_7ahtn") + animation = &"idle_down" + offset = Vector2(0, -8) + script = ExtResource("4_sweqy") + checklist: + - SpriteFrames enthält acht Animationen, `idle_*` mit einem Frame, `walk_*` mit vier, und jede `walk_*` zeigt die Buchstaben ihrer Zeile. + - Beim Laufen nach oben erscheinen U1 bis U4 nacheinander, beim Loslassen bleibt U1 stehen. + - Du hast die vier Tastatur-Diagonalen getestet und notiert, welche Richtung jede zeigt. + - Beim Wechsel von rechts nach unten während des Laufens springt die Nummer nicht auf 1 zurück. + - Mit `mirror_left` an zeigt das Laufen nach links ein gespiegeltes R. + - Die Ausgabe zeigt beim Testen keine Fehlermeldung. + explanation: | + Der Richtungsbogen macht jede Entscheidung des Skripts sichtbar: Der Buchstabe zeigt die Richtung, die Nummer die Phase, der gespiegelte Buchstabe das Spiegeln. Mit Godot 4.7.2 nachgemessen: Nach oben laufen zeigt `walk_up` Frame 0 bis 3, rechts plus oben ergibt mit vier Richtungen ebenfalls `walk_up`, nach dem Loslassen kommt `idle_up`. Die Werte in der Lösung sind Platzhalter. Wenn deine Figuren kommen, tauschst du nur die Bilder in SpriteFrames aus; Namen und Skript bleiben. + resources: + - title: Godot Docs · 2D sprite animation + url: https://docs.godotengine.org/en/stable/tutorials/2d/2d_sprite_animation.html + + - id: animationsplan + type: practice + title: Dein Animationsplan + prompt: | + Schreib einen kurzen Plan für die Animationen deiner Spielfigur, so dass ein Coding-Agent und du beim Zeichnen dieselbe Liste benutzt. Lege fest: wie viele Richtungen, ob links gespiegelt wird, welche Zustände es gibt, wie die Animationen heißen, mit welcher FPS und wie vielen Frames du startest und was auf Tastatur-Diagonalen gelten soll. Rechne für den Laufzyklus aus, welche FPS zu deinem Platzhalter-Tempo und deinem geplanten Schrittweg passt. Alle Zahlen sind Platzhalter, die du später im Spiel prüfst. + lang: prose + checklist: + - Der Plan nennt die Zahl der Richtungen und begründet sie mit deinem Zeichenaufwand oder dem Spielgefühl, nicht mit einer Vorgabe. + - Er sagt, ob links gespiegelt wird, und nennt mindestens ein Detail, das beim Spiegeln stören könnte, oder warum es keins gibt. + - Jede Animation hat einen Namen nach einem festen Schema, FPS und Frame-Zahl als Platzhalter. + - Für den Laufzyklus steht die Rechnung Tempo × Frames / Schrittweg mit deinen Zahlen da. + - Eine Regel für Tastatur-Diagonalen ist festgehalten. + - Du hast notiert, woran du im Spiel prüfst, ob die Füße rutschen. + hints: + - Die Tabelle mit den gemessenen Diagonalen aus der Lektion zeigt, was ohne eigene Regel passiert. + - Ein Schrittweg lässt sich in der Zeichnung messen, indem du einem Fuß folgst, solange er am Boden steht. + explanation: | + Der Plan ist die Schnittstelle zwischen Zeichnung und Code. Er macht Entscheidungen sichtbar, die sonst ein Coding-Agent nebenbei trifft: wie viele Richtungen, was auf der Diagonale gilt, ob gespiegelt wird. Die Rechnung für die FPS gibt dir einen Startwert, kein Urteil. Ob ein leichtes Gleiten zu deinem Stil passt, siehst du erst im Spiel. + resources: + - title: Godot Docs · SpriteFrames + url: https://docs.godotengine.org/en/stable/classes/class_spriteframes.html diff --git a/content/godot/modules/audio.yaml b/content/godot/modules/audio.yaml new file mode 100644 index 0000000..35ea1fd --- /dev/null +++ b/content/godot/modules/audio.yaml @@ -0,0 +1,515 @@ +id: audio +title: "Audio zuordnen: Player, Busse, Dezibel und der richtige Moment" +level: 2 +minutes: 130 +summary: Wie du eigene Töne importierst, sie mit AudioStreamPlayer oder AudioStreamPlayer2D abspielst, über Busse in Dezibel abmischst und im richtigen Moment auslöst – aus Code, aus einer Zeitleiste oder beim passenden Frame –, ohne dass queue_free() den Ton abschneidet. +place: pc +goals: + - Du importierst WAV- und Ogg-Dateien und erklärst, was Loop-Modus und Kompression im Import-Dock bewirken. + - Du unterscheidest `AudioStreamPlayer` und `AudioStreamPlayer2D` und sagst vorher, wie Abstand und Seite die Lautstärke ändern. + - Du legst Busse an, ordnest Player zu und rechnest zwischen Dezibel und Amplitude um. + - Du löst Töne aus Code, aus einem `AnimationPlayer` und über `frame_changed` aus und erklärst, warum ein Ton im gelöschten Pickup verstummt. + - Du variierst wiederholte Töne mit `AudioStreamRandomizer`. +resources: + - title: Godot Docs · Audio buses + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html + - title: Godot Docs · Audio streams + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_streams.html + - title: Godot Docs · Importing audio samples + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_audio_samples.html + - title: Godot Docs · AudioStreamPlayer2D + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer2d.html +lesson: | + Du hängst an das Pickup im Testgelände einen `AudioStreamPlayer`, rufst beim Einsammeln `play()` auf, und es bleibt still. Der Zähler im HUD springt, der Ton fehlt. Die Schritte der Figur klingen wie ein Maschinengewehr aus identischen Klicks, und als du später fragst, warum ein Ton zu laut ist, weiß niemand, wo seine Lautstärke eingestellt wird. + + Ton in Godot hat vier Stellen: die importierte Datei, den Player, der sie abspielt, den Bus, über den sie läuft, und den Moment, in dem jemand `play()` aufruft. Dieses Modul geht sie der Reihe nach durch, mit einem Prüfton statt fertiger Sounds. Welche Ereignisse in deinem Spiel klingen, wie laut und wie, entscheidest du. Hier geht es darum, dass der Ton im richtigen Frame, auf dem richtigen Bus und in der Lautstärke ankommt, die du eingestellt hast. + + ## Der Prüfton: ein eigener Klick statt fertiger Sounds + + Für die Tests brauchst du zwei Töne. Einen kurzen Klick oder Klatscher, unter 100 ms, und einen gleichmäßigen Ton von einer Sekunde für Schleifen. Nimm den Klick selbst auf, mit dem Handy oder am Rechner, oder erzeug ihn in REAPER. Exportiere als WAV mit 16 Bit, ohne Stille am Anfang: Jede Millisekunde Stille vor dem Klick ist eine Millisekunde Verzögerung im Spiel ([Edit > Trim](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_audio_samples.html#edit-trim)). Leg die Dateien als `res://test_assets/test_click.wav` und `test_hum.ogg` ab. Sie sind Messwerkzeuge wie der Testbogen, keine Sounds für dein Spiel. + + Für die Messungen in diesem Modul liefen ein Klick von 80 ms (1 kHz, abklingend) und ein Ton von 220 Hz mit einer Sekunde durch Godot 4.7.2. Die Pegel stammen aus einem `AudioEffectCapture` auf dem Master-Bus. + + ## Import: WAV und Ogg Vorbis + + WAV ist roh oder leicht komprimiert und kostet beim Abspielen wenig Rechenzeit; laut Doku sind hunderte gleichzeitige Stimmen kein Problem. Ogg Vorbis ist viel kleiner, braucht beim Abspielen aber deutlich mehr Rechenzeit ([Supported audio formats](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_audio_samples.html#supported-audio-formats)). Kurze Effekte sind deshalb meist WAV, lange Musik Ogg. + + ![Import-Dock für click.wav: Importiere als Microsoft WAV, Erzwingen mit 8-Bit, Mono und Max. Rate aus, Bearbeiten mit Beschneiden und Normalisieren aus, Loop-Modus Detect From WAV, Komprimieren Modus Quite OK Audio](/media/godot/audio/import-wav.png "Godot 4.7.2, Import-Dock des Messklicks click.wav mit Standardwerten") + + Zwei Einstellungen zählen für dich. **Loop-Modus** (Loop Mode) steht auf *Detect From WAV*: Godot übernimmt die Schleifenpunkte aus der Datei, falls es welche gibt. *Disabled* schaltet Schleifen ab, *Forward* wiederholt ([Edit > Loop Mode](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_audio_samples.html#edit-loop-mode)). **Komprimieren → Modus** steht auf *Quite OK Audio*, einer verlustbehafteten Kompression mit laut Doku hoher Qualität und wenig Speicher. Für Ogg-Dateien zeigt das Dock nur *Loop* (aus), *Loop-Versatz* und Takt-Angaben; *Erweitert …* öffnet einen Dialog mit Vorhör. + + Ob eine Datei in Schleife läuft, entscheidet über ein Signal: `finished` kommt nur, wenn der Ton endet. Gemessen mit dem Ton von einer Sekunde: ohne Loop `finished nach 1.0 s`, mit Loop nach 2,5 s noch `playing=true` und kein `finished`. Code, der auf das Ende eines Tons wartet, wartet bei einer Schleife ewig. + + ## AudioStreamPlayer und AudioStreamPlayer2D + + Ein `AudioStreamPlayer` spielt ohne Ort: gleich laut, mittig, egal wo der Node steht. Für Musik, Menüs und Töne, die zur Figur gehören, reicht das. Ein `AudioStreamPlayer2D` hat eine Position. Er wird mit dem Abstand leiser und wandert zwischen linkem und rechtem Kanal. Gehört wird laut Doku von der Bildschirmmitte aus, solange kein `AudioListener2D` aktiv ist ([AudioStreamPlayer2D](https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer2d.html)). Im Testgelände folgt die Kamera der Figur, die Bildschirmmitte ist also die Figur. + + Die Lautstärke fällt mit `(1 − Abstand / max_distance) ^ attenuation`. Der Standard für `max_distance` ist 2000 Pixel, für `attenuation` 1,0. Gemessen mit dem 220-Hz-Ton, Quelle rechts von der Bildschirmmitte, Spitzenpegel links / rechts: + + | Abstand | `attenuation` 1 | `attenuation` 2 | + | --- | --- | --- | + | 0 px | 0.1246 / 0.1246 | 0.1246 / 0.1246 | + | 160 px (Bildrand) | 0.0860 / 0.1433 | 0.0791 / 0.1319 | + | 1000 px | 0.0313 / 0.0938 | 0.0156 / 0.0469 | + | 2010 px | 0 / 0 | 0 / 0 | + + Links plus rechts ergibt die Gesamtlautstärke: bei 1000 px die Hälfte, mit `attenuation` 2 ein Viertel. Am Bildrand eines 320 Pixel breiten Spiels ist die Quelle nur 8 % leiser als in der Mitte; mit den Standardwerten hörst du Dinge, die sechs Bildschirmbreiten entfernt sind. Ganz auf eine Seite wandert der Ton mit dem Standard-Panning (`audio/general/2d_panning_strength` 0,5) nie, gemessen höchstens 3 : 1. Welche Reichweite deine Töne haben, entscheidest du; die Tabelle zeigt, wo du anfängst. + + Beide Player haben `volume_db` (Standard 0), `pitch_scale` und `max_polyphony`. Bei `max_polyphony` 1, dem Standard, schneidet ein neues `play()` den laufenden Ton ab ([max_polyphony](https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer2d.html#class-audiostreamplayer2d-property-max-polyphony)). Und: Einen `AudioStreamPlayer2D` unsichtbar zu schalten, macht ihn nicht stumm; der `AudioStreamPlayer` hat gar keine Sichtbarkeit. + + ## Busse und Dezibel: wo die Lautstärke wohnt + + Ein Bus ist ein Kanal, durch den Ton auf dem Weg zum Lautsprecher läuft, mit Lautstärkeregler, Effekten und Pegelanzeige ([Audio buses](https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html)). Ganz links steht `Master`, er geht an die Lautsprecher. Im unteren Panel *Audio* legst du mit *Audiobus hinzufügen* weitere an. Jeder Bus hat die Knöpfe S, M und B: *Allein* (Solo), *Stummschalten* (Mute) und *Bypass* für die Effekte, darunter *Effekt hinzufügen* und das Ziel, an das er sendet. + + ![Audio-Panel mit zwei Bussen: Master mit Regler bei 0 dB, SFX mit Regler bei −6 dB, je mit Knöpfen S, M, B, Effekt hinzufügen und Ziel; oben Layout default_bus_layout.tres und der Knopf Audiobus hinzufügen](/media/godot/audio/audio-busse.png "Godot 4.7.2, Audio-Panel mit dem Platzhalter-Bus SFX") + + Godot speichert das Layout in `res://default_bus_layout.tres`. Mit einem Bus `SFX` (Name als Platzhalter) steht dort: + + ```ini title=default_bus_layout.tres + [gd_resource type="AudioBusLayout" format=3] + + [resource] + bus/1/name = &"SFX" + bus/1/solo = false + bus/1/mute = false + bus/1/bypass_fx = false + bus/1/volume_db = 0.0 + bus/1/send = &"Master" + ``` + + Ein Player wählt seinen Bus über die Eigenschaft `bus`, und zwar über den **Namen**. Benennst du einen Bus um, landen alle Player, die den alten Namen tragen, auf `Master`, ohne Fehlermeldung ([Bus rearrangement](https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html#bus-rearrangement)). Gemessen: Ein Player mit `bus = &"Sfx"`, während der Bus `SFX` heißt und auf −20 dB steht, spielt mit vollem Pegel 0.2443 statt 0.0244. + + **Dezibel** sind ein Verhältnis, kein absoluter Wert. Für die Amplitude, den Ausschlag der Welle, gilt dB = 20 × log10(Verhältnis). −6 dB halbieren die Amplitude, −20 dB teilen sie durch zehn. Die Doku schreibt, −6 dB seien „half as loud“; gemeint ist die Amplitude, wie laut es wirkt, beurteilst du mit dem Ohr ([Decibel scale](https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html#decibel-scale)). Gemessen: derselbe Ton mit `volume_db` 0 hat den Pegel 0.2443, mit −6 dB 0.1224. Zum Umrechnen gibt es `linear_to_db()` und `db_to_linear()` ([linear_to_db](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-linear-to-db)): + + ```text + linear_to_db(0.5) = -6.02059991327962 + db_to_linear(-6) = 0.50118723362727 + db_to_linear(-20) = 0.1 + ``` + + Über 0 dB auf dem Master-Bus übersteuert das Signal. Zwischen −60 und −80 dB gilt ein Ton laut Doku als nicht mehr hörbar. + + Für Webentwickler: Ein Bus ist ein `GainNode` in der Web Audio API, Effekte sind weitere Knoten davor, `Master` ist `destination`. Die Grenze: Godot leitet nur nach links weiter, zu einem Bus weiter vorn in der Liste, statt beliebige Graphen zu erlauben. + + > [!MERKE] + > Die Lautstärke eines Tons ist die Summe aus `volume_db` am Player und den Reglern aller Busse auf seinem Weg. Such sie an diesen Stellen, und schreib in jede Aufgabe für einen Coding-Agenten, welcher Bus gemeint ist. + + ## Der richtige Moment: Code, Zeitleiste, Frame + + ### Aus Code: das Pickup und queue_free() + + `pickup.gd` ruft beim Einsammeln `queue_free()` auf. Hängt der Player als Kind am Pickup, verschwindet er mit ihm. Gemessen mit dem Ton von 220 Hz: Pegel 0.2443 vor `queue_free()`, 0.0 danach. Hängst du den Player vor dem Löschen mit `reparent()` an einen anderen Node, spielt er weiter und meldet nach rund einer Sekunde `finished`. + + Im Testgelände geht es ohne Eingriff in `pickup.gd`: Der Player sendet schon das Signal `collected`. Ein `AudioStreamPlayer` namens `PickupSound` in `main.tscn` hört darauf und lebt weiter, wenn das Pickup verschwindet: + + ```gdscript title=pickup_sound.gd + extends AudioStreamPlayer + + ## Spielt den Platzhalter-Ton, wenn der Player etwas einsammelt. + ## Der Node lebt in main.tscn weiter, auch wenn das Pickup verschwindet. + + + func _on_player_collected(_total: int) -> void: + play() + ``` + + Nach dem Verbinden im Dock *Signale* und dem Speichern in Godot 4.7.2 steht in `main.tscn` (Auszug): + + ```ini title=main.tscn + [node name="PickupSound" type="AudioStreamPlayer" parent="." unique_id=428933553] + stream = ExtResource("5_7mycd") + bus = &"SFX" + script = ExtResource("6_272bh") + + [connection signal="collected" from="Objects/Player" to="PickupSound" method="_on_player_collected"] + ``` + + Gemessen: Nach dem Einsammeln hat der Master-Bus den Pegel des Klicks. Verbindest du `collected` dagegen direkt mit `play`, bleibt es still. Das Signal übergibt `total`, und `play()` nimmt sein erstes Argument als Startposition in Sekunden: Der Klick von 80 ms startet bei Sekunde 1. + + ### Aus einer Zeitleiste + + Gehört ein Ton zu einem Ablauf mit festen Frames, etwa einem Angriff, legst du ihn als Audio Playback Track in den `AnimationPlayer` oder rufst `play` über eine Call Method Track auf ([Audio Playback Track](https://docs.godotengine.org/en/stable/tutorials/animation/animation_track_types.html#audio-playback-track)). Dann sitzt er im selben Takt wie Hitbox und Bild. Für Treffer gilt: Der Ton gehört in den Frame, in dem der Treffer gemeldet wird, nicht in den, in dem die Hitbox angeht ([Trefferfeedback](/combat/trefferfeedback)). + + ### Beim passenden Frame: Schritte + + Schritte hängen am Laufzyklus. Das `AnimatedSprite2D` aus [Animationen zuordnen](/godot/animationen) sendet `frame_changed`; ein eigener Node `Footsteps` unter dem Player hört darauf: + + ```gdscript title=footsteps.gd + extends AudioStreamPlayer + + ## Spielt einen Schritt, wenn das Sprite einen Schritt-Frame erreicht. + ## Welche Frames Schritte sind, ist ein Platzhalter: Du legst es fest. + + @export var step_frames: Array[int] = [0, 2] + + var _last_frame := -1 + + @onready var sprite: AnimatedSprite2D = $"../Sprite" + + + func _ready() -> void: + # Deferred: erst prüfen, wenn player_animation.gd den Frame fertig gesetzt hat. + sprite.frame_changed.connect(_check_step, CONNECT_DEFERRED) + sprite.animation_changed.connect(_check_step, CONNECT_DEFERRED) + + + func _check_step() -> void: + if not String(sprite.animation).begins_with("walk"): + _last_frame = -1 + return + if sprite.frame != _last_frame and sprite.frame in step_frames: + play() + _last_frame = sprite.frame + ``` + + Warum reicht `frame_changed` allein nicht? Zwei Messungen mit der ersten, kürzeren Fassung. Beim Loslaufen wechselt die Animation von `idle_right` (Frame 0) zu `walk_right` (Frame 0); der Frame ändert sich nicht, `frame_changed` kommt nicht, der erste Schritt fehlt. Erst bei Frame 2 klingt der erste Schritt, bei 8 FPS also rund 0,25 s später (gemessen 14 Bilder bei 60 pro Sekunde). `animation_changed` kommt dagegen ([animation_changed](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html#class-animatedsprite2d-signal-animation-changed)). Und beim Richtungswechsel setzt `player_animation.gd` mit `set_frame_and_progress()` den Frame neu, was wieder `frame_changed` auslöst: ein doppelter Schritt. `CONNECT_DEFERRED` prüft erst am Ende des Bilds, wenn der Frame feststeht, und `_last_frame` verhindert die Wiederholung. Gemessen mit der Fassung oben: der erste Schritt sofort beim Loslaufen, dann alle 0,25 s, kein Doppelschritt beim Abbiegen. + + Damit zwanzig Schritte nicht wie einer klingen, nimmt `Footsteps` als `stream` einen `AudioStreamRandomizer`. Er wählt aus mehreren Tönen und verändert Tonhöhe und Lautstärke bei jedem Abspielen ([AudioStreamRandomizer](https://docs.godotengine.org/en/stable/classes/class_audiostreamrandomizer.html)). In Godot 4.7 stellst du die Tonhöhe auch in Halbtönen ein (`random_pitch_semitones`); in der Szene steht danach der Faktor: 1 Halbton wird zu `random_pitch = 1.0594631`. Wie viel Abwechslung deine Schritte vertragen, entscheidest du beim Hören. + + > [!PRAXIS] + > Bevor ein Ton ins Spiel kommt, ordne ihn einem Bus zu und schreib den Bus in den Node, nicht in den Code. Ein Spieler, der später Musik und Effekte getrennt regeln will, braucht genau diese Trennung. Und wenn ein Ton fehlt, schalte den Bus im Audio-Panel auf *Allein*: Schlägt die Pegelanzeige aus, liegt das Problem hinter dem Bus, sonst davor. + + > [!INTERVIEW] + > „Ein Sammelgeräusch spielt nicht, obwohl `play()` aufgerufen wird. Woran kann es liegen?“ Gute Antwort, etwa als Gameplay Programmer: Ich prüfe, ob der Player noch lebt, wenn der Ton spielen soll; ein Kind eines Nodes, der gleich `queue_free()` aufruft, verstummt sofort. Dann den Bus: stummgeschaltet, auf sehr niedrigem Pegel oder ein Name, den es nicht mehr gibt. Dann den Stream selbst, etwa Stille am Anfang oder eine Startposition hinter dem Ende. Ich schalte den Bus auf Solo und sehe an der Pegelanzeige, wo der Ton verschwindet. + + ## Häufige Fehler + + - **Ton im Pickup, das sich löscht.** Mit `queue_free()` verschwindet der Player. Ton auf einen Node legen, der weiterlebt, oder vorher mit `reparent()` umhängen. + - **Signal direkt mit `play` verbunden.** Ein Signal mit Argument startet den Ton an dieser Stelle in Sekunden. Eigene Methode dazwischen. + - **Bus umbenannt.** Player mit dem alten Namen spielen über `Master`, ohne Meldung. + - **Auf `finished` gewartet, Datei läuft in Schleife.** Loop im Import-Dock prüfen. + - **`AudioStreamPlayer2D` mit Standardreichweite in einem kleinen Bild.** 2000 Pixel Reichweite bei 320 Pixeln Breite heißt: kaum Unterschied am Bildrand. + - **Erster Schritt fehlt, Schritt doppelt.** `frame_changed` allein reicht nicht; `animation_changed` und eine verzögerte Prüfung dazu. + - **Stille am Anfang der Datei.** Verzögert jeden Ton. *Beschneiden* im Import-Dock oder beim Export schneiden. + + ## Zusammenfassung + + - WAV für kurze Effekte, Ogg für lange Musik. Loop-Modus und Kompression stehen im Import-Dock; eine Schleife meldet nie `finished`. + - `AudioStreamPlayer` spielt ohne Ort, `AudioStreamPlayer2D` wird mit `(1 − Abstand / max_distance) ^ attenuation` leiser und von der Bildschirmmitte aus gehört. + - Busse laufen von rechts nach links in `Master`; Player finden ihren Bus über den Namen. + - −6 dB halbieren die Amplitude, −20 dB teilen sie durch zehn; `linear_to_db()` und `db_to_linear()` rechnen um. + - Töne gehören auf Nodes, die weiterleben. Signale mit Argument nicht direkt an `play` hängen. + - Schritte über `frame_changed` und `animation_changed`, verzögert geprüft; Abwechslung über `AudioStreamRandomizer`. + - Welche Töne, welche Busse, welche Reichweite und wie viel Variation, entscheidest du. +exercises: + - id: welcher-player + type: choice + title: Ort oder kein Ort? + prompt: | + Welche Aussagen über die beiden Player stimmen in Godot 4.7? Mehrere Antworten richtig. + options: + - text: "`AudioStreamPlayer2D` wird ohne eigenen Listener von der Bildschirmmitte aus gehört." + correct: true + why: So steht es in der Doku. Folgt die Kamera der Figur, ist die Bildschirmmitte die Figur. + - text: Mit den Standardwerten ist eine Quelle am Rand eines 320 Pixel breiten Bilds deutlich leiser als in der Mitte. + correct: false + why: Bei 2000 Pixeln Reichweite und `attenuation` 1 fehlen 160 Pixel weit draußen nur 8 %. Gemessen mit Godot 4.7.2. + - text: Ein unsichtbar geschalteter `AudioStreamPlayer2D` spielt weiter. + correct: true + why: Die Doku sagt ausdrücklich, dass Verstecken die Ausgabe nicht abschaltet. Dafür gibt es `volume_db` oder `stop()`. + - text: "`AudioStreamPlayer` ändert seine Lautstärke, wenn du ihn im Level verschiebst." + correct: false + why: Er hat gar keine Position, er ist kein `Node2D`. Für Abstand und Seite brauchst du die 2D-Variante. + explanation: | + Der Unterschied ist der Ort. `AudioStreamPlayer` spielt immer gleich, die 2D-Variante rechnet Abstand und Seite relativ zur Bildschirmmitte. Wie stark, hängt an `max_distance` und `attenuation`; die Standardwerte sind für ein Bild mit 320 Pixeln Breite sehr großzügig. Die Entscheidung, welche Töne einen Ort haben, gehört zu deinem Sounddesign. + resources: + - title: Godot Docs · AudioStreamPlayer2D + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer2d.html + + - id: bus-einrichten + type: order + title: Ein Ton auf seinem eigenen Bus + prompt: | + Bring die Schritte in eine sinnvolle Reihenfolge, damit der Prüfklick beim Einsammeln über einen eigenen Bus spielt. + lang: prose + lines: | + Den Klick als WAV ohne Stille am Anfang nach res://test_assets/ legen + Im Import-Dock Loop-Modus und Kompression prüfen + Im Panel Audio mit Audiobus hinzufügen einen Bus anlegen und benennen + In main.tscn einen AudioStreamPlayer PickupSound anlegen, Stream und Bus zuweisen + pickup_sound.gd anhängen und collected vom Player im Dock Signale mit _on_player_collected verbinden + Im Spiel ein Pickup einsammeln und die Pegelanzeige des Busses beobachten + alternatives: + - [1, 3, 2, 4, 5, 6] + - [3, 1, 2, 4, 5, 6] + explanation: | + Datei und Import kommen vor dem Player, denn der braucht einen fertigen Stream. Den Bus kannst du auch vorher oder danach anlegen; er muss nur existieren, bevor du ihn im Player wählst und testest. Erst mit der Verbindung zu `collected` löst das Einsammeln den Ton aus. Die Pegelanzeige zeigt im Test, ob der Ton über den Bus läuft. + resources: + - title: Godot Docs · Audio buses + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html + + - id: dezibel-rechnen + type: output + title: Zwischen Dezibel und Amplitude + prompt: | + Was geben die beiden Zeilen in Godot 4.7.2 aus? + lang: gdscript + code: | + print(snappedf(linear_to_db(0.5), 0.01)) + print(db_to_linear(-20.0)) + expected: | + -6.02 + 0.1 + accept: + - | + -6,02 + 0,1 + - | + -6.02 + 0.10 + hints: + - Für die Amplitude gilt dB = 20 × log10(Verhältnis). + - 20 × log10(0,5) ist knapp −6. + explanation: | + Die halbe Amplitude sind 20 × log10(0,5) = −6,02 dB, daher die Faustregel „−6 dB halbieren“. −20 dB entsprechen 10^(−20/20) = 0,1, ein Zehntel. Gemessen am Pegel: Ein Ton mit 0.2443 kam mit −6 dB auf 0.1224 und über einen Bus mit −20 dB auf 0.0244. Ungerundet gibt Godot 4.7.2 für die erste Zeile `-6.02059991327962` aus; `snappedf()` rundet auf zwei Stellen. Die Ausgabe stammt aus einem Lauf mit Godot 4.7.2. + resources: + - title: Godot Docs · @GlobalScope.linear_to_db + url: https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-linear-to-db + + - id: halbtoene-faktor + type: output + title: Halbtöne in Faktoren + prompt: | + Was gibt der Code aus? + lang: gdscript + code: | + var r := AudioStreamRandomizer.new() + r.random_pitch_semitones = 2.0 + print(snappedf(r.random_pitch, 0.0001)) + r.random_pitch = 2.0 + print(r.random_pitch_semitones) + expected: | + 1.1225 + 12.0 + accept: + - | + 1.1225 + 12 + - | + 1,1225 + 12,0 + - | + 1,1225 + 12 + hints: + - Ein Halbton ist der Faktor 2^(1/12). + - Eine Oktave sind zwölf Halbtöne und verdoppelt die Frequenz. + explanation: | + Beide Eigenschaften beschreiben dieselbe Spanne. Zwei Halbtöne sind 2^(2/12) = 1,1225; der Randomizer wählt die Tonhöhe dann zwischen 1/1,1225 und 1,1225. Der Faktor 2,0 ist eine Oktave, also 12 Halbtöne. In der Szene speichert Godot den Faktor (`random_pitch`), auch wenn du Halbtöne eingibst. Ungerundet gibt Godot 4.7.2 `1.12246203422546` aus; der Wert ist intern eine Gleitkommazahl mit begrenzter Genauigkeit, deshalb rundet `snappedf()` auf vier Stellen. + resources: + - title: Godot Docs · AudioStreamRandomizer.random_pitch_semitones + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamrandomizer.html#class-audiostreamrandomizer-property-random-pitch-semitones + + - id: reichweite-prozent + type: output + title: Wie viel kommt an? + prompt: | + Ein `AudioStreamPlayer2D` steht 1000 Pixel rechts von der Bildschirmmitte, `max_distance` 2000, `attenuation` 2. Wie viel Prozent der Amplitude kommen insgesamt (links plus rechts) an, verglichen mit derselben Quelle in der Mitte? Antworte mit der Zahl in Prozent. + lang: text + code: | + Abstand: 1000 px + max_distance: 2000 px + attenuation: 2 + expected: | + 25 + accept: + - 25 % + - 25% + - "0,25" + - "0.25" + hints: + - "Faktor = (1 − Abstand / max_distance) ^ attenuation." + explanation: | + (1 − 1000 / 2000)² = 0,5² = 0,25. Gemessen mit Godot 4.7.2: in der Mitte 0.1246 je Kanal, zusammen 0.2492; bei 1000 Pixeln mit `attenuation` 2 links 0.0156 und rechts 0.0469, zusammen 0.0625, also ein Viertel. Mit `attenuation` 1 wäre es die Hälfte. Die Verteilung auf links und rechts kommt vom Panning und ändert die Summe nicht. + resources: + - title: Godot Docs · AudioStreamPlayer2D.attenuation + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer2d.html#class-audiostreamplayer2d-property-attenuation + + - id: pickup-sound-zeilen + type: gap + title: Ein Ton, der weiterlebt + prompt: | + Ergänze den Ausschnitt aus `main.tscn`: `PickupSound` spielt über den Bus `SFX` und reagiert auf das Signal des Players, mit dem er Einsammeln meldet. + lang: ini + code: | + [node name="PickupSound" type="⟦AudioStreamPlayer⟧" parent="." unique_id=428933553] + stream = ExtResource("5_7mycd") + bus = &"⟦SFX⟧" + script = ExtResource("6_272bh") + + [connection signal="⟦collected⟧" from="Objects/Player" to="PickupSound" method="⟦_on_player_collected⟧"] + gaps: + - label: Node-Typ + hint: Ein Player ohne Position genügt, die Figur ist die Bildschirmmitte. + - label: Bus + hint: Der Name aus dem Audio-Panel, genau so geschrieben. + - label: Signal + hint: Steht in `player.gd` hinter `signal`. + - label: Methode + hint: Die Methode aus `pickup_sound.gd`. + explanation: | + Der Node hängt direkt unter `Main` und überlebt damit jedes Pickup. Der Bus wird über den Namen gefunden, Groß- und Kleinschreibung zählen. Die Verbindung führt vom Signal `collected` des Players zur eigenen Methode, die `play()` ohne Argument aufruft. So stand es nach dem Speichern in Godot 4.7.2 in der Datei; `unique_id` und die ids der Ressourcen weichen bei dir ab. + resources: + - title: Godot Docs · AudioStreamPlayer.bus + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer.html#class-audiostreamplayer-property-bus + + - id: ton-im-pickup + type: bug + title: Der Zähler steigt, der Ton fehlt + prompt: | + Ein Coding-Agent hat dem Pickup ein Kind `Sound` (`AudioStreamPlayer`) gegeben und `pickup.gd` so geändert. Beim Einsammeln steigt der Zähler im HUD, aber nichts ist zu hören. Der Bus ist nicht stumm. Finde die Zeile, die den Ton verhindert. + lang: gdscript + code: | + func _on_body_entered(body: Node2D) -> void: + if body is Player: + body.collect(value) + $Sound.play() + queue_free() + lines: [5] + explanation: | + `queue_free()` löscht das Pickup am Ende des Bilds, mit allen Kindern, auch dem Player, der gerade angefangen hat zu spielen. Gemessen mit Godot 4.7.2 und dem Ton von 220 Hz: Pegel 0.2443 vor dem Löschen, 0.0 danach. Lösungen: den Ton auf einen Node legen, der weiterlebt, wie `PickupSound` in `main.tscn`; oder `$Sound.reparent(get_parent())` vor `queue_free()` und den Sound mit `finished` selbst löschen lassen; oder das Pickup verstecken, seine Form abschalten und erst nach `finished` löschen. + resources: + - title: Godot Docs · Node.queue_free + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-queue-free + + - id: direkt-an-play + type: bug + title: Verbunden, aber stumm + prompt: | + Statt eines Skripts hat ein Coding-Agent das Signal direkt mit der Methode `play` des `PickupSound` verbunden. Beim ersten Einsammeln ist nichts zu hören, eine Fehlermeldung gibt es nicht. Finde die Zeile und korrigiere sie so, dass die Methode aus `pickup_sound.gd` aufgerufen wird. + lang: ini + code: | + [node name="PickupSound" type="AudioStreamPlayer" parent="." unique_id=428933553] + stream = ExtResource("5_7mycd") + bus = &"SFX" + script = ExtResource("6_272bh") + + [connection signal="collected" from="Objects/Player" to="PickupSound" method="play"] + lines: [6] + fix: + - line: 6 + answers: + - '[connection signal="collected" from="Objects/Player" to="PickupSound" method="_on_player_collected"]' + explanation: | + `collected` übergibt die Gesamtzahl, beim ersten Pickup 1. Verbunden mit `play` landet sie im ersten Parameter `from_position`: Der Ton startet bei Sekunde 1, der Klick ist aber nur 80 ms lang. Gemessen mit Godot 4.7.2: Spitzenpegel 0.0031 statt 0.603 mit der eigenen Methode, praktisch Stille. Die eigene Methode `_on_player_collected(_total)` nimmt das Argument an und ruft `play()` ohne Argument auf. + resources: + - title: Godot Docs · AudioStreamPlayer.play + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer.html#class-audiostreamplayer-method-play + + - id: bus-falsch-geschrieben + type: bug + title: Der Regler wirkt nicht + prompt: | + Ein Coding-Agent hat den Node `Footsteps` direkt in `player.tscn` geschrieben. Im Audio-Panel steht der Bus `SFX` auf −20 dB, die Schritte bleiben trotzdem gleich laut, egal wie du den Regler schiebst. Eine Fehlermeldung gibt es nicht. Finde die Zeile und korrigiere sie. + lang: ini + code: | + [node name="Footsteps" type="AudioStreamPlayer" parent="." unique_id=1821499515] + stream = SubResource("AudioStreamRandomizer_dw050") + bus = &"Sfx" + script = ExtResource("6_1jxqw") + lines: [3] + fix: + - line: 3 + answers: + - bus = &"SFX" + hints: + - Wähle den Node im Editor. Welchen Bus zeigt der Inspektor bei *Bus* an? + - Busnamen unterscheiden Groß- und Kleinschreibung. + explanation: | + Player finden ihren Bus über den Namen, und `Sfx` ist nicht `SFX`. Kann Godot den Namen nicht auflösen, spielt der Player über `Master`, ohne Meldung. Gemessen mit Godot 4.7.2: mit `Sfx` Pegel 0.2443, mit `SFX` bei −20 dB Pegel 0.0244. Derselbe Fehler entsteht, wenn jemand einen Bus umbenennt. Im Editor siehst du ihn am Inspektor: Für einen unbekannten Namen liefert die Eigenschaft `bus` `Master`, gemessen zur Laufzeit; gespeichert schreibt der Editor die Zeile dann gar nicht. Die falsche Zeile entsteht also nur, wenn jemand die Datei von Hand oder per Agent schreibt. + resources: + - title: Godot Docs · Audio buses · Bus rearrangement + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html#bus-rearrangement + + - id: schritte-erklaeren + type: explain + title: Warum zwei Signale und deferred? + prompt: | + Eine erste Fassung von `footsteps.gd` hörte nur auf `frame_changed` und prüfte sofort. Gemessen: Beim Loslaufen kam der erste Schritt erst mit Frame 2, rund 0,25 s nach dem Start, und beim Abbiegen klang ein Schritt doppelt. Erkläre, wie die Zeilen unten beides beheben. + lang: gdscript + code: | + func _ready() -> void: + sprite.frame_changed.connect(_check_step, CONNECT_DEFERRED) + sprite.animation_changed.connect(_check_step, CONNECT_DEFERRED) + + + func _check_step() -> void: + if not String(sprite.animation).begins_with("walk"): + _last_frame = -1 + return + if sprite.frame != _last_frame and sprite.frame in step_frames: + play() + _last_frame = sprite.frame + points: + - Beim Loslaufen bleibt der Frame 0, nur der Name wechselt; `frame_changed` kommt nicht, `animation_changed` schon. + - Beim Abbiegen setzt `player_animation.gd` den Frame mit `set_frame_and_progress()` zurück, das löst `frame_changed` mit einem Schritt-Frame erneut aus. + - "`CONNECT_DEFERRED` prüft erst am Ende des Bilds, wenn Animation und Frame feststehen." + - "`_last_frame` merkt sich den zuletzt geprüften Frame; derselbe Frame löst keinen zweiten Schritt aus, und im Stand wird er zurückgesetzt." + explanation: | + Der fehlende erste Schritt liegt daran, dass sich der Frame beim Wechsel von `idle_right` zu `walk_right` nicht ändert: beide stehen auf 0. `animation_changed` fängt diesen Fall. Beim Abbiegen feuern beide Signale mitten im Wechsel, einmal mit Frame 0 und einmal mit dem übernommenen Frame. Verzögert geprüft sieht das Skript nur den Endzustand, und `_last_frame` verhindert, dass der übernommene Schritt-Frame noch einmal klingt. Gemessen mit Godot 4.7.2: erster Schritt sofort, dann alle 0,25 s, kein Doppelschritt. + resources: + - title: Godot Docs · Object.connect · ConnectFlags + url: https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-constant-connect-deferred + + - id: pruefton-einbauen + type: practice + title: Prüfton, Bus und Schritte im Testgelände + prompt: | + Nimm einen eigenen Klick auf (unter 100 ms, ohne Stille am Anfang) und leg ihn als `res://test_assets/test_click.wav` ab. Leg einen Bus an (Name als Platzhalter), setz `PickupSound` mit `pickup_sound.gd` in `main.tscn` und `Footsteps` mit `footsteps.gd` und einem `AudioStreamRandomizer` unter den Player. Prüfe mit der Pegelanzeige im Audio-Panel und mit Solo und Mute. + lang: ini + solution: | + ; default_bus_layout.tres + [resource] + bus/1/name = &"SFX" + bus/1/solo = false + bus/1/mute = false + bus/1/bypass_fx = false + bus/1/volume_db = 0.0 + bus/1/send = &"Master" + + ; player.tscn nach dem Speichern in Godot 4.7.2 (Auszug, ids weichen ab) + [sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_dw050"] + random_pitch = 1.0594631 + random_volume_offset_db = 2.0 + streams_count = 1 + stream_0/stream = ExtResource("5_2hs0m") + + [node name="Footsteps" type="AudioStreamPlayer" parent="." unique_id=1821499515] + stream = SubResource("AudioStreamRandomizer_dw050") + bus = &"SFX" + script = ExtResource("6_1jxqw") + checklist: + - Der Klick ist deine eigene Aufnahme oder in REAPER erzeugt, und im Import-Dock steht der Loop-Modus auf *Detect From WAV* oder *Disabled*. + - Beim Einsammeln schlägt die Pegelanzeige deines Busses aus, und der Ton ist zu hören. + - Mit *Stummschalten* am Bus `SFX` verstummen Pickup und Schritte. Mit *Allein* am Bus `SFX` bleiben sie hörbar, mit *Allein* am Master verstummen sie. + - Beim Loslaufen klingt der erste Schritt sofort, beim Abbiegen gibt es keinen Doppelschritt. + - Pitch- und Lautstärkevariation des Randomizers stehen als Platzhalter in der Szene, und du hast notiert, welche Werte du gehört hast. + - Die Ausgabe zeigt keine Fehlermeldung, auch nicht, wenn du alle drei Pickups einsammelst. + explanation: | + Die Übung prüft die ganze Kette mit einem Ton, den du kennst: Import, Player, Bus, Auslöser. Solo und Mute zeigen, dass der Ton über deinen Bus läuft und nicht direkt über Master. Steht ein Bus auf *Allein*, schweigen alle anderen Busse: Gemessen mit Godot 4.7.2 zeigen die Pegel von Master und `SFX` normal −12,2 dB, mit *Allein* am Master −200 dB (still), mit *Allein* an `SFX` wieder −12,1 dB. Die Variation im Randomizer (hier 1 Halbton und 2 dB) ist ein Platzhalter. Ob deine Schritte mehr oder weniger Abwechslung brauchen, hörst du erst mit deinen eigenen Aufnahmen. + resources: + - title: Godot Docs · AudioStreamRandomizer + url: https://docs.godotengine.org/en/stable/classes/class_audiostreamrandomizer.html + - title: Godot Docs · Audio buses + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html + + - id: klangplan + type: practice + title: Dein Klangplan für das Testgelände + prompt: | + Schreib einen kurzen Klangplan für die Ereignisse, die dein Spiel hören lassen soll, zunächst für das Testgelände. Lege für jedes Ereignis fest: welcher Player (mit oder ohne Ort), welcher Bus, welcher Auslöser (Code, Zeitleiste oder Frame), ob es eine Schleife ist und wie du prüfst, dass der Ton im richtigen Moment kommt. Lautstärken und Reichweiten sind Platzhalter. + lang: prose + checklist: + - Jedes Ereignis hat einen Player-Typ mit kurzer Begründung (Ort oder kein Ort). + - Die Busse haben Namen nach einem festen Schema, und jedes Ereignis ist genau einem Bus zugeordnet. + - Für jedes Ereignis steht der Auslöser da und, bei Frames, welcher Frame. + - Für Töne mit Ort steht eine Platzhalter-Reichweite, gerechnet mit der Formel aus der Lektion. + - Du hast für mindestens ein Ereignis notiert, wie du den Moment misst (Pegelanzeige, Frame-Nummer, Ausgabe). + hints: + - Nimm die drei Wege aus der Lektion als Spalten einer Tabelle. + - Die Messung mit Solo und Mute im Audio-Panel ist eine Prüfung, die jeder wiederholen kann. + explanation: | + Der Klangplan trennt Technik und Entscheidung. Welche Ereignisse klingen, wie sie klingen und wie laut sie im Verhältnis sind, entscheidest du. Wo ein Ton technisch hingehört, damit er nicht abgeschnitten wird, auf dem richtigen Bus landet und im richtigen Frame kommt, legt der Plan fest. So kann ein Coding-Agent die Player einbauen, ohne dass er eine Entscheidung trifft, die dir gehört. + resources: + - title: Godot Docs · Audio streams + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_streams.html diff --git a/content/godot/modules/bildrate-simulation.yaml b/content/godot/modules/bildrate-simulation.yaml new file mode 100644 index 0000000..ff4aba0 --- /dev/null +++ b/content/godot/modules/bildrate-simulation.yaml @@ -0,0 +1,521 @@ +id: bildrate-simulation +title: "Bildrate und Simulation: zwei Takte im selben Spiel" +level: 2 +minutes: 120 +summary: Godot zeichnet Bilder so oft, wie Monitor und Rechner es zulassen, rechnet die Physik aber in festen Ticks – wer beide Takte auseinanderhält, erklärt Ruckeln bei 144 Hz, Zeitlupe bei Überlast und irreführende Monitorwerte. +place: unterwegs +goals: + - Du unterscheidest `_process` und `_physics_process` und sagst vorher, wie oft jede bei 30, 60 und 144 Bildern pro Sekunde läuft. + - Du erklärst, warum eine Figur auf einem 144-Hz-Monitor ohne Physik-Interpolation ungleichmäßig läuft und was die Interpolation kostet. + - Du erkennst, wann ein Spiel in Zeitlupe läuft, weil `max_physics_steps_per_frame` greift. + - Du liest FPS, Prozessierung und Physik-Prozess im Debugger und rechnest sie gegen das Frame-Budget. + - Du setzt `Engine.time_scale` als Zeitlupe zum Beobachten ein und weißt, was sie nicht anhält. +resources: + - title: Godot Docs · Idle and Physics Processing + url: https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html + - title: Godot Docs · Physics interpolation · Introduction + url: https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html + - title: Godot Docs · Fixing jitter, stutter and input lag + url: https://docs.godotengine.org/en/stable/tutorials/rendering/jitter_stutter.html + - title: Godot Docs · Engine + url: https://docs.godotengine.org/en/stable/classes/class_engine.html + - title: Godot Docs · Debugger panel · Monitors + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html#monitors +lesson: | + Am Laptop läuft die Figur im Testgelände gleichmäßig. An einem 144-Hz-Monitor wirkt dieselbe Bewegung unruhig, obwohl der Monitor mehr als doppelt so viele Bilder zeigt. Und als du zum Testen eine schwere Szene lädst, läuft plötzlich alles wie in Zeitlupe, auch die Figur, deren Code doch mit `delta` rechnet. Beides hat dieselbe Ursache: Ein Godot-Spiel hat zwei Takte. Der eine zeichnet Bilder, der andere rechnet die Simulation. Wie sie zusammenspielen, entscheidet, was auf dem Bildschirm landet. + + Im [Godot-Start](/combat/godot-start) hast du gesehen, dass ein `AnimationPlayer` im Modus `Idle` je nach Bildrate einen Frame mehr oder weniger trifft. Dahinter steckt ein Denkmodell mit zwei Takten, und Godot bringt die Werkzeuge mit, um beide zu messen. Alle Messungen hier stammen aus Läufen mit Godot 4.7.2. + + ## Zwei Takte: Bilder und Physik-Ticks + + Godot ruft zwei Funktionen automatisch auf, wenn ein Skript sie hat ([Idle and Physics Processing](https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html)): + + - **`_process(delta)`** läuft einmal pro gezeichnetem Bild. `delta` ist die Zeit seit dem letzten Bild in Sekunden und schwankt mit der Bildrate. + - **`_physics_process(delta)`** läuft in festen **Physik-Ticks**, standardmäßig 60 pro Sekunde, unabhängig von der Bildrate. Den Takt stellst du unter *Projekt → Projekteinstellungen → Physik → Allgemein* ein, der Eintrag heißt „Physik-Ticks pro Sekunde“ (`physics/common/physics_ticks_per_second`). + + ![Projekteinstellungen, Physik, Allgemein: Physik-Ticks pro Sekunde 60, Max Physik-Schritte pro Frame 8, Physik-Jitter-Korrektur 0.5, Physik-Interpolation aus](/media/godot/bildrate-simulation/physik-allgemein@2x.png "Godot 4.7.2, Projekteinstellungen mit Erweiterten Einstellungen, Standardwerte") + + Aus der Webentwicklung kennst du den ersten Takt: `_process` entspricht `requestAnimationFrame`, das der Browser einmal pro Bild aufruft. Den zweiten Takt hat das Web nicht eingebaut. Ein `setInterval` mit 16 ms wäre kein Ersatz: Er holt verpasste Aufrufe nicht nach, und 16 ms sind nicht 1/60 Sekunde. Godot holt verpasste Ticks bis zu einer Grenze nach, und jeder Tick bekommt dasselbe `delta` von 1/60 Sekunde. + + Wie viele Ticks vor einem Bild laufen, zeigt ein kleines Skript, das beide Funktionen zählt: + + ```gdscript title=zaehler.gd + extends Node + + ## Zählt, wie viele Physik-Ticks vor jedem gezeichneten Bild gelaufen sind. + + var frames := 0 + var ticks := 0 + var last_ticks := 0 + + + func _process(delta: float) -> void: + frames += 1 + print("Bild ", frames, " delta ", snappedf(delta, 0.000001), " Ticks davor ", ticks - last_ticks) + last_ticks = ticks + + + func _physics_process(_delta: float) -> void: + ticks += 1 + ``` + + Gestartet mit `--fixed-fps`: Diese Option gibt jedem Bild eine feste Spielzeit und schaltet den Abgleich mit der Echtzeit ab ([Command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html)). So bekommst du bei jedem Lauf dieselbe Ausgabe, egal wie schnell der Rechner ist. Mit `--fixed-fps 30`: + + ```text + Bild 9 delta 0.033333 Ticks davor 2 + Bild 10 delta 0.033333 Ticks davor 2 + ``` + + Mit `--fixed-fps 144`: + + ```text + Bild 12 delta 0.006944 Ticks davor 0 + Bild 13 delta 0.006944 Ticks davor 0 + Bild 14 delta 0.006944 Ticks davor 1 + Bild 15 delta 0.006944 Ticks davor 0 + Bild 16 delta 0.006944 Ticks davor 1 + ``` + + Bei 30 Bildern laufen vor jedem Bild zwei Ticks, bei 60 genau einer, bei 144 mal einer, mal keiner. In 300 Bildern zählte das Skript rund 600 Ticks bei 30 Bildern pro Sekunde, 300 bei 60 und 125 bei 144; je nachdem, wann gezählt wird, ist es ein Tick mehr oder weniger. Die Physik bleibt also bei 60 pro Sekunde; nur die Verteilung auf die Bilder ändert sich. + + > [!MERKE] + > Was auf jedem Rechner gleich lange dauern muss, läuft in `_physics_process`: Bewegung mit Kollision, Zähler für Frame-Daten, Eingabepuffer. In `_process` gehört, was nur das Bild betrifft, etwa der Text einer Debug-Anzeige. + + > [!DEEP] Die Reihenfolge in einem Durchlauf + > Ein Durchlauf der Hauptschleife misst zuerst, wie viel Zeit vergangen ist. Dann laufen alle fälligen Ticks, jeder mit `_physics_process` aller Nodes und danach dem Physik-Schritt. Erst dann kommt `_process`, einmal, und zum Schluss wird das Bild gezeichnet. Die Doku sagt dazu, `_process` laufe in einem Spiel mit einem Thread „after the physics step“. + + ## Ticks auf Bilder verteilen: warum es bei 144 Hz ruckelt + + Ein Monitor mit 144 Hz zeigt 144 Bilder pro Sekunde, die Physik liefert 60 neue Stände. 84 der 144 Bilder bekommen also keinen neuen Tick und zeigen dieselbe Position wie das Bild davor. Die Doku beschreibt diesen Treppeneffekt am extremen Beispiel mit 10 Ticks pro Sekunde: Jede neue Position steht dann sechs Bilder lang still ([Physics ticks and rendered frames](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html#physics-ticks-and-rendered-frames)). + + Gemessen an einem Klotz, der mit 90 Pixeln pro Sekunde nur in `_physics_process` nach rechts läuft, bei 144 Bildern pro Sekunde, Spielauflösung 320 × 180 mit Pixel-Snap. Pro Tick sind das 1,5 Pixel. Die Spalte „Sprung“ zählt, um wie viele Pixel die linke Kante seit dem Bild davor gewandert ist: + + ```text + Bild Ticks davor Kante x Sprung + 21 1 34 2 + 22 0 34 0 + 23 1 35 1 + 24 0 35 0 + 25 0 35 0 + 26 1 37 2 + ``` + + Der Klotz steht zwei oder drei Bilder still und springt dann um ein oder zwei Pixel. Das Auge sieht das als Ruckeln, obwohl im Mittel alles stimmt. Derselbe Klotz bei 60 Bildern pro Sekunde springt in jedem Bild, abwechselnd 1 und 2 Pixel. Bei 30 Bildern springt er in jedem Bild 3 Pixel, weil jedes Bild zwei Ticks auf einmal zeigt: grober, aber ohne Stillstand. + + ![Zwei Spalten mit je 12 Bildern untereinander: links ohne Interpolation steht der weiße Klotz mehrere Bilder an derselben Stelle und springt dann, rechts rückt er in kleinen Schritten von höchstens einem Pixel vor](/media/godot/bildrate-simulation/takt-streifen@2x.png "Godot 4.7.2, Klotz mit 90 px/s bei 144 Bildern pro Sekunde, Bild 21 bis 32, jede Zeile ein Bild, sechsfach vergrößert") + + ## Physik-Interpolation: Zwischenstände zeichnen + + Die **Physik-Interpolation** merkt sich für jedes Objekt den Stand der letzten beiden Ticks und zeichnet ein Bild an der Stelle dazwischen, die zur aktuellen Zeit passt. In 2D gibt es sie seit Godot 4.3 ([Release 4.3](https://godotengine.org/releases/4.3/#highlights-2d-physics-interpolation)). Du schaltest sie unter *Physik → Allgemein → Physik-Interpolation* ein (`physics/common/physics_interpolation`, Standard aus). Die rechte Spalte im Bild oben ist derselbe Lauf mit Interpolation: + + ```text + Bild Ticks davor Kante x Sprung + 21 1 32 0 + 22 0 33 1 + 23 1 34 1 + 24 0 34 0 + 25 0 35 1 + 26 1 35 0 + ``` + + Der Klotz rückt jetzt in etwa sechs von zehn Bildern um ein Pixel vor und steht sonst still; Sprünge von 2 Pixeln kamen in den gemessenen Bildern 20 bis 40 nicht mehr vor. Ganz glatt wird es trotzdem nicht: Mit Pixel-Snap kann der Klotz nur ganze Pixel springen, und 0,625 Pixel pro Bild ergeben ein Muster aus 0 und 1. + + Die Kosten stehen in derselben Tabelle. In Bild 21 zeigt die Interpolation die Kante bei 32, ohne Interpolation bei 34. Das Bild liegt hinter dem Stand der Physik, weil es zwischen zwei schon vergangenen Ticks liegt. Die Doku nennt ein bis zwei Ticks Rückstand ([In the past](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html#in-the-past)) und warnt vor zusätzlicher Eingabeverzögerung, gerade bei Spielen mit festem Timing ([Jitter](https://docs.godotengine.org/en/stable/tutorials/rendering/jitter_stutter.html#jitter)). Dazu kommen Regeln, die dein Code einhalten muss: + + - Interpoliert wird nur, was sich im Physik-Takt bewegt. Setzt ein Skript die Position in `_process`, zittert das Objekt ([Using physics interpolation](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/using_physics_interpolation.html#move-almost-all-game-logic-from-process-to-physics-process)). + - Springt ein Objekt absichtlich, etwa beim Neustart an einem Startpunkt, ruf danach `reset_physics_interpolation()` auf. Sonst zeichnet Godot Zwischenstände auf dem Weg dorthin. + + ```gdscript + func respawn(spawn_point: Vector2) -> void: + global_position = spawn_point + reset_physics_interpolation() # kein Zwischenstand zwischen alter und neuer Stelle + ``` + + Gemessen: Der Klotz sprang im Tick 30 von x = 64 auf x = 200. Ohne `reset_physics_interpolation()` zeigten die drei Bilder nach diesem Tick die Kante bei 64, 120 und 177, eine Schliere quer durchs Bild; erst mit dem nächsten Tick stand sie bei 200. Mit dem Aufruf stand sie schon im ersten Bild nach dem Tick bei 200 ([Call reset_physics_interpolation() when teleporting objects](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/using_physics_interpolation.html#call-reset-physics-interpolation-when-teleporting-objects)). + + > [!PRAXIS] + > Für Frame-Messungen bleibt die Interpolation aus, wie im [Godot-Start](/combat/godot-start): Dann zeigt jedes Bild genau einen Tick, und du zählst, was du siehst. Ob dein Spiel sie später bekommt, entscheidest du nach einem Test auf einem Monitor mit hoher Bildrate, mit und ohne. Ein Coding-Agent schaltet sie gern pauschal ein; prüf danach, ob irgendwo eine Position in `_process` gesetzt wird. + + ## VSync, Max FPS und das Frame-Budget + + Wie viele Bilder Godot zeichnet, bestimmen drei Dinge: + + - **VSync** wartet mit jedem Bild auf den Monitor. Die Einstellung steht unter *Anzeige → Fenster → V-Sync-Modus* (`display/window/vsync/vsync_mode`), Standard `Enabled`. Dann kann die Bildrate die Bildwiederholrate des Monitors nicht übersteigen ([Engine.max_fps](https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-max-fps)). + - **Max. FPS** unter *Anwendung → Ausführen* (`application/run/max_fps`) begrenzt die Bildrate zusätzlich; 0 heißt unbegrenzt. + - Die Rechenzeit. Ein Bild bei 60 Bildern pro Sekunde hat ein **Frame-Budget** von 1000 / 60 = 16,67 ms, bei 144 sind es 6,94 ms. Wer länger braucht, verliert Bilder. + + Zum Testen gibt es zwei Startoptionen: `--disable-vsync` schaltet VSync ab, `--max-fps 30` begrenzt die Bildrate auf 30 Bilder pro Sekunde wie bei einem langsamen Rechner ([Command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html), Abschnitt *Debug options*; beide nennt auch [Input lag](https://docs.godotengine.org/en/stable/tutorials/rendering/jitter_stutter.html#input-lag)). + + Wird ein Bild sehr langsam, müsste die Physik viele Ticks nachholen. Godot begrenzt das mit **Max Physik-Schritte pro Frame** (`physics/common/max_physics_steps_per_frame`, Standard 8). Dauert ein Bild länger als 8 Ticks, also 133 ms, fehlen Ticks, und das Spiel läuft langsamer, „even if delta is consistently used“ ([max_physics_steps_per_frame](https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-max-physics-steps-per-frame)). Gemessen mit einem Skript, das jedes Bild künstlich verzögert, jeweils 3 Sekunden Echtzeit: + + ```text + Dauer je Bild 100 ms 250 ms + Bilder 29 11 + Ticks 174 88 + höchstens Ticks/Bild 7 8 + Spielzeit 2,9 s 1,467 s + ``` + + Bei 100 ms holt die Physik alles nach. Bei 250 ms wären 15 Ticks pro Bild fällig, es laufen nur 8, und in 3 Sekunden vergehen 1,467 Sekunden Spielzeit: halbe Geschwindigkeit. Diese Zeitlupe ist kein Fehler im Code der Figur, sondern ein Zeichen, dass Bilder zu lange dauern. + + > [!WARNING] Andere Tick-Zahlen + > Änderst du „Physik-Ticks pro Sekunde“ auf einen Wert, der kein Vielfaches von 60 ist, verteilt sich der Takt auf einem 60-Hz-Monitor ungleichmäßig. Die Doku rät dann zur Physik-Interpolation ([physics_ticks_per_second](https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-physics-ticks-per-second)). Mehr Ticks kosten Rechenzeit, und die Doku empfiehlt, `max_physics_steps_per_frame` dann ebenfalls zu erhöhen. + + ## Messen: Monitore im Debugger lesen + + Im Debugger unten im Editor zeigt der Reiter **Monitore** Messwerte des laufenden Spiels ([Monitors](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html#monitors)). Für die Takte zählt die Gruppe **Zeit**: + + ![Debugger, Reiter Monitore, Gruppe Zeit: FPS 60, Prozessierung 17.89 ms, Physik-Prozess 0.10 ms, Navigations-Prozess 0.00 ms](/media/godot/bildrate-simulation/monitore-zeit@2x.png "Godot 4.7.2, Testgelände am 60-Hz-Monitor mit VSync") + + - **FPS** sind die Bilder der letzten Sekunde. + - **Prozessierung** ist laut Doku die Zeit für ein Bild. Godot nimmt dabei den längsten Durchlauf der letzten Sekunde, und darin stecken `_process`, das Zeichnen und mit VSync auch das Warten auf den Monitor (so rechnet es der [Quelltext von 4.7.2](https://github.com/godotengine/godot/blob/4.7.2-stable/main/main.cpp), Funktion `Main::iteration`). + - **Physik-Prozess** ist der längste Tick der letzten Sekunde. + + Die Monitore holen ihre Werte einmal pro Sekunde ([Custom performance monitors](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html)). Ein einzelnes langsames Bild siehst du dort als Ausschlag, aber nicht, welche Funktion schuld ist; das zeigt der Profiler in [Fehler eingrenzen](/godot/fehler-eingrenzen). + + Gemessen am selben kleinen Projekt, jeweils nach 3,5 Sekunden: + + - normaler Start mit VSync: 60 FPS, Prozessierung 19,34 ms, Physik-Prozess 0,06 ms + - mit `--disable-vsync`: 8849 FPS, Prozessierung 1,65 ms, Physik-Prozess 0,07 ms + - mit `--disable-vsync --max-fps 30`: 30 FPS, Prozessierung 0,8 ms, Physik-Prozess 0,07 ms + + Mit VSync sieht Prozessierung aus, als wäre das Budget überschritten, dabei wartet Godot nur auf den Monitor. Ohne VSync zeigt derselbe Wert die echte Arbeit. Lies die Monitore deshalb immer zusammen: FPS am Limit und Prozessierung um oder knapp über 16,67 ms heißt bei VSync meist, dass Godot auf den Monitor wartet. + + > [!INTERVIEW] + > „Ein Tester sagt, das Spiel ruckle auf seinem 144-Hz-Monitor, bei dir nicht. Wie gehst du vor?“ Gute Antwort: Zuerst trenne ich Bildrate und Simulation. Bewegt sich alles im Physik-Takt, sehen 144 Bilder nur 60 neue Stände, und Bilder ohne neuen Tick wiederholen die Position. Ich stelle das mit `--fixed-fps 144` nach und zähle die Ticks pro Bild. Dann teste ich Physik-Interpolation, prüfe, dass keine Position in `_process` gesetzt wird, und wäge die zusätzliche Verzögerung gegen die ruhigere Bewegung ab. + + ## Zeitlupe zum Beobachten: Engine.time_scale + + `Engine.time_scale` streckt die Spielzeit: Bei 0.5 läuft alles, was mit `delta` rechnet, halb so schnell ([Engine.time_scale](https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-time-scale)). Die Zahl der Ticks pro Sekunde bleibt 60, nur ihr `delta` halbiert sich auf 0,00833. Gemessen im Testgelände mit den Platzhaltern 90/600/900: Bei 1.0 erreicht die Figur 90 px/s nach 9 Ticks, bei 0.5 nach 18 Ticks, und pro Tick legt sie bei vollem Tempo 0,75 statt 1,5 Pixel zurück. + + Daraus folgen zwei Dinge. Zum Zusehen ist `time_scale` praktisch: Bei 0.25 erkennst du, wo eine Bewegung hakt. Zum Anhalten taugt es nicht, denn Code, der Ticks zählt, zählt weiter; das hat [Testszene: Angriff](/combat/testszene-angriff) bei `time_scale = 0` gemessen. Auch der Ton läuft im normalen Tempo weiter. Für Einzelbilder nimmst du Pause und Nächster Frame. Starten lässt sich die Zeitlupe auch von außen mit `--time-scale 0.5`. + + ## Häufige Fehler + + - **Frames in `_process` gezählt.** Ein Ausweichen mit 18 Frames dauerte gemessen bei 60 Bildern pro Sekunde 0,3 s, bei 144 rund 0,13 s, bei 30 rund 0,6 s. In `_physics_process` waren es immer 0,3 s. + - **Bewegung in `_process` ohne `delta`.** Die Figur läuft auf schnellen Monitoren schneller. + - **Position in `_process` gesetzt, Interpolation an.** Das Objekt zittert, weil die Interpolation nur Ticks kennt. + - **Teleport ohne `reset_physics_interpolation()`.** Ein, zwei Bilder zeigen das Objekt auf halbem Weg. + - **Prozessierung mit VSync für Rechenzeit gehalten.** Ohne VSync messen oder den Profiler nehmen. + - **Zeitlupe für einen Code-Fehler gehalten.** Fallen die FPS unter 60 / 8 = 7,5, greift `max_physics_steps_per_frame`, und das ganze Spiel wird langsamer. + - **`Engine.time_scale = 0` als Pause.** Tick-Zähler laufen weiter, der Ton auch. + + ## Zusammenfassung + + - `_process` läuft pro Bild mit schwankendem `delta`, `_physics_process` in festen Ticks, standardmäßig 60 pro Sekunde mit `delta` 1/60. + - Bei 30 Bildern laufen zwei Ticks pro Bild, bei 144 mal einer, mal keiner. 84 von 144 Bildern zeigen dann keinen neuen Stand. + - Physik-Interpolation glättet die Bewegung mit Zwischenständen und kostet dafür Verzögerung. Sie wirkt nur auf Bewegung im Physik-Takt; nach Teleports `reset_physics_interpolation()`. + - VSync und Max. FPS begrenzen die Bildrate. Das Frame-Budget ist 16,67 ms bei 60 und 6,94 ms bei 144 Bildern pro Sekunde. + - Dauert ein Bild länger als 8 Ticks, läuft das Spiel in Zeitlupe, auch wenn der Code `delta` nutzt. + - Prozessierung enthält mit VSync das Warten auf den Monitor. Rechenzeit misst du ohne VSync oder im Profiler. + - `Engine.time_scale` streckt `delta`, nicht die Zahl der Ticks. Zum Beobachten gut, zum Anhalten nicht. +exercises: + - id: takt-zuordnen + type: choice + title: Welcher Takt für welche Arbeit? + prompt: | + Welche Zuordnungen stimmen für ein Spiel, das auf 60-Hz- und 144-Hz-Monitoren gleich laufen soll? + options: + - text: "Figur mit `velocity` und `move_and_slide()` bewegen: `_physics_process`" + correct: true + why: Die Bewegung mit Kollision gehört in den festen Takt. Nur dort wirkt auch die Physik-Interpolation. + - text: "Den Text einer Debug-Anzeige mit dem aktuellen Tempo setzen: `_process`" + correct: true + why: Die Anzeige betrifft nur das Bild. Ob sie 60- oder 144-mal pro Sekunde neu geschrieben wird, ändert am Spiel nichts. + - text: "Die Frames eines Ausweichens herunterzählen: `_process`" + correct: false + why: Dann hängt die Dauer an der Bildrate. Gemessen dauerten 18 Frames bei 144 Bildern pro Sekunde rund 0,13 s statt 0,3 s. + - text: "Die Position eines Objekts setzen, während die Physik-Interpolation an ist: `_process`" + correct: false + why: Die Interpolation rechnet zwischen zwei Ticks. Positionen, die außerhalb eines Ticks gesetzt werden, lassen das Objekt zittern. + explanation: | + Die Regel lautet: Was auf jedem Rechner gleich lange dauern oder gleich weit führen muss, läuft in `_physics_process`. In `_process` gehört, was nur die Darstellung betrifft. Der feste Takt liefert 60 Ticks pro Sekunde mit immer gleichem `delta`, `_process` läuft so oft, wie Bilder gezeichnet werden. Die Doku empfiehlt mit Physik-Interpolation ausdrücklich, fast alle Spiellogik in `_physics_process` zu verlegen. + resources: + - title: Godot Docs · Idle and Physics Processing + url: https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html + - title: Godot Docs · Using physics interpolation + url: https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/using_physics_interpolation.html#move-almost-all-game-logic-from-process-to-physics-process + + - id: durchlauf-ordnen + type: order + title: Ein Durchlauf der Hauptschleife + prompt: | + Das Spiel läuft mit 30 Bildern pro Sekunde, die Physik mit 60 Ticks. Bring die Schritte eines Durchlaufs in die Reihenfolge, in der Godot 4.7 sie abarbeitet. + lang: prose + lines: | + Godot misst, wie viel Zeit seit dem letzten Durchlauf vergangen ist. + Erster fälliger Tick: `_physics_process` aller Nodes, danach der Physik-Schritt. + Zweiter fälliger Tick: `_physics_process` aller Nodes, danach der Physik-Schritt. + `_process` aller Nodes, einmal. + Godot zeichnet das Bild. + explanation: | + Aus der vergangenen Zeit ergibt sich, wie viele Ticks fällig sind; bei 30 Bildern pro Sekunde sind es zwei. Jeder Tick ruft zuerst `_physics_process` auf, dann rechnet die Physik. Erst danach läuft `_process`, die Doku sagt „after the physics step“, und zum Schluss entsteht das Bild. Deshalb sieht `_process` immer die Positionen nach dem letzten Tick, und das Bild zeigt ohne Interpolation genau diesen Stand. + resources: + - title: Godot Docs · Idle and Physics Processing + url: https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html + + - id: zaehler-bei-30 + type: output + title: Was meldet das zehnte Bild? + prompt: | + Das Spiel startet mit `--fixed-fps 30`, die Physik läuft mit 60 Ticks pro Sekunde. `ticks` zählt in `_physics_process` mit. Was gibt diese Funktion im 10. Bild aus? + lang: gdscript + code: | + func _process(delta: float) -> void: + frames += 1 + print("Bild ", frames, " delta ", snappedf(delta, 0.000001), " Ticks davor ", ticks - last_ticks) + last_ticks = ticks + expected: | + Bild 10 delta 0.033333 Ticks davor 2 + accept: + - Bild 10 delta 0.033333 Ticks davor 2 + - "Bild 10 delta 0.03333 Ticks davor 2" + explanation: | + Ein Bild dauert bei 30 Bildern pro Sekunde 1/30 s, das ist das `delta` von `_process`. In dieser Zeit sind zwei Physik-Ticks zu je 1/60 s fällig, und beide laufen vor `_process`. Gemessen mit Godot 4.7.2: In den ersten Bildern schwankt `delta` noch (0.025, 0.041667), ab Bild 7 steht es bei 0.033333, und jedes Bild hat zwei Ticks davor. + resources: + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html + + - id: bilder-ohne-tick + type: output + title: Wie viele Bilder wiederholen sich? + prompt: | + Ein Monitor zeigt 144 Bilder pro Sekunde, die Physik rechnet 60 Ticks pro Sekunde, die Physik-Interpolation ist aus. Kein Bild bekommt mehr als einen Tick. Wie viele der 144 Bilder einer Sekunde zeigen dieselbe Position wie das Bild davor? + lang: text + code: | + Bilder pro Sekunde 144 + Ticks pro Sekunde 60 + Interpolation aus + expected: | + 84 + accept: + - 84 Bilder + explanation: | + 60 Bilder bekommen einen neuen Tick, die anderen 144 − 60 = 84 zeigen den Stand des letzten Ticks noch einmal. Gemessen mit Godot 4.7.2 bei `--fixed-fps 144`: In 300 Bildern liefen 125 Ticks, also 60 pro 144 Bilder. Weil die Bilder ohne Tick unregelmäßig verteilt sind (0, 0, 1, 0, 1 …), wirkt die Bewegung unruhig, obwohl der Monitor mehr Bilder zeigt. + resources: + - title: Godot Docs · Physics ticks and rendered frames + url: https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html#physics-ticks-and-rendered-frames + + - id: zeitlupe-log + type: output + title: Das Log in halber Geschwindigkeit + prompt: | + Im Testgelände ist am Player `debug_log` an, `Engine.time_scale` steht auf 0.5. Die Werte sind die Platzhalter 90/600/900. Ab Tick 10 ist `move_right` gedrückt, vorher stand die Figur. Welche drei Zeilen gibt `player.gd` in den Ticks 10, 11 und 12 aus? + lang: gdscript + code: | + func _physics_process(delta: float) -> void: + var input := Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down") + var rate := stats.acceleration if input != Vector2.ZERO else stats.friction + velocity = velocity.move_toward(input * stats.max_speed, rate * delta) + if input != Vector2.ZERO: + facing = input + move_and_slide() + if debug_log: + print(Engine.get_physics_frames(), " speed ", snappedf(velocity.length(), 0.1)) + expected: | + 10 speed 5.0 + 11 speed 10.0 + 12 speed 15.0 + accept: + - | + 10 speed 5.0 + 11 speed 10.0 + 12 speed 15.0 + - | + 10 speed 5 + 11 speed 10 + 12 speed 15 + explanation: | + `time_scale` 0.5 halbiert `delta` auf 1/120 s. `move_toward` erhöht das Tempo pro Tick um `acceleration × delta` = 600 / 120 = 5 statt 10 px/s. Die Zahl der Ticks pro Sekunde bleibt 60, deshalb zählt `Engine.get_physics_frames()` normal weiter. Gemessen mit Godot 4.7.2: bei `time_scale` 1.0 stand in Tick 10 `speed 10.0`, bei 0.5 `speed 5.0`; 90 px/s erreicht die Figur nach 18 statt 9 Ticks. + resources: + - title: Godot Docs · Engine.time_scale + url: https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-time-scale + + - id: ueberlast-ticks + type: output + title: Wenn ein Bild 250 ms braucht + prompt: | + Eine schwere Szene braucht für jedes Bild 250 ms. Die Projekteinstellungen stehen auf den Standardwerten. Wie viele Physik-Ticks laufen pro Bild? + lang: text + code: | + Physik-Ticks pro Sekunde 60 + Max Physik-Schritte pro Frame 8 + Dauer je Bild 250 ms + expected: | + 8 + accept: + - 8 Ticks + explanation: | + Fällig wären 0,25 × 60 = 15 Ticks, erlaubt sind höchstens 8. Die übrigen 7 fallen weg, und pro Bild vergehen nur 8 / 60 = 0,133 s Spielzeit statt 0,25 s. Gemessen mit Godot 4.7.2: In 3 Sekunden Echtzeit liefen 11 Bilder mit 88 Ticks, die Spielzeit betrug 1,467 s. Das Spiel lief also in halber Geschwindigkeit, auch wenn jeder Code mit `delta` rechnet. + resources: + - title: Godot Docs · Engine.max_physics_steps_per_frame + url: https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-max-physics-steps-per-frame + + - id: teleport-ergaenzen + type: gap + title: Ohne Schliere zum Startpunkt + prompt: | + Die Physik-Interpolation ist an. Ergänze die Lücken, damit die Figur interpoliert läuft und beim Neustart ohne Zwischenbild am Startpunkt erscheint. + lang: gdscript + code: | + func ⟦_physics_process⟧(delta: float) -> void: + velocity = direction * speed + move_and_slide() + + + func respawn(spawn_point: Vector2) -> void: + global_position = spawn_point + ⟦reset_physics_interpolation()⟧ + gaps: + - label: Funktion für den festen Takt + hint: Die Funktion, die Godot vor jedem Physik-Schritt aufruft. + - label: Interpolation zurücksetzen + hint: Eine Methode von `Node`, die den gemerkten vorigen Stand auf den aktuellen setzt. + accept: ["self.reset_physics_interpolation()"] + explanation: | + Interpoliert wird nur, was sich im Physik-Takt bewegt, deshalb steht die Bewegung in `_physics_process`. Nach einem Sprung setzt `reset_physics_interpolation()` den gemerkten vorigen Stand gleich dem neuen. Ohne den Aufruf zeigten im Test mit Godot 4.7.2 zwei Bilder den Klotz unterwegs, bei x = 120 und 177 auf dem Weg von 64 nach 200. + resources: + - title: Godot Docs · Node.reset_physics_interpolation + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-reset-physics-interpolation + + - id: fester-takt-starten + type: command + title: 144 Bilder pro Sekunde nachstellen + prompt: | + Du hast keinen 144-Hz-Monitor, willst aber sehen, wie sich die Ticks auf 144 Bilder verteilen. Starte das Spiel so, dass jedes Bild genau 1/144 Sekunde Spielzeit bekommt, unabhängig davon, wie schnell dein Rechner ist. + context: | + Du bist im Projektordner. `godot` steht für dein Godot-Binary. + answers: + - godot --fixed-fps 144 + - godot --path . --fixed-fps 144 + - godot --fixed-fps 144 --path . + explanation: | + `--fixed-fps` gibt jedem Bild dieselbe Spielzeit und schaltet den Abgleich mit der Echtzeit ab. Das Ergebnis ist bei jedem Lauf gleich, deshalb eignet es sich für Messungen wie die Tick-Verteilung in der Lektion. `--max-fps 144` wäre etwas anderes: Es begrenzt nur die Bildrate, und auf einem 60-Hz-Monitor mit VSync kommst du gar nicht so hoch. + resources: + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html + + - id: ohne-vsync-starten + type: command + title: Rechenzeit ohne Warten messen + prompt: | + Der Monitor „Prozessierung“ zeigt mit VSync rund 19 ms. Starte das Spiel ohne VSync, damit der Wert die echte Arbeit pro Bild zeigt. + context: | + Du bist im Projektordner. `godot` steht für dein Godot-Binary. + answers: + - godot --disable-vsync + - godot --path . --disable-vsync + - godot --disable-vsync --path . + explanation: | + `--disable-vsync` schaltet VSync ab, auch wenn die Projekteinstellung es vorsieht. Dann wartet Godot nicht mehr auf den Monitor, und Prozessierung zeigt Verarbeitung und Zeichnen. Gemessen mit Godot 4.7.2 an einem kleinen Projekt: mit VSync 19,34 ms bei 60 FPS, ohne 1,65 ms bei 8849 FPS. Für den Test ist das richtig; für Spieler entscheidest du später, ob VSync an bleibt. + resources: + - title: Godot Docs · Fixing jitter, stutter and input lag + url: https://docs.godotengine.org/en/stable/tutorials/rendering/jitter_stutter.html#input-lag + + - id: prozessierung-lesen + type: choice + title: Sind 17,89 ms zu viel? + prompt: | + Das Testgelände läuft am 60-Hz-Monitor mit VSync. Der Debugger zeigt diese Werte. Welche Aussage stimmt? + + ![Debugger, Reiter Monitore, Gruppe Zeit: FPS 60, Prozessierung 17.89 ms, Physik-Prozess 0.10 ms](/media/godot/bildrate-simulation/monitore-zeit@2x.png "Godot 4.7.2, Monitore des Testgeländes") + options: + - text: Das Spiel schafft alle 60 Bilder. In den 17,89 ms steckt das Warten auf den Monitor; die Rechenzeit zeigt erst ein Lauf ohne VSync. + correct: true + why: FPS stehen am Limit. Prozessierung ist der längste Durchlauf der letzten Sekunde inklusive Zeichnen und Warten. + - text: Das Spiel überschreitet das Budget von 16,67 ms und verliert Bilder. + correct: false + why: Dann läge FPS unter 60. Gemessen ohne VSync brauchte ein ähnliches Projekt 1,65 ms. + - text: Die Physik ist der Engpass, weil sie 60-mal pro Sekunde läuft. + correct: false + why: Der längste Tick dauerte 0,10 ms, ein winziger Teil des Budgets. + - text: Weil Prozessierung über 16,67 ms liegt, rechnet auch die Physik weniger als 60 Ticks pro Sekunde. + correct: false + why: Die Physik holt fällige Ticks nach, bis zu 8 pro Bild. Weniger als 60 Ticks pro Sekunde gibt es erst, wenn ein Bild länger als 133 ms dauert. + explanation: | + Prozessierung misst den längsten Durchlauf aus Verarbeitung und Zeichnen der letzten Sekunde, mit VSync einschließlich der Zeit, in der Godot auf den Monitor wartet. Ein Wert um 16 bis 19 ms bei 60 FPS ist deshalb normal. Erst ohne VSync oder im Profiler siehst du, wie viel davon dein Spiel tatsächlich rechnet. + resources: + - title: Godot Docs · Debugger panel · Monitors + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html#monitors + - title: Godot Docs · Performance.TIME_PROCESS + url: https://docs.godotengine.org/en/stable/classes/class_performance.html#class-performance-constant-time-process + + - id: ausweichen-zaehlt-bilder + type: bug + title: Am schnellen Monitor kürzer + prompt: | + Ein Coding-Agent hat das Ausweichen gebaut. Am 60-Hz-Monitor dauert es 0,3 Sekunden, am 144-Hz-Monitor rund 0,13 Sekunden, mit 30 Bildern pro Sekunde rund 0,6. Finde die Zeile, die das verursacht, und korrigiere sie. + lang: gdscript + code: | + var dodge_frames_left := 0 + + + func start_dodge() -> void: + dodge_frames_left = 18 # Platzhalter + + + func _process(_delta: float) -> void: + if dodge_frames_left > 0: + dodge_frames_left -= 1 + lines: [8] + fix: + - line: 8 + answers: + - "func _physics_process(_delta: float) -> void:" + - "func _physics_process(delta: float) -> void:" + - "func _physics_process(_delta):" + - "func _physics_process(delta):" + - "func _physics_process(_delta) -> void:" + - "func _physics_process(delta) -> void:" + explanation: | + `_process` läuft einmal pro gezeichnetem Bild. 18 Bilder dauern bei 144 Bildern pro Sekunde 0,125 s, bei 30 Bildern 0,6 s. In `_physics_process` sind 18 Ticks immer 0,3 s. Gemessen mit Godot 4.7.2 und `--fixed-fps`: 0,3 s, 0,132 s und 0,592 s in `_process`, 0,3 s in allen drei Fällen in `_physics_process`. Die Zahl 18 ist ein Platzhalter; wie lange dein Ausweichen dauert, entscheidest du. + resources: + - title: Godot Docs · Idle and Physics Processing + url: https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html + + - id: interpolation-abwaegen + type: explain + title: Was die Interpolation zeigt und was sie kostet + prompt: | + Erkläre anhand dieser Messung in wenigen Sätzen, was die Physik-Interpolation am Bild ändert und welchen Preis sie hat. Der Klotz läuft mit 90 px/s nur in `_physics_process`, das Spiel zeigt 144 Bilder pro Sekunde. + lang: text + code: | + Kante x des Klotzes + Bild Ticks davor ohne mit + 20 0 32 32 + 21 1 34 32 + 22 0 34 33 + 23 1 35 34 + 24 0 35 34 + 25 0 35 35 + 26 1 37 35 + points: + - Ohne Interpolation zeigt jedes Bild den Stand des letzten Ticks; Bilder ohne neuen Tick wiederholen die Position, danach springt der Klotz um bis zu 2 Pixel. + - Mit Interpolation zeichnet Godot Zwischenstände, der Klotz rückt in kleinen, gleichmäßigeren Schritten von 0 oder 1 Pixel vor. + - Der gezeichnete Stand liegt zwischen zwei vergangenen Ticks und damit hinter der Physik, hier 2 Pixel in Bild 21; das bedeutet zusätzliche Eingabeverzögerung. + explanation: | + Ohne Interpolation landet jeder Tick unverändert im nächsten Bild. Bei 144 Bildern und 60 Ticks bekommen manche Bilder keinen Tick, die Position steht, und beim nächsten Tick holt der Klotz 1,5 Pixel auf einmal auf, gerundet 1 oder 2. Mit Interpolation verteilt Godot den Weg auf die Bilder dazwischen, Pixel-Snap rundet auf 0 oder 1. Dafür zeigt jedes Bild einen Stand aus der Vergangenheit: In Bild 21 steht die Kante bei 32 statt 34. Die Doku nennt ein bis zwei Ticks Rückstand und warnt vor mehr Eingabeverzögerung, besonders bei Spielen mit festem Timing. + resources: + - title: Godot Docs · Physics interpolation · Introduction + url: https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html + + - id: messplan-takte + type: practice + title: Ein Messplan für deine Takte + prompt: | + Schreib in drei bis fünf Sätzen auf, wie du in deinem Projekt prüfst, ob Bewegung und Timing auf verschiedenen Monitoren gleich sind. Nenne, welche Startoptionen du nutzt, was du dabei zählst oder abliest und wo du es notierst. Welche Ergebnisse du dir wünschst, gehört nicht hinein, nur wie du misst. + lang: prose + checklist: + - Du hast die Bildwiederholrate deines eigenen Monitors notiert. + - Der Plan nennt mindestens zwei Läufe mit `--fixed-fps` oder `--max-fps` und die Werte, die du dabei vergleichst. + - Du hast festgelegt, wo du zählst, etwa Ticks im Log oder FPS und Prozessierung im Monitor. + - Ein Lauf mit und einer ohne Physik-Interpolation stehen darin, und die Entscheidung darüber ist als offen markiert. + - Für Prozessierung steht dabei, ob VSync an oder aus war. + hints: + - Denk an die drei Bildraten aus der Lektion, 30, 60 und 144. + - Die Ausgabe von `print()` mit `Engine.get_physics_frames()` liefert dir die Tick-Nummer für das Log. + explanation: | + Ein Messplan trennt die beiden Takte, bevor du an Werten drehst. Läufe mit festem Takt liefern bei jeder Wiederholung dieselben Zahlen, Läufe in Echtzeit zeigen, was ein Spieler sieht. Wenn du notierst, unter welchen Bedingungen ein Wert entstand, kannst du ihn später mit einer neuen Godot-Version oder einem anderen Rechner vergleichen. Wie sich die Bewegung anfühlen soll, entscheidest du erst danach. + resources: + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html diff --git a/content/godot/modules/editor-anatomie.yaml b/content/godot/modules/editor-anatomie.yaml new file mode 100644 index 0000000..0fb6a61 --- /dev/null +++ b/content/godot/modules/editor-anatomie.yaml @@ -0,0 +1,594 @@ +id: editor-anatomie +title: "Der Editor als Werkstatt: Docks, Projektordner und res://" +level: 1 +minutes: 100 +summary: Was ein Coding-Agent baut, liegt als Textdatei im Projektordner. Hier lernst du, diese Dateien zu lesen, im Editor zu finden und an den Docks abzulesen, was eingestellt ist und was nur Standard ist. +place: unterwegs +goals: + - Du ordnest die Dateien eines Godot-Projekts ein (`project.godot`, `.tscn`, `.tres`, `.gd`, `.uid`, `.import`, `.godot/`) und weißt, welche Godot selbst erzeugt. + - Du unterscheidest `res://` und `user://` und findest den Ordner hinter `user://` unter Linux. + - Du sagst zu einer Frage an ein Projekt, welches Dock sie beantwortet. + - Du liest den Inspektor nach Vererbungsabschnitten und erkennst am Zurücksetzen-Pfeil, welche Werte vom Standard abweichen. + - Du startest gezielt die Hauptszene oder die offene Szene und weißt, wo Ausgaben landen. +resources: + - title: Godot Docs · First look at Godot's interface + url: https://docs.godotengine.org/en/stable/getting_started/introduction/first_look_at_the_editor.html + - title: Godot Docs · File system + url: https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html + - title: Godot Docs · File paths in Godot projects + url: https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html + - title: Godot Docs · Inspector Dock + url: https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html +lesson: | + Ein Coding-Agent meldet: „Bewegung eingebaut. Die Werte liegen in `data/player_stats.tres`, die Figur ist in `main.tscn` eingesetzt.“ Du öffnest das Projekt und siehst acht Bereiche, drei Leisten und eine graue Fläche. Wo liegt die Datei? Wo stellst du die Höchstgeschwindigkeit ein? Und woran erkennst du, ob ein Wert dort bewusst gesetzt wurde oder nur der Standard ist? + + Bevor du irgendetwas abstimmst, musst du diese Fragen in einer Minute beantworten können. Alles, was ein Agent baut, landet als Datei im Projektordner. Der Editor zeigt dir diese Dateien, und was du dort änderst, schreibt er zurück. Die Beispiele stammen aus dem **Testgelände**, einem neutralen Projekt mit einer Figur, einer Kiste, drei Sammelstücken und einem Zähler. Du baust es in [Ein fremdes Projekt erkunden](/godot/projekt-erkunden) selbst auf; hier reichen die Screenshots. Installation und Pixel-Einstellungen stehen in [Godot-Start](/combat/godot-start). Alles gilt für Godot 4.7.2. + + ## Der Projektordner: jede Einstellung ist eine Datei + + Godot hält jeden Ordner mit einer Datei `project.godot` für ein Projekt. Der Ort dieser Datei ist die Wurzel, die Godot `res://` nennt ([File system](https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html#project-godot)). Nach dem ersten Öffnen im Editor sieht das Testgelände so aus: + + ```console + $ ls -A + crate.tscn pickup.gd + data pickup.gd.uid + .godot pickup.tscn + hud.gd player.gd + hud.gd.uid player.gd.uid + main.tscn player.tscn + movement_stats.gd project.godot + movement_stats.gd.uid + ``` + + - **`project.godot`**: Projekteinstellungen wie Name, Hauptszene, Eingabeaktionen, Layer-Namen und Autoloads. Der Editor schreibt sie, wenn du unter *Projekt → Projekteinstellungen* etwas änderst. + - **`.tscn`**: eine Szene, also ein Baum aus Nodes mit ihren geänderten Eigenschaften. Der Editor schreibt sie beim Speichern. + - **`.tres`**: eine Ressource, ein Datenobjekt wie die Bewegungswerte. Du änderst sie im Inspektor. + - **`.gd`**: ein GDScript, von dir oder vom Agenten. + - **`.gd.uid`**: die eindeutige ID des Skripts. Godot legt sie selbst an. + - **`.png.import`**: die Import-Einstellungen eines Bildes; Töne bekommen ebenso eine `.import`. Das Import-Dock schreibt sie. + - **`.godot/`**: der Cache mit importierten Dateien und dem Zustand des Editors. Godot erzeugt ihn jederzeit neu. + + Die `.uid`-Dateien gibt es seit Godot 4.4. Skripte haben keinen Platz für Godot-eigene Daten, also legt Godot die ID daneben ab. Szenen und Ressourcen tragen ihre ID im Dateikopf, importierte Dateien in der `.import`-Datei ([Godot 4.4: Universal UID support](https://godotengine.org/releases/4.4/#universal-uid-support)). Legst du ein Bild `pruefmuster.png` ins Projekt, erscheint daneben `pruefmuster.png.import`, und das umgewandelte Ergebnis landet in `.godot/imported/`. Wie du den Import einstellst, steht im [Modul zum Sprite-Import](/godot/sprites-importieren). + + `project.godot` ist eine Textdatei im INI-Format. Ein Ausschnitt aus dem Testgelände: + + ```ini title=project.godot + [application] + + config/name="Testgelände" + run/main_scene="res://main.tscn" + config/features=PackedStringArray("4.7", "GL Compatibility") + + [layer_names] + + 2d_physics/layer_1="world" + 2d_physics/layer_2="player" + 2d_physics/layer_3="pickups" + ``` + + Ein Autoload, ein Node, den Godot vor jeder Szene in den Baum hängt, stünde als `[autoload]` mit einer Zeile wie `HitLog="*res://hit_log.gd"` in derselben Datei ([Singletons (Autoload)](https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html#autoload)). Das Testgelände hat keinen. + + Weil alles Text ist, zeigt Git dir, was eine Sitzung im Editor geändert hat. Die Vorlagen des Testgeländes sind von Hand geschrieben, `data/player_stats.tres` nennt alle drei Werte. Stellst du im Inspektor `max_speed` von 90 auf 120 und speicherst, schreibt der Editor die Datei zum ersten Mal selbst: + + ```console + $ git diff + diff --git a/data/player_stats.tres b/data/player_stats.tres + index 7009262..02fefd4 100644 + --- a/data/player_stats.tres + +++ b/data/player_stats.tres + @@ -1,9 +1,7 @@ + -[gd_resource type="Resource" script_class="MovementStats" format=3] + +[gd_resource type="Resource" script_class="MovementStats" format=3 uid="uid://c328erhiur8b8"] + + -[ext_resource type="Script" path="res://movement_stats.gd" id="1"] + +[ext_resource type="Script" uid="uid://bwctjxhdkev23" path="res://movement_stats.gd" id="1"] + + [resource] + script = ExtResource("1") + -max_speed = 90.0 + -acceleration = 600.0 + -friction = 900.0 + +max_speed = 120.0 + ``` + + Drei Dinge passieren: Der Kopf und der Verweis aufs Skript bekommen eine `uid`. `max_speed` bekommt den neuen Wert. Und `acceleration` und `friction` verschwinden, weil ihre Werte dem Standard aus `movement_stats.gd` entsprechen; der Editor speichert nur, was davon abweicht. Ab jetzt sind Diffs klein. Stellst du danach `acceleration` auf 700, ändert sich genau eine Zeile: + + ```console + $ git diff + diff --git a/data/player_stats.tres b/data/player_stats.tres + index 02fefd4..299394e 100644 + --- a/data/player_stats.tres + +++ b/data/player_stats.tres + @@ -5,3 +5,4 @@ + [resource] + script = ExtResource("1") + max_speed = 120.0 + +acceleration = 700.0 + ``` + + > [!MERKE] + > Was in einer `.tscn` oder `.tres` nicht steht, hat den Standardwert. Ändert jemand den Standard im Skript, ändert sich jeder Wert, der nicht ausdrücklich in einer Datei steht. + + > [!DEEP] Der Cache-Ordner + > Nach dem ersten Öffnen enthält `.godot/` unter anderem `imported/` (umgewandelte Assets), `editor/` (offene Szenen, eingeklappte Abschnitte), `uid_cache.bin` und `global_script_class_cache.cfg` (alle `class_name`). Nichts davon ist Quelle. Löschst du den Ordner bei geschlossenem Editor, erzeugt Godot ihn beim nächsten Öffnen neu und importiert alle Assets erneut. Deshalb gehört er nicht ins Repository ([Git-Einstieg](/git/git-einstieg)). + + ## `res://` und `user://`: zwei Wurzeln + + `res://` zeigt immer auf den Projektordner. Im Editor kannst du dort schreiben, in einem exportierten Spiel ist er nur lesbar ([File system](https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html#resource-path)). Für Spielstände, Einstellungen und Protokolle gibt es `user://`: Godot legt den Ordner selbst an, und er ist immer beschreibbar ([File paths](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#accessing-persistent-user-data-user)). Unter Linux heißt er `~/.local/share/godot/app_userdata//`, der Name kommt aus `config/name`: + + ```gdscript + print(ProjectSettings.globalize_path("user://")) + ``` + + ```text + /home/stevenh/.local/share/godot/app_userdata/Testgelände/ + ``` + + Dort liegt nach dem ersten Start schon etwas: Godot schreibt Protokolle der Läufe nach `user://logs/godot.log` ([Logging](https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html)). Im Editor öffnet *Projekt → Userdaten-Ordner öffnen* den Ordner im Dateimanager. + + Aus dem Web kennst du die Trennung: Der gebaute Code der App ist schreibgeschützt, Nutzerdaten kommen in `localStorage` oder IndexedDB. Anders als dort ist `user://` ein normaler Ordner, dessen Dateien du mit `ls` und jedem Editor ansehen kannst. + + ## Docks und Bildschirme: welches Fenster beantwortet welche Frage + + ![Godot-Editor mit geöffneter main.tscn; acht gelbe Nummern markieren Szenen-Dock, Dateisystem, die Hauptbildschirme 2D bis Asset Store, die Start-Knöpfe, Inspektor, den Reiter Signale, das untere Panel und die 2D-Ansicht](/media/godot/editor-anatomie/editor-ueberblick@2x.png "Godot 4.7.2, Testgelände, main.tscn mit gewähltem Player") + + Auf dem Handy tippst du das Bild an, dann öffnet es sich groß. + + | Bereich | Frage, die er beantwortet | + | --- | --- | + | 1 · **Szenen-Dock**, Reiter „Szene“ (Scene) | Woraus besteht die offene Szene, welcher Node ist gewählt? | + | 2 · **Dateisystem** (FileSystem) | Welche Dateien hat das Projekt, welche ist die Hauptszene? | + | 3 · **2D, 3D, Skript, Spiel, Asset Store** | Bearbeite ich Szene oder Code, oder schaue ich dem laufenden Spiel zu? | + | 4 · **Start-Knöpfe** | Welche Szene startet? | + | 5 · **Inspektor** (Inspector) | Welche Werte hat der gewählte Node oder die Ressource? | + | 6 · **Signale** (Signals), **Gruppen** (Groups) | Wer reagiert auf welches Ereignis, in welchen Gruppen steckt der Node? | + | 7 · **unteres Panel**: Ausgabe, Debugger, Audio, Animation, Shader-Editor | Was hat das Spiel ausgegeben, welche Fehler gab es? | + | 8 · **2D-Ansicht** | Wo liegt was im Raum? | + + Hinter den Reitern stecken weitere Docks: **Importieren** neben dem Reiter Szene, **Verlauf** neben Dateisystem. Der Verlauf listet deine letzten Änderungen, die du rückgängig machen kannst. Gruppen ist in 4.7.2 ein eigenes Dock neben Signale; bei schmalem Fenster erreichst du es über die Pfeile rechts der Reiter. Jedes Dock findest du außerdem unter *Editor → Editor-Docks*. Die drei Filterfelder „Nodes filtern“, „Dateien filtern“ und „Eigenschaften filtern“ durchsuchen jeweils ihr Dock ([First look at Godot's interface](https://docs.godotengine.org/en/stable/getting_started/introduction/first_look_at_the_editor.html)). + + Das Dateisystem-Dock zeigt nicht alles, was im Ordner liegt: + + ![Dateisystem-Dock des Testgeländes: Favoriten, res://, Ordner data mit player_stats.tres, darunter die Szenen und Skripte; main.tscn ist blau, .uid-Dateien fehlen in der Liste](/media/godot/editor-anatomie/dateisystem@2x.png "Godot 4.7.2, Dateisystem-Dock des Testgeländes") + + `.uid`- und `.import`-Dateien blendet es aus, `.godot/` ebenso. Die Hauptszene, hier `main.tscn`, steht in Blau. Was das Dock nicht zeigt, siehst du im Terminal. + + ## Den Inspektor lesen + + Der Inspektor zeigt die Eigenschaften des gewählten Nodes, gegliedert nach Klassen. Oben steht die Klasse des Skripts, darunter jede Engine-Klasse, von der der Node erbt ([Inspector Dock](https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html#usage)). Für den Wurzel-Node von `player.tscn`: + + ![Inspektor des Player aus player.tscn: Abschnitte Player, CharacterBody2D, CollisionObject2D, Node2D, CanvasItem, Node; Zurücksetzen-Pfeile bei Stats, Motion Mode und Layer](/media/godot/editor-anatomie/inspektor-player@2x.png "Godot 4.7.2, Inspektor von player.tscn, Wurzel-Node Player") + + - **Player** kommt aus `class_name Player` in `player.gd`. Hier stehen die exportierten Variablen `stats` und `debug_log`. Ein Skript ohne `class_name` erscheint mit seinem Dateinamen, etwa `pickup.gd`. + - **CharacterBody2D → CollisionObject2D → Node2D → CanvasItem → Node** ist die Vererbungskette. Zwischen CharacterBody2D und CollisionObject2D liegt noch `PhysicsBody2D`; es bringt keine eigenen Eigenschaften mit und bekommt deshalb keinen Abschnitt. + - Suchst du eine Eigenschaft, frag dich, welche Klasse sie einführt: Kollisionsebenen gehören zu CollisionObject2D, die Position zu Node2D, Sichtbarkeit und Zeichenreihenfolge (*Ordering*) zu CanvasItem. + + Die Namen im Inspektor sind aufbereitet: Aus `max_speed` wird „Max Speed“. Fährst du mit der Maus über eine Eigenschaft, zeigt der Tooltip die Beschreibung und, wie sie im Skript heißt. Steht über einer exportierten Variable ein Kommentar mit `##`, wird er zum Tooltip ([Documentation comments](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_documentation_comments.html)). + + **Der Zurücksetzen-Pfeil** (↺) rechts neben einem Namen heißt: Dieser Wert weicht vom Standard ab. Ein Klick setzt ihn zurück. Im Screenshot tragen ihn `Stats` (Standard: leer), `Motion Mode` (Standard: Grounded) und `Layer` (Standard: Ebene 1). `Mask` steht auf Ebene 1, das ist der Standard, also kein Pfeil. Was als Standard gilt, hängt vom Ort ab: In einer eigenen Szene ist es der Standard der Klasse oder des Skripts. Bei einer eingesetzten Szene ist es der Wert aus deren Datei. Das vertieft [Instanzen und Ressourcen](/godot/instanzen-ressourcen). + + > [!PRAXIS] + > Hat ein Agent eine Szene gebaut, wähl im Werkzeugmenü neben „Eigenschaften filtern“ *Nicht-Default expandieren*. Dann sind nur die Abschnitte aufgeklappt, in denen etwas vom Standard abweicht, und du siehst in Sekunden, was er eingestellt hat. Vor dem Commit liest du dieselbe Liste als `git diff` der `.tscn`. + + **Ressourcenfelder** wie `Stats` zeigen ein Würfelsymbol und den Dateinamen. Ein Klick klappt die Ressource auf, und du bearbeitest ihre Werte direkt darunter. Das Kettensymbol davor zeigt Godot seit 4.6 bei Ressourcen, die der Node mit anderen teilt oder aus einer eigenen Datei lädt; der Tooltip nennt sie „extern für die Szene“ ([Godot 4.6](https://godotengine.org/releases/4.6/)). Öffnest du `player_stats.tres` selbst, siehst du, was **Export-Hints** bewirken. In `movement_stats.gd` stehen die Variablen so: + + ```gdscript + @export_range(0, 400, 1, "suffix:px/s") var max_speed := 90.0 + @export_range(0, 5000, 10, "suffix:px/s²") var acceleration := 600.0 + @export_range(0, 5000, 10, "suffix:px/s²") var friction := 900.0 + ``` + + ![Inspektor von player_stats.tres: Abschnitt MovementStats mit Max Speed 90 px/s, Acceleration 600 px/s² und Friction 900 px/s², jeweils mit Schieberegler](/media/godot/editor-anatomie/stats-regler@2x.png "Godot 4.7.2, Inspektor von data/player_stats.tres") + + `export_range` macht aus der Zahl einen Schieberegler mit Grenzen und Schrittweite, `suffix:` hängt die Einheit an ([GDScript exported properties](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html)). Die Werte sind im Testgelände Platzhalter. Welche Werte deine Figur bekommt, entscheidest du; wie du sie abstimmst, zeigt das [Modul zu Eingabeaktionen und Parametern](/godot/aktionen-parameter). + + ## Starten: Hauptszene, offene Szene und das Spiel + + Oben rechts liegen die Start-Knöpfe. Ihre Tastenkürzel und Tooltips in 4.7.2: + + - **F5** – „Die Hauptszene des Projekts abspielen.“ Startet die Szene aus `run/main_scene`. + - **F6** – „Zur Zeit bearbeitete Szene abspielen.“ Startet die Szene im aktiven Tab. + - **Strg + Umschalt + F5** – „Eigene Szene abspielen.“ Startet eine Szene, die du auswählst. + - **F7** – „Ausführung des laufenden Projekts zum Debuggen pausieren.“ Hält das Spiel im Debugger an. + - **F8** – „Laufendes Projekt stoppen.“ + + F6 ist praktisch, um eine Szene allein zu prüfen, aber auch eine Falle: Ist `player.tscn` der aktive Tab, läuft nur die Figur, ohne Boden, Sammelstücke und Zähler. Die Hauptszene änderst du im Dateisystem-Dock per Rechtsklick → *Als Hauptszene setzen* oder in den Projekteinstellungen ([ProjectSettings](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-application-run-main-scene)). + + Seit Godot 4.4 kann der Editor das laufende Spiel einbetten ([Godot 4.4: Embedded game window](https://godotengine.org/releases/4.4/#embedded-game-window)). In 4.7 startet es standardmäßig in einem schwebenden Fenster mit einer Leiste für Pause, Einzelbild und Spieltempo; der Bildschirm **Spiel** holt dieses Fenster nach vorn ([Game embedding](https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html)). Pause und Einzelbild kennst du aus [Godot-Start](/combat/godot-start). Was du am laufenden Spiel änderst, ist nach dem Beenden weg. Die Ausgaben von `print()` erscheinen im unteren Panel unter **Ausgabe**. Fehler des laufenden Spiels sammelt der **Debugger**; sein Reiter zeigt dann eine rote Zahl, etwa „Debugger (3335)“. Wie du sie liest, zeigt das [Modul zur Fehlersuche](/godot/fehler-eingrenzen). + + Vom Terminal aus geht dasselbe ohne Editor: + + ```console + $ godot --path ~/godot/testgelaende + $ godot --path ~/godot/testgelaende --scene res://player.tscn + $ godot -e --path ~/godot/testgelaende + ``` + + Die erste Zeile startet die Hauptszene, die zweite eine bestimmte Szene, die dritte öffnet den Editor ohne Projektmanager ([Command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#command-line-reference)). `godot` steht hier für dein Binary, etwa aus `pacman` oder als Link auf die heruntergeladene Datei. + + ## Was du aus dem Web schon kennst + + - **Szenenbaum ≈ DOM.** Aber Nodes sind Objekte mit Verhalten, kein Markup, und die Reihenfolge im Baum bestimmt auch, wann ihr Code läuft. + - **Szene (`.tscn`) ≈ Komponente.** Aber eine Szene speichert Startwerte, keine Render-Funktion. + - **Instanz ≈ eingesetzte Komponente.** Abweichungen stehen als Overrides in der Eltern-Szene. + - **`@export` ≈ Props.** Der Wert wird im Editor gesetzt und gespeichert, nicht zur Laufzeit vom Elternteil übergeben. + - **Signal ≈ Event mit Callback.** Verbindungen können in der `.tscn` stehen statt im Code. + - **Gruppe ≈ CSS-Klasse für `querySelectorAll`.** Gruppen sind nur Etiketten, sie tragen keine Darstellung ([Groups](https://docs.godotengine.org/en/stable/tutorials/scripting/groups.html)). + - **Autoload ≈ globaler Store.** Er ist ein Node im Baum und kann wie jeder Node `_process` haben. + - **Import und `.godot/imported` ≈ Bundler.** Godot importiert neu, sobald sich die Prüfsumme der Quelldatei ändert, ohne Build-Befehl ([Import process](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#automatic-reimport)). + - **Inspektor ≈ Styles-Panel der DevTools.** Was du im Inspektor änderst, schreibt der Editor aber beim Speichern in die Datei. + - **Remote-Baum ≈ Elements-Panel.** Wie in den DevTools ist eine Änderung dort nach dem Beenden weg. + + Den Remote-Baum, die Laufzeitansicht des Szenenbaums, nutzt du in [Ein fremdes Projekt erkunden](/godot/projekt-erkunden). + + > [!INTERVIEW] + > „Ein Kollege sagt, er habe die Laufgeschwindigkeit angepasst. Wie prüfst du, was genau sich geändert hat?“ Gute Antwort: Ich schaue mir den `git diff` an. Szenen und Ressourcen sind Text, eine geänderte Zeile wie `max_speed = 120.0` in einer `.tres` sagt mir Datei, Eigenschaft und Wert. Im Editor finde ich dieselbe Stelle über den Zurücksetzen-Pfeil im Inspektor. Steht der Wert nirgends, gilt der Standard aus dem Skript, und dann prüfe ich, ob jemand den Standard geändert hat. + + ## Häufige Fehler + + - **Dateien im Dateimanager verschoben.** Godot meldet beim Start etwa `Parse Error: [ext_resource] referenced non-existent resource at: res://player.gd.` Verschieben und Umbenennen gehören ins Dateisystem-Dock, dann passt Godot die Verweise an ([File system, Drawbacks](https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html#drawbacks)). Mit 4.7.2 geprüft: Wandert ein Skript samt `.uid` in einen anderen Ordner, finden Verweise mit `uid=` es nach dem nächsten Scan wieder. Verweise ohne `uid=`, wie sie ein Agent von Hand schreibt, brechen. + - **Groß- und Kleinschreibung.** Linux unterscheidet `Player.gd` und `player.gd`. Nimm für Dateien und Ordner nur Kleinbuchstaben. + - **`.godot/` versioniert oder vermisst.** Der Ordner ist Cache. Er gehört nicht ins Repository, und wenn er fehlt, baut Godot ihn neu. + - **Falsche Szene gestartet.** F6 startet den aktiven Tab, nicht die Hauptszene. Wenn Boden und Zähler fehlen, prüf zuerst den Tab. + - **Spielstand nach `res://` geschrieben.** Im Editor geht das, im exportierten Spiel ist `res://` schreibgeschützt. Spielstände gehören nach `user://`. + - **Wert am laufenden Spiel geändert und erwartet, dass er bleibt.** Änderungen im Spiel oder im Remote-Inspektor verschwinden mit dem Beenden. Übernimm den Wert danach im Inspektor in die Datei. + + ## Zusammenfassung + + - Ein Godot-Projekt ist ein Ordner mit `project.godot`. Szenen (`.tscn`), Ressourcen (`.tres`), Skripte und Einstellungen sind Text und lassen sich mit Git vergleichen. + - `.uid` und `.import` erzeugt Godot neben den Dateien, `.godot/` ist Cache und jederzeit neu erzeugbar. + - In einer `.tscn` oder `.tres` steht nur, was vom Standard abweicht. Fehlt eine Zeile, gilt der Standard. + - `res://` ist der Projektordner und im fertigen Spiel nur lesbar. `user://` liegt unter Linux in `~/.local/share/godot/app_userdata//`. + - Das Szenen-Dock zeigt den Baum, das Dateisystem die Dateien, Inspektor die Werte, Signale und Gruppen die Verbindungen, das untere Panel Ausgaben und Fehler. + - Der Inspektor gliedert nach Klassen der Vererbungskette. Der Zurücksetzen-Pfeil markiert jede Abweichung vom Standard. + - F5 startet die Hauptszene, F6 den aktiven Tab. Änderungen am laufenden Spiel bleiben nicht erhalten. +exercises: + - id: dock-fuer-die-frage + type: choice + title: Welches Dock fragst du? + prompt: | + Welche Zuordnungen von Frage und Dock stimmen in Godot 4.7.2? + options: + - text: "„Welche Methode läuft, wenn `Player` das Signal `collected` sendet?“ → Signale, mit `Player` gewählt" + correct: true + why: "Das Signale-Dock listet die Signale des gewählten Nodes und darunter ihre Verbindungen, im Testgelände `../../HUD :: show_total`." + - text: "„Welche Klasse führt die Eigenschaft `collision_layer` ein?“ → Inspektor, der Abschnitt, in dem sie steht" + correct: true + why: Der Inspektor gliedert nach Klassen. `Layer` steht im Abschnitt CollisionObject2D. + - text: "„Liegt neben `player.gd` eine `player.gd.uid`?“ → Dateisystem-Dock" + correct: false + why: Das Dateisystem-Dock blendet `.uid`- und `.import`-Dateien aus. Das siehst du im Terminal oder im Dateimanager. + - text: "„Welche Fehler gab es beim letzten Lauf?“ → Importieren" + correct: false + why: Das Import-Dock zeigt die Import-Einstellungen eines Assets. Fehler des laufenden Spiels sammelt der Debugger im unteren Panel. + explanation: | + Jedes Dock beantwortet eine Art Frage. Signale zeigt Verbindungen, der Inspektor Werte nach Klassen, das untere Panel Ausgaben und Fehler. Das Dateisystem-Dock ist eine gefilterte Sicht auf den Projektordner: Was Godot selbst neben die Dateien legt, blendet es aus. Wer nur dort schaut, vergisst die `.uid`-Dateien leicht, wenn er Skripte außerhalb von Godot verschiebt. + resources: + - title: Godot Docs · First look at Godot's interface + url: https://docs.godotengine.org/en/stable/getting_started/introduction/first_look_at_the_editor.html + + - id: abschnitte-ordnen + type: order + title: Von oben nach unten im Inspektor + prompt: | + Du wählst in `pickup.tscn` den Wurzel-Node `Pickup`. Er ist ein `Area2D` mit dem Skript `pickup.gd`, das kein `class_name` hat. Bring die Abschnitte in die Reihenfolge, in der der Inspektor sie zeigt, oben zuerst. + lang: text + lines: | + pickup.gd + Area2D + CollisionObject2D + Node2D + CanvasItem + Node + hints: + - Ganz oben steht, was das Skript hinzufügt. + - Danach folgt die Vererbungskette von der speziellsten zur allgemeinsten Klasse. + explanation: | + Das Skript erweitert den Node, deshalb steht sein Abschnitt oben, mangels `class_name` unter dem Dateinamen. Danach kommt die Kette der Engine-Klassen: `Area2D` erbt von `CollisionObject2D`, das von `Node2D`, dann `CanvasItem` und `Node`. So findest du jede Eigenschaft über die Frage, welche Klasse sie einführt. Gesehen mit Godot 4.7.2. + resources: + - title: Godot Docs · Inspector Dock + url: https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html#usage + - title: Godot Docs · Area2D + url: https://docs.godotengine.org/en/stable/classes/class_area2d.html + + - id: abweichungen-erkennen + type: choice + title: Was hat jemand eingestellt? + prompt: | + Der Inspektor zeigt den Wurzel-Node von `pickup.tscn`. Welche Eigenschaften weichen vom Standard ab? + + ![Inspektor des Pickup aus pickup.tscn: Abschnitt pickup.gd mit Value 1, Area2D mit Monitoring an und Monitorable aus, CollisionObject2D mit Layer 3 und Mask 2 markiert](/media/godot/editor-anatomie/pickup-inspektor@2x.png "Godot 4.7.2, Inspektor von pickup.tscn") + options: + - text: Monitorable + correct: true + why: Der Pfeil steht daneben. Standard für ein `Area2D` ist an, hier ist es aus. + - text: Collision → Layer + correct: true + why: Der Pfeil steht daneben. Standard ist Ebene 1, hier ist Ebene 3 gewählt. + - text: Collision → Mask + correct: true + why: Der Pfeil steht daneben. Standard ist Ebene 1, hier ist Ebene 2 gewählt. + - text: Value + correct: false + why: "`1` ist der Standard aus `pickup.gd` (`@export var value := 1`), deshalb kein Pfeil." + - text: Monitoring + correct: false + why: An ist der Standard eines `Area2D`, kein Pfeil. + explanation: | + Der Zurücksetzen-Pfeil erscheint genau dort, wo ein Wert vom Standard abweicht. Für die Engine-Eigenschaften ist das der Standard der Klasse, für exportierte Variablen der Wert im Skript. Diese drei Abweichungen findest du auch als Zeilen in `pickup.tscn`: `collision_layer = 4`, `collision_mask = 2`, `monitorable = false`. Layer 3 hat den Bitwert 4; mehr dazu im [Modul zu Kollisionsebenen](/godot/kollision-ebenen). + resources: + - title: Godot Docs · Inspector Dock + url: https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html#usage + + - id: einstellung-ausgeben + type: output + title: Zwei Einstellungen aus project.godot + prompt: | + Das Testgelände hat diese `project.godot` (Ausschnitt). Was geben die zwei Zeilen aus? + + ```ini + [application] + + config/name="Testgelände" + run/main_scene="res://main.tscn" + + [layer_names] + + 2d_physics/layer_1="world" + 2d_physics/layer_2="player" + 2d_physics/layer_3="pickups" + ``` + lang: gdscript + code: | + print(ProjectSettings.get_setting("application/run/main_scene")) + print(ProjectSettings.get_setting("layer_names/2d_physics/layer_3")) + expected: | + res://main.tscn + pickups + hints: + - Der Schlüssel im Code ist der Abschnitt in eckigen Klammern plus der Name in der Zeile. + explanation: | + Eine Projekteinstellung heißt im Code `abschnitt/name`: `[application]` plus `run/main_scene` ergibt `application/run/main_scene`. `print` gibt Strings ohne Anführungszeichen aus. Dieselben Schlüssel zeigen die Projekteinstellungen, wenn du mit der Maus über eine Einstellung fährst. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · ProjectSettings.get_setting + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-method-get-setting + + - id: standard-aus-dem-skript + type: output + title: Wer bestimmt den Wert? + prompt: | + Jemand hat in `movement_stats.gd` den Standard geändert. Es gibt zwei Ressourcen mit diesem Skript. Was gibt das Skript unten aus? + + ```gdscript + # movement_stats.gd + @export_range(0, 400, 1, "suffix:px/s") var max_speed := 120.0 + ``` + + ```ini + ; data/implicit.tres + [resource] + script = ExtResource("1") + + ; data/explicit.tres + [resource] + script = ExtResource("1") + max_speed = 90.0 + ``` + lang: gdscript + code: | + print(load("res://data/implicit.tres").max_speed) + print(load("res://data/explicit.tres").max_speed) + expected: | + 120.0 + 90.0 + hints: + - Welche der beiden Dateien nennt `max_speed` überhaupt? + explanation: | + `implicit.tres` speichert keinen Wert für `max_speed`, also gilt beim Laden der Standard aus dem Skript, jetzt `120.0`. `explicit.tres` nennt `90.0` ausdrücklich, das überschreibt den Standard. Deshalb ist eine Änderung am Standard im Skript nie harmlos: Sie trifft jede Ressource, die den Wert nicht selbst speichert. Und weil der Editor beim Speichern Zeilen weglässt, die dem Standard entsprechen, fehlt der Wert oft gerade dort, wo du ihn nie angefasst hast. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Resources + url: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html + + - id: welche-szene-laeuft + type: output + title: F6 im falschen Tab + prompt: | + Ein Autoload gibt beim Start den Pfad der laufenden Szene aus. Im Editor ist `player.tscn` der aktive Tab, die Hauptszene ist `main.tscn`. Du drückst **F6**. Was steht in der Ausgabe? + lang: gdscript + code: | + # probe.gd, als Autoload eingetragen + extends Node + + + func _ready() -> void: + await get_tree().process_frame + print(get_tree().current_scene.scene_file_path) + expected: | + res://player.tscn + explanation: | + F6 spielt die Szene im aktiven Tab ab, unabhängig von der Hauptszene. Mit F5 stünde hier `res://main.tscn`. Der Autoload wartet einen Frame, weil Godot Autoloads vor der Szene in den Baum hängt. Mit 4.7.2 nachgestellt über `--scene res://player.tscn`, den Befehlszeilen-Weg zu einer bestimmten Szene. + resources: + - title: Godot Docs · Singletons (Autoload) + url: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html#autoload + - title: Godot Docs · SceneTree.current_scene + url: https://docs.godotengine.org/en/stable/classes/class_scenetree.html#class-scenetree-property-current-scene + + - id: projekt-godot-ergaenzen + type: gap + title: Hauptszene und Layer-Name + prompt: | + Ergänze die Zeilen in `project.godot`, sodass F5 `main.tscn` startet und Kollisionsebene 3 im Inspektor „pickups“ heißt. + lang: ini + code: | + [application] + + config/name="Testgelände" + run/⟦main_scene⟧="res://⟦main.tscn⟧" + + [layer_names] + + 2d_physics/layer_⟦3⟧="pickups" + gaps: + - label: Schlüssel der Hauptszene + hint: Im Code heißt die Einstellung `application/run/…`. + - label: Datei der Hauptszene + hint: Die Szene mit Boden, Objekten und HUD. + - label: Nummer der Ebene + hint: Die Ebenen zählen im Inspektor ab 1. + explanation: | + `run/main_scene` im Abschnitt `[application]` ist die Hauptszene, die F5 startet. Die Namen der Kollisionsebenen stehen unter `[layer_names]` und zählen wie im Inspektor ab 1. Beides schreibt der Editor, wenn du es in den Projekteinstellungen einstellst; lesen kannst du es auch im Texteditor. + resources: + - title: Godot Docs · ProjectSettings · application/run/main_scene + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-application-run-main-scene + - title: Godot Docs · File system · project.godot + url: https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html#project-godot + + - id: editor-aus-dem-terminal + type: command + title: Direkt in den Editor + prompt: | + Öffne das Testgelände im Editor, ohne den Projektmanager. + context: | + Godot liegt bei dir als `godot` im Pfad. Das Projekt liegt in `~/godot/testgelaende`. + answers: + - godot -e --path ~/godot/testgelaende + - godot --editor --path ~/godot/testgelaende + - godot --path ~/godot/testgelaende -e + - godot --path ~/godot/testgelaende --editor + - godot -e --path ~/godot/testgelaende/ + - godot --editor --path ~/godot/testgelaende/ + - godot -e ~/godot/testgelaende/project.godot + - godot --editor ~/godot/testgelaende/project.godot + - godot --path ~/godot/testgelaende/ -e + - godot --path ~/godot/testgelaende/ --editor + - godot ~/godot/testgelaende/project.godot + - godot ~/godot/testgelaende/project.godot -e + explanation: | + `-e` oder `--editor` startet den Editor statt des Spiels, `--path` nennt den Ordner mit `project.godot`. Ohne `-e` liefe die Hauptszene. Gibst du statt `--path` den Pfad zur `project.godot` an, öffnet Godot den Editor auch ohne `-e`; mit 4.7.2 geprüft. Ein Alias in deiner Shell spart dir das Tippen. + resources: + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#command-line-reference + + - id: cache-loeschen + type: command + title: Den Cache neu aufbauen + prompt: | + Du willst ausschließen, dass ein Fehler am Cache liegt. Lösche den Ordner, den Godot beim nächsten Öffnen neu erzeugt. + context: | + Der Editor ist geschlossen. Du bist im Projektordner, neben `project.godot`. + answers: + - rm -rf .godot + - rm -rf .godot/ + - rm -rf ./.godot + - rm -rf ./.godot/ + - rm -r .godot + - rm -r .godot/ + explanation: | + `.godot/` enthält importierte Assets, den UID-Cache und den Zustand des Editors. Nichts davon ist Quelle: Beim nächsten Öffnen erzeugt Godot den Ordner neu und importiert alle Assets erneut, das dauert bei großen Projekten eine Weile. Schließ vorher den Editor, sonst schreibt er sofort wieder hinein. `-f` unterdrückt Rückfragen bei schreibgeschützten Dateien. Mit 4.7.2 geprüft. + resources: + - title: Godot Docs · Import process + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html + + - id: hauptszene-falsch + type: bug + title: F5 zeigt nur die Figur + prompt: | + Drückst du F5, siehst du das hier: eine blaue Figur auf grauem Grund, kein Boden, keine Sammelstücke, kein Zähler. Öffnest du `main.tscn` und drückst F6, ist alles da. Welche Zeile in `project.godot` ist schuld? Korrigiere sie. + + ![Spielbild 320 × 180: grauer Hintergrund, in der Mitte nur das blaue Rechteck der Figur](/media/godot/editor-anatomie/falsche-hauptszene@2x.png "Godot 4.7.2, Spielbild nach F5, doppelt vergrößert") + lang: ini + code: | + [application] + + config/name="Testgelände" + run/main_scene="res://player.tscn" + config/features=PackedStringArray("4.7", "GL Compatibility") + lines: [4] + fix: + - line: 4 + answers: + - run/main_scene="res://main.tscn" + explanation: | + F5 startet die Szene aus `run/main_scene`. Steht dort `player.tscn`, läuft nur die Figur mit ihrer Kamera, und der graue Grund ist die Standard-Hintergrundfarbe. F6 startet dagegen den aktiven Tab, deshalb fiel es dort nicht auf. Im Dateisystem-Dock hättest du es auch gesehen: Die Hauptszene steht in Blau. Mit 4.7.2 nachgestellt. + resources: + - title: Godot Docs · ProjectSettings · application/run/main_scene + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-application-run-main-scene + + - id: pfad-gross-geschrieben + type: bug + title: Die Figur lädt nicht + prompt: | + Ein Coding-Agent hat `player.tscn` von Hand geschrieben. Startest du das Spiel im Terminal, erscheint diese Ausgabe (im Editor stehen dieselben Meldungen im Debugger), und die Figur bewegt sich nicht. Das Skript liegt als `player.gd` im Projektordner. Finde die Zeile und korrigiere sie. + + ```text + ERROR: Attempt to open script 'res://Player.gd' resulted in error 'File not found'. + ERROR: Failed loading resource: res://Player.gd. + ERROR: res://player.tscn:13 - Parse Error: [ext_resource] referenced non-existent resource at: res://Player.gd. + ``` + lang: ini + code: | + [gd_scene format=3] + + [ext_resource type="Script" path="res://Player.gd" id="1"] + [ext_resource type="Resource" path="res://data/player_stats.tres" id="2"] + + [sub_resource type="RectangleShape2D" id="feet"] + size = Vector2(10, 6) + lines: [3] + fix: + - line: 3 + answers: + - '[ext_resource type="Script" path="res://player.gd" id="1"]' + explanation: | + Unter Linux sind `Player.gd` und `player.gd` zwei verschiedene Dateien. Unter Windows und macOS fiele der Fehler nicht auf; laut Doku kann er im exportierten Spiel wieder auftreten. Deshalb empfiehlt sie Kleinbuchstaben für alle Dateinamen. Mit 4.7.2 geprüft: Hätte die Zeile ein `uid="uid://…"`, fände Godot das Skript über die UID trotzdem, ohne Fehlermeldung. Von Hand geschriebene Szenen haben meist keine UIDs; der Editor ergänzt sie beim ersten Speichern. + resources: + - title: Godot Docs · File system · Drawbacks + url: https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html#drawbacks + + - id: projektordner-erklaeren + type: explain + title: Was davon ist Quelle? + prompt: | + Eine Kollegin, die aus der Webentwicklung kommt, sieht dieses Verzeichnis zum ersten Mal und fragt, welche Dateien sie lesen muss, um eine Änderung zu verstehen, und welche Godot selbst erzeugt. Erkläre es in drei bis fünf Sätzen. + lang: text + code: | + $ ls -AF + crate.tscn hud.gd.uid pickup.gd player.gd.uid + data/ main.tscn pickup.gd.uid player.tscn + .godot/ movement_stats.gd pickup.tscn project.godot + hud.gd movement_stats.gd.uid player.gd + $ ls -AF .godot + editor/ scene_groups_cache.cfg + .gdignore shader_cache/ + global_script_class_cache.cfg uid_cache.bin + imported/ + points: + - "`project.godot`, die `.tscn`, die `.tres` in `data/` und die `.gd` sind Textdateien mit dem Inhalt des Projekts; eine Änderung zeigt `git diff`." + - "Die `.gd.uid` erzeugt Godot für jedes Skript; sie gehört zum Skript und wandert mit, wenn man es verschiebt." + - "`.godot/` ist Cache aus Importen und Editor-Zustand, Godot baut ihn neu, er gehört nicht ins Repository." + explanation: | + Die Quellen sind `project.godot` mit den Einstellungen, die Szenen (`.tscn`), die Ressourcen (`.tres`) und die Skripte (`.gd`), alles Text, also liest du eine Änderung am besten als `git diff`. Neben jedem Skript legt Godot seit 4.4 eine `.uid` an, die seine ID trägt; du musst sie nicht lesen, aber sie wird mitversioniert und beim Verschieben mitgenommen. `.godot/` ist ein Cache wie ein Build-Ordner, Godot erzeugt ihn beim Öffnen neu. + resources: + - title: Godot Docs · File system + url: https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html + - title: Godot 4.4 · Universal UID support + url: https://godotengine.org/releases/4.4/#universal-uid-support + + - id: werkstatt-notiz + type: practice + title: Deine Fragen an ein fremdes Projekt + prompt: | + Schreib eine kurze Notiz für deine nächste Sitzung mit einem Coding-Agenten. Nimm drei Fragen, die du an ein Projekt hast, das du nicht selbst gebaut hast. Notiere zu jeder, welches Dock oder welche Datei du zuerst öffnest und woran du die Antwort erkennst. + lang: prose + checklist: + - Drei Fragen stehen da, jede in einem Satz. + - Zu jeder Frage steht ein Dock oder eine Datei mit Pfad. + - Bei mindestens einer Frage ist der Beleg ein Zurücksetzen-Pfeil im Inspektor oder eine Zeile in einer `.tscn` oder `.tres`. + - Bei mindestens einer Frage steht, ob die Antwort im Editor oder im laufenden Spiel liegt. + hints: + - "Typische Fragen: Wo steht ein Wert? Wer reagiert auf ein Ereignis? Welche Szene startet? Was hat sich seit gestern geändert?" + explanation: | + Die Notiz ist deine erste Projektkarte im Kleinen. Wer zu jeder Frage weiß, wo die Antwort steht, verliert in einem fremden Projekt keine Zeit mit Suchen und kann einem Agenten präzise sagen, wo er etwas ändern soll. Die große Fassung schreibst du in [Ein fremdes Projekt erkunden](/godot/projekt-erkunden). + resources: + - title: Godot Docs · First look at Godot's interface + url: https://docs.godotengine.org/en/stable/getting_started/introduction/first_look_at_the_editor.html diff --git a/content/godot/modules/export-sichern.yaml b/content/godot/modules/export-sichern.yaml new file mode 100644 index 0000000..ab99ab3 --- /dev/null +++ b/content/godot/modules/export-sichern.yaml @@ -0,0 +1,546 @@ +id: export-sichern +title: Testversionen exportieren und die Arbeit sichern +level: 2 +minutes: 130 +summary: Aus dem Testgelände eine Version machen, die ohne Editor startet – mit Exportvorlagen, Presets für Linux und Windows, Debug- und Release-Builds und Log-Datei – und das Projekt so in Git sichern, dass ein frischer Klon ohne Nacharbeit läuft. +place: pc +goals: + - Du installierst Exportvorlagen passend zur Editor-Version und legst Presets für Linux und Windows an. + - Du exportierst Debug- und Release-Versionen im Editor und mit `--export-release` und `--export-debug` aus dem Terminal und liest die Meldungen, wenn es scheitert. + - Du startest eine exportierte Version mit `--verbose`, findest ihr Log unter `user://logs/` und erklärst, warum ein Release-Build keinen Backtrace zeigt. + - Du entscheidest begründet, welche Dateien eines Godot-Projekts ins Repository gehören, und nutzt `.gitignore`, `.gdignore` und bei Bedarf Git LFS. + - Du sicherst Abstimmungsrunden mit Commits und markierst Testversionen mit Tags. +resources: + - title: Godot Docs · Exporting projects + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html + - title: Godot Docs · Command line tutorial · Exporting + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#exporting + - title: Godot Docs · Version control systems + url: https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html + - title: Godot Docs · Logging + url: https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html + - title: Godot Blog · UID changes coming to Godot 4.4 + url: https://godotengine.org/article/uid-changes-coming-to-godot-4-4/ +lesson: | + Du willst das Testgelände auf dem Laptop ausprobieren, auf dem kein Godot installiert ist, oder einem Freund eine Testversion schicken. Und du willst, dass das Projekt nach einem Plattencrash aus Git wieder genau so entsteht, wie es war, mit allen Import-Einstellungen und ohne den Cache. Beides gehört zusammen: Eine Testversion ist nur so viel wert wie der Stand, aus dem sie gebaut wurde, und den hält Git fest. + + Beides baust du hier am Testgelände: zuerst die Exportvorlagen und Presets, dann Debug- und Release-Builds aus Editor und Terminal, dann die Regeln fürs Repository. Alle Meldungen stammen aus Läufen mit Godot 4.7.2 unter Linux; der Editor war deutsch eingestellt, deshalb sind manche Meldungen deutsch. + + ## Exportvorlagen: Godot ohne Editor + + Ein exportiertes Spiel besteht aus zwei Teilen: einer **Exportvorlage**, also einem fertig gebauten Godot ohne Editor, und den Daten deines Projekts in einer **PCK**-Datei. Für jede Plattform gibt es eine Debug- und eine Release-Vorlage; nur die Debug-Vorlage schreibt zum Beispiel bei Skriptfehlern einen Backtrace ins Log ([Exporting projects](https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html#export-templates)). Die Vorlagen lädst du einmal pro Godot-Version. Unter *Editor → Exportvorlagen verwalten …* zeigt der Dialog die Version des Editors, verfügbare und installierte Vorlagen: + + ![Exportvorlagen-Manager: Godot-Version 4.7.2.stable, Verfügbare Vorlagen als Baum mit Desktop, Windows und Linux, Installierte Vorlagen Windows x86_64 und Linux x86_64, unten der Hinweis Offline Modus und der Link Online gehen](/media/godot/export-sichern/exportvorlagen@2x.png "Godot 4.7.2, Exportvorlagen-Manager mit Vorlagen für Linux und Windows") + + Du kannst einzelne Plattformen anhaken und installieren oder über den Knopf oben rechts eine TPZ-Datei mit allen Vorlagen einspielen. Die TPZ für 4.7.2 ist laut GitHub-Release 1 281 349 702 Byte groß, rund 1,3 GB ([Release 4.7.2](https://github.com/godotengine/godot/releases/tag/4.7.2-stable)); für Linux und Windows reichen die einzelnen Vorlagen. Steht unten „Offline Modus“, darf der Editor nichts herunterladen, bis du auf „Online gehen“ klickst. + + Die Vorlagen müssen exakt zur Version passen. Godot sucht sie im Ordner `export_templates/4.7.2.stable/` seiner Editordaten, unter Linux standardmäßig in `~/.local/share/godot/` ([Editor data paths](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#editor-data-paths)). Vorlagen für 4.7.1 helfen einem 4.7.2-Editor nicht. + + ## Presets für Linux und Windows + + Unter *Projekt → Exportieren ...* legst du mit *Hinzufügen …* je Zielplattform ein **Export-Preset** an. Ohne passende Vorlage zeigt der Dialog, wo er gesucht hat: + + ![Export-Dialog mit Presets Windows Desktop und Linux; Linux ist gewählt, Export Path leer, PCK einbetten aus; unten rot: Keine Exportvorlage unter dem erwarteten Pfad gefunden, mit den Pfaden zu linux_debug.x86_64 und linux_release.x86_64](/media/godot/export-sichern/export-vorlage-fehlt@2x.png "Godot 4.7.2, Linux-Preset ohne installierte Vorlagen; die Pfade stammen aus dem Testaufbau mit eigenem Datenordner") + + Mit installierten Vorlagen verschwinden die roten Zeilen. Die wichtigen Felder für eine Testversion: + + - **Export Path**: wohin die Datei geht, etwa `build/linux/testgelaende.x86_64`. Relative Pfade gelten ab dem Projektordner. + - **Ausführbar**: Das Preset steht für die Ein-Klick-Auslieferung auf ein Gerät bereit; je Plattform darf nur eins so markiert sein ([One-click deploy](https://docs.godotengine.org/en/stable/tutorials/export/one-click_deploy.html)). + - **Konsolenwrapper exportieren**: Standard `Debug Only`. Ein Debug-Export bekommt dann zusätzlich einen Starter mit Konsole: für Linux das Skript `testgelaende.sh`, für Windows `testgelaende.console.exe`. + - **PCK einbetten**: aus heißt zwei Dateien, Programm und `.pck`; an heißt eine einzige Datei. + + ![Export-Dialog, Linux-Preset mit Export Path build/linux/testgelaende.x86_64, Konsolenwrapper Debug Only, PCK einbetten aus, Architektur x86_64, Texturformat S3TC BPTC an; keine Fehlermeldung](/media/godot/export-sichern/export-preset-linux@2x.png "Godot 4.7.2, Linux-Preset mit installierten Vorlagen") + + Der Editor speichert die Presets als Text in `export_presets.cfg` im Projektordner. Ein Ausschnitt, so wie Godot 4.7.2 ihn geschrieben hat: + + ```ini title=export_presets.cfg + [preset.1] + + name="Linux" + platform="Linux" + export_filter="all_resources" + export_path="build/linux/testgelaende.x86_64" + + [preset.1.options] + + debug/export_console_wrapper=1 + binary_format/embed_pck=false + binary_format/architecture="x86_64" + ``` + + Diese Datei gehört ins Repository; die Doku sagt, darin stehe nichts, was man geheim halten müsste. Vertrauliches wie ein Schlüssel zum Verschlüsseln der PCK oder das Passwort für eine Windows-Signatur legt Godot getrennt in `.godot/export_credentials.cfg` ab ([Configuration files](https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html#configuration-files)). Im Test stand dort je Preset `script_encryption_key=""`, beim Windows-Preset zusätzlich `codesign/password=""`. Unter *Ressourcen* stellst du ein, was in die PCK kommt; Standard ist „all resources“. Dateien, die keine Ressourcen sind, etwa `.txt` oder `.json`, brauchen dort einen eigenen Filter, und Dateien, deren Name mit einem Punkt beginnt, exportiert Godot nie. + + ## Debug oder Release, und das Log danach + + *Projekt exportieren …* öffnet einen Speicherdialog mit dem Schalter *Exportiere mit Debuginformationen*. Für Testversionen ist der Unterschied wichtig. Mit einem eingebauten Fehler im Testgelände (`$Count` statt `$Counter` in `hud.gd`) meldete der Release-Export: + + ```text + ERROR: Node not found: "Count" (relative to "/root/Main/HUD"). + at: get_node (scene/main/node.cpp:1975) + ``` + + Der Debug-Export derselben Version: + + ```text + ERROR: Node not found: "Count" (relative to "/root/Main/HUD"). + at: get_node (scene/main/node.cpp:1975) + GDScript backtrace (most recent call first): + [0] @implicit_ready (res://hud.gd:5) + ``` + + Nur der Debug-Build nennt die Zeile in deinem Code. In Release-Builds ist der Backtrace aus Leistungsgründen abgeschaltet, außer du schaltest die Projekteinstellung *Always Track Call Stacks* (`debug/settings/gdscript/always_track_call_stacks`, Standard aus) ein ([Script backtraces](https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html#script-backtraces)). Für Testversionen, die andere spielen, ist Debug deshalb oft die bessere Wahl: Wenn etwas schiefgeht, steht im Log, wo. + + Das Log schreibt ein Desktop-Export ohne weitere Einstellung nach `user://logs/godot.log` ([Built-in file logging](https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html#built-in-file-logging)). Unter Linux ist `user://` für das Testgelände `~/.local/share/godot/app_userdata/Testgelände/` ([user://](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#accessing-persistent-user-data-user)). Jeder Start legt ein neues `godot.log` an, das alte heißt danach etwa `godot2026-10-22T07.12.21.log`; fünf Dateien bleiben liegen. Mehr Einzelheiten liefert `--verbose`. Gekürzt aus einem Lauf des Release-Exports, für den Test ohne Fenster und nach 60 Durchläufen beendet: + + ```console + $ ./build/linux/testgelaende.x86_64 --verbose --headless --quit-after 60 + WorkerThreadPool: 24 threads, 7 max low-priority. + Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org + … + Loading resource: res://player.gdc remapped: res://player.gdc + Completed load for: 'res://player.gd' remapped 'res://player.gdc' at thread -1 + … + ``` + + Die Endung `.gdc` zeigt: Im Export liegen die Skripte nicht als Text, sondern vorverarbeitet. Fehlt neben dem Programm die `.pck`, startet es nicht (der Pfad stammt aus dem Test): + + ```text + ERROR: Error: Couldn't load project data at path "/tmp/lrn23/d/pcktest". Is the .pck file missing? + ``` + + Das passiert, wenn du nur die `.x86_64`-Datei weitergibst. Mit *PCK einbetten* entsteht eine einzige Datei; im Test war sie 73 534 928 Byte groß, statt 73 519 416 Byte Programm plus 15 508 Byte PCK. + + > [!PRAXIS] + > Schick Testversionen als Debug-Export mit eingebetteter PCK und bitte darum, bei Problemen die Datei `godot.log` mitzuschicken, nicht einen Screenshot der Fehlermeldung. Notiere zu jeder Testversion den Tag im Repository. Dann weißt du beim Lesen des Logs, welcher Stand lief. + + ## Exportieren aus dem Terminal + + Was der Dialog kann, geht auch ohne Editor-Fenster, mit demselben Preset ([Exporting](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#exporting)). `godot` steht für dein Godot-Binary; Fortschrittszeilen sind gekürzt: + + ```console + $ godot --headless --path . --export-release "Linux" build/linux/testgelaende.x86_64 + Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org + … + ERROR: Vorlage vorbereiten: Der angegebene Export-Pfad existiert nicht. + at: add_message (./editor/export/editor_export_platform.h:270) + ERROR: Project export for preset "Linux" failed. + at: _fs_changed (editor/editor_node.cpp:1417) + $ mkdir -p build/linux + $ godot --headless --path . --export-release "Linux" build/linux/testgelaende.x86_64 + Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org + … + $ ls build/linux + testgelaende.pck testgelaende.x86_64 + ``` + + Der Zielordner muss existieren. Ein relativer Pfad gilt ab dem Ordner mit `project.godot`, nicht ab dem Ordner, in dem du gerade stehst; das sagt die Doku, und der Test bestätigt es. `--export-debug` baut den Debug-Build, `--export-pack` nur die PCK. Der Preset-Name muss genau stimmen, auch in Groß- und Kleinschreibung: + + ```console + $ godot --headless --path . --export-release "linux" build/linux/testgelaende.x86_64 + Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org + ERROR: Invalid export preset name: linux. + The following presets were detected in this project's `export_presets.cfg`: + "Windows Desktop" + "Linux" + at: _fs_changed (editor/editor_node.cpp:1417) + ``` + + Ein Export aus dem Terminal importiert vorher alles, was nötig ist. Im Test lief er auch in einem frischen Klon ohne `.godot/`, und die exportierte Datei startete ohne Fehler. Das macht ihn zur Grundlage für automatische Builds, wie sie [CI/CD rund um Git](/git/ci-cd) beschreibt. + + ## Was ins Repository gehört + + Ein Godot-Projekt ist fast nur Text, und fast alles davon gehört ins Repository: + + | Datei oder Ordner | Ins Repository? | Warum | + | --- | --- | --- | + | `project.godot`, `.tscn`, `.tres`, `.gd` | ja | das Projekt selbst | + | `.import` neben Bildern und Tönen | ja | Import-Einstellungen; ohne sie importiert ein Klon mit Standardwerten | + | `.uid` neben Skripten | ja | stabile Verweise, auch wenn Dateien wandern | + | `export_presets.cfg` | ja | Presets ohne Geheimnisse | + | `.godot/` | nein | Cache, Importergebnisse, `export_credentials.cfg` | + | `build/` | nein | Exporte entstehen aus dem Stand neu | + + Die `.gitignore` erzeugt Godot selbst: *Projekt → Versionsverwaltung → Metadaten der Versionsverwaltung erstellen/überschreiben...*, dann Git wählen ([Files to exclude from VCS](https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html#files-to-exclude-from-vcs)). + + ![Dialog Versionsverwaltungsmetadaten erstellen: Dateien mit VCS Metadaten erstellen für Git; Hinweis, dass bestehende Dateien überschrieben werden](/media/godot/export-sichern/vcs-metadaten@2x.png "Godot 4.7.2, Projekt → Versionsverwaltung") + + Godot 4.7.2 schrieb damit diese beiden Dateien. Die letzte Zeile der `.gitignore` für den Export-Ordner hast du selbst ergänzt: + + ```text title=.gitignore + # Godot 4+ specific ignores + .godot/ + /android/ + /build/ + ``` + + ```text title=.gitattributes + # Normalize EOL for all files that Git considers text files. + * text=auto eol=lf + ``` + + Die `.uid`-Dateien gibt es seit Godot 4.4. Das Godot-Team schreibt ausdrücklich, dass sie committet werden müssen; sonst brechen auf einem anderen Rechner die UID-Verweise, und Godot fällt mit einer Warnung auf den Pfad zurück ([What happens if I don't commit .uid files](https://godotengine.org/article/uid-changes-coming-to-godot-4-4/#what-happens-if-i-dont-commit-uid-files-to-version-control)). Muster und Regeln der `.gitignore` erklärt [Gute Commits und Commit-Hygiene](/git/commit-hygiene). + + Ein frischer Klon hat kein `.godot/`. Öffnest du ihn einmal im Editor oder rufst `godot --headless --path . --import` auf, baut Godot den Cache neu. Startest du das Spiel direkt, fehlt die Liste der Klassennamen, und es scheitert: + + ```text + SCRIPT ERROR: Parse Error: Could not find type "MovementStats" in the current scope. + at: GDScript::reload (res://player.gd:8) + ``` + + ### Originale, große Dateien und `.gdignore` + + Deine Arbeitsdateien aus Aseprite, Krita oder REAPER (`.aseprite`, `.kra`, `.rpp` samt Aufnahmen) importiert Godot nicht, aber exportierte PNG- und WAV-Dateien in denselben Ordnern schon. Zwei Wege halten sie heraus: Die Originale liegen in einem Ordner neben dem Projekt, oder ein Ordner im Projekt bekommt eine leere Datei `.gdignore`. Dann importiert Godot darin nichts, `load()` findet die Dateien nicht, und das Dateisystem-Dock blendet den Ordner aus ([Ignoring specific folders](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html#ignoring-specific-folders)). Im Test bekam eine PNG im ignorierten Ordner keine `.import`-Datei, und `ResourceLoader.exists()` meldete `false`. + + Große Binärdateien belasten Git: Jede Version einer Datei ist ein eigener Blob in der History ([Das Datenmodell](/git/datenmodell)). Für Aufnahmen und große Arbeitsdateien gibt es **Git LFS**. Es ersetzt die Dateien im Repository durch kleine Verweise und lagert den Inhalt auf dem Server aus. Nach `git lfs install` legst du je Dateityp mit `git lfs track "*.wav"` eine Regel in `.gitattributes` an und committest diese Datei ([Git LFS](https://git-lfs.com/)). Die Doku rät, LFS einzurichten, bevor die ersten großen Dateien committet sind ([Git LFS mit Godot](https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html#git-lfs)). Welche Dateitypen du auslagerst und wo deine Originale liegen, entscheidest du nach deinen Werkzeugen und Dateigrößen. + + ## Commits und Tags für Testversionen + + Für Abstimmungsrunden aus [Tuning als Experiment](/combat/tuning) heißt das: ein Commit vor der Runde mit Variante A, einer danach mit B, jeweils mit der geänderten Größe in der Nachricht. Lies vor jedem Commit den Diff der `.import`-Dateien und von `project.godot`; dort fallen Einstellungen auf, die ein Coding-Agent nebenbei geändert hat. + + Eine Testversion markierst du mit einem annotierten Tag, etwa `git tag -a testversion-2026-10-22 -m "Testversion für die Messrunde"`. Den Namen wählst du; wichtig ist, dass du ihn in deinem Protokoll und beim Weitergeben nennst. Tags gehen mit `git push origin ` auf den Server; mehr dazu in [CI/CD rund um Git](/git/ci-cd). + + > [!INTERVIEW] + > „Ein Kollege klont euer Godot-Projekt, und die Sprites sehen bei ihm anders aus. Was prüfst du?“ Gute Antwort: Ob die `.import`-Dateien im Repository sind, denn ohne sie importiert sein Godot mit Standardwerten. Dann, ob seine Godot-Version gleich ist. Ins Repository gehören Szenen, Ressourcen, Skripte, `.import`- und `.uid`-Dateien und `export_presets.cfg`; `.godot/` und die Exporte nicht. Zugangsdaten für den Export liegen in `.godot/export_credentials.cfg` und bleiben damit lokal. + + ## Häufige Fehler + + - **Vorlagen einer anderen Version.** Godot sucht in `export_templates/4.7.2.stable/`; der Dialog nennt den Pfad. + - **Zielordner fehlt.** `Der angegebene Export-Pfad existiert nicht.` Vorher `mkdir -p build/linux`. + - **Preset-Name falsch geschrieben.** Groß- und Kleinschreibung zählen; die Meldung listet die vorhandenen Namen. + - **Nur die Programmdatei weitergegeben.** Ohne `.pck` startet sie nicht; PCK einbetten oder beide Dateien schicken. + - **Release-Build zum Fehlersuchen.** Kein Backtrace im Log; für Tests den Debug-Build nehmen. + - **`.godot/` committet oder `.import`/`.uid` ignoriert.** Das eine bläht das Repository und erzeugt Konflikte, das andere bricht Import-Einstellungen und Verweise beim nächsten Klon. + - **Klon direkt gestartet.** Ohne Editor-Start oder `--import` fehlen die Klassennamen. + - **Große Aufnahmen ohne LFS.** Jede Version bleibt für immer in der History. + + ## Zusammenfassung + + - Ein Export ist eine Vorlage plus PCK. Die Vorlagen müssen zur Editor-Version passen und liegen in `export_templates/4.7.2.stable/`. + - Presets stehen in `export_presets.cfg` und gehören ins Repository; Geheimnisse liegen in `.godot/export_credentials.cfg`. + - Debug-Builds schreiben einen Backtrace ins Log, Release-Builds nicht. Das Log liegt unter `user://logs/godot.log`. + - `--export-release` und `--export-debug` brauchen einen vorhandenen Zielordner und den exakten Preset-Namen; relative Pfade gelten ab dem Projektordner. + - Ins Repository: Projektdateien, `.import`, `.uid`, `export_presets.cfg`. Nicht: `.godot/`, Exporte. + - Originale liegen neben dem Projekt oder hinter `.gdignore`; große Binärdateien kommen bei Bedarf in Git LFS. + - Commits rahmen jede Abstimmungsrunde ein, Tags markieren Testversionen. +exercises: + - id: repo-oder-nicht + type: choice + title: Was gehört ins Repository? + prompt: | + Welche Dateien des Testgeländes gehören ins Git-Repository? + options: + - text: "`export_presets.cfg`" + correct: true + why: Die Presets sind Text ohne Geheimnisse; mit ihnen baut jeder Klon dieselbe Testversion. + - text: "`player.gd.uid`" + correct: true + why: Die UID-Dateien halten Verweise stabil. Das Godot-Team sagt ausdrücklich, dass sie committet werden müssen. + - text: "`.godot/export_credentials.cfg`" + correct: false + why: Darin stehen Schlüssel und Passwörter für den Export. Die Datei liegt in `.godot/` und bleibt lokal. + - text: "`build/linux/testgelaende.x86_64`" + correct: false + why: Exporte entstehen aus dem Stand neu. Die Testversion markierst du mit einem Tag, nicht mit der Datei im Repository. + explanation: | + Ins Repository gehört, was sich nicht verlustfrei neu erzeugen lässt: Szenen, Ressourcen, Skripte, Import-Einstellungen, UID-Dateien, Presets. Was Godot aus diesen Dateien erzeugt, also den Cache in `.godot/` und die Exporte, bleibt draußen. Die Zugangsdaten liegen absichtlich in `.godot/`, damit sie mit dem Cache ignoriert werden. + resources: + - title: Godot Docs · Exporting projects · Configuration files + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html#configuration-files + - title: Godot Docs · Files to exclude from VCS + url: https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html#files-to-exclude-from-vcs + + - id: export-ablauf + type: order + title: Von der Vorlage zur gestarteten Testversion + prompt: | + Bring die Schritte in eine Reihenfolge, mit der der erste Linux-Export klappt. + lang: prose + lines: | + Unter Editor → Exportvorlagen verwalten die Vorlagen für 4.7.2 installieren. + Unter Projekt → Exportieren ein Preset Linux anlegen. + Den Ordner build/linux anlegen. + Mit --export-debug in diesen Ordner exportieren. + Die exportierte Datei mit --verbose starten und das Log lesen. + alternatives: + - [1, 3, 2, 4, 5] + - [3, 1, 2, 4, 5] + - [2, 1, 3, 4, 5] + - [2, 3, 1, 4, 5] + - [3, 2, 1, 4, 5] + explanation: | + Vorlagen, Preset und Ordner sind voneinander unabhängig, alle drei müssen aber vor dem Export stehen. Ohne Vorlage meldet der Dialog den fehlenden Pfad, ohne Preset kennt `--export-debug` den Namen nicht, ohne Ordner scheitert der Export mit „Der angegebene Export-Pfad existiert nicht.“ Der Start mit `--verbose` zeigt danach, was die Testversion lädt. + resources: + - title: Godot Docs · Exporting projects + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html + + - id: debug-export-dateien + type: output + title: Was liegt im Ordner? + prompt: | + Das Linux-Preset steht auf den Standardwerten: Konsolenwrapper `Debug Only`, PCK einbetten aus. Du exportierst mit `--export-debug` nach `build/linux-debug/testgelaende.x86_64`. Was zeigt danach `ls build/linux-debug`, eine Datei pro Zeile? + lang: text + code: | + $ godot --headless --path . --export-debug "Linux" build/linux-debug/testgelaende.x86_64 + $ ls build/linux-debug + expected: | + testgelaende.pck + testgelaende.sh + testgelaende.x86_64 + accept: + - testgelaende.pck testgelaende.sh testgelaende.x86_64 + - testgelaende.pck testgelaende.sh testgelaende.x86_64 + explanation: | + Ohne eingebettete PCK entstehen Programm und `.pck` getrennt. Der Konsolenwrapper `testgelaende.sh` kommt nur beim Debug-Export dazu, weil die Einstellung auf `Debug Only` steht; er startet das Spiel aus einem Terminal. Gemessen mit Godot 4.7.2: Der Release-Export desselben Presets enthielt nur `testgelaende.pck` und `testgelaende.x86_64`. + resources: + - title: Godot Docs · Exporting for Linux + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_linux.html + + - id: release-meldung + type: choice + title: Was der Release-Build verrät + prompt: | + In `hud.gd` steht `@onready var counter: Label = $Count`, das Label heißt aber `Counter`. Du startest den **Release**-Export im Terminal. Was steht danach in der Ausgabe? + options: + - text: 'Die Meldung `Node not found: "Count" …` mit der Stelle im C++-Quelltext von Godot, aber ohne GDScript-Backtrace.' + correct: true + why: Fehler meldet auch der Release-Build. Den Backtrace mit `hud.gd:5` schreibt im Standard nur der Debug-Build. + - text: 'Die Meldung mit GDScript-Backtrace bis `[0] @implicit_ready (res://hud.gd:5)`.' + correct: false + why: So sieht die Ausgabe des Debug-Exports aus. Im Release-Build ist das Mitschreiben der Aufrufkette aus Leistungsgründen aus. + - text: Nichts, weil ein Release-Build keine Fehler ausgibt. + correct: false + why: Fehler und Warnungen gehen auch im Release-Build auf die Fehlerausgabe und ins Log; nur der Backtrace fehlt. + - text: Eine Fehlermeldung beim Export, weil Godot den falschen Pfad schon beim Exportieren erkennt. + correct: false + why: Der Export prüft keine Node-Pfade in Skripten. Der Fehler tritt erst beim Start auf, wenn `@onready` den Pfad auflöst. + explanation: | + Gemessen mit Godot 4.7.2: Der Release-Export schrieb `ERROR: Node not found: "Count" (relative to "/root/Main/HUD").` und die Zeile `at: get_node (scene/main/node.cpp:1975)`. Der Debug-Export derselben Version schrieb zusätzlich `GDScript backtrace (most recent call first):` und `[0] @implicit_ready (res://hud.gd:5)`. Die Doku nennt den Grund: In Release-Builds ist der Backtrace abgeschaltet, außer man schaltet `debug/settings/gdscript/always_track_call_stacks` ein. Für Testversionen, bei denen du Fehler suchst, ist der Debug-Export deshalb die bessere Wahl. + resources: + - title: Godot Docs · Logging · Script backtraces + url: https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html#script-backtraces + + - id: relativer-pfad + type: output + title: Wo landet der Export? + prompt: | + Du stehst in deinem Home-Ordner und rufst auf: + + `godot --headless --path ~/projekte/testgelaende --export-release "Linux" build/linux/testgelaende.x86_64` + + Der Ordner existiert an der richtigen Stelle. Unter welchem Pfad liegt danach die exportierte Datei? + lang: text + code: | + ~ $ godot --headless --path ~/projekte/testgelaende --export-release "Linux" build/linux/testgelaende.x86_64 + expected: | + ~/projekte/testgelaende/build/linux/testgelaende.x86_64 + accept: + - $HOME/projekte/testgelaende/build/linux/testgelaende.x86_64 + - projekte/testgelaende/build/linux/testgelaende.x86_64 + explanation: | + Relative Exportpfade gelten ab dem Ordner mit `project.godot`, nicht ab dem aktuellen Ordner. Die Doku warnt ausdrücklich davor, und im Test mit Godot 4.7.2 landete die Datei bei einem Aufruf aus `/tmp` im Projektordner, `/tmp/build` entstand nicht. Wer in CI-Skripten mit absoluten Pfaden arbeitet, umgeht die Verwechslung. + resources: + - title: Godot Docs · Command line tutorial · Exporting + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#exporting + + - id: release-exportieren + type: command + title: Release-Export aus dem Terminal + prompt: | + Exportiere die Release-Version des Presets `Linux` nach `build/linux/testgelaende.x86_64`, ohne dass ein Fenster aufgeht. Der Ordner existiert. + context: | + Du bist im Projektordner. `godot` steht für dein Godot-Binary. + answers: + - godot --headless --path . --export-release "Linux" build/linux/testgelaende.x86_64 + - godot --headless --export-release "Linux" build/linux/testgelaende.x86_64 + - godot --path . --headless --export-release "Linux" build/linux/testgelaende.x86_64 + - godot --headless --export-release "Linux" build/linux/testgelaende.x86_64 --path . + - godot --path . --export-release "Linux" build/linux/testgelaende.x86_64 --headless + - godot --export-release "Linux" build/linux/testgelaende.x86_64 --headless + - godot --headless --path . --export-release "Linux" ./build/linux/testgelaende.x86_64 + - godot --headless --export-release "Linux" ./build/linux/testgelaende.x86_64 + explanation: | + `--export-release` nimmt den Preset-Namen und den Zielpfad. `--headless` verhindert das Fenster; auf Rechnern ohne Grafik, etwa in CI, ist es Pflicht. Im Projektordner kannst du `--path .` weglassen. `--export-debug` hätte denselben Aufbau und baut den Debug-Build. + resources: + - title: Godot Docs · Command line tutorial · Exporting + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#exporting + + - id: testversion-taggen + type: command + title: Die Testversion markieren + prompt: | + Setze auf den aktuellen Commit einen **annotierten** Tag `testversion-2026-10-22` mit der Nachricht `Testversion für die Messrunde`. + answers: + - git tag -a testversion-2026-10-22 -m "Testversion für die Messrunde" + - git tag -a -m "Testversion für die Messrunde" testversion-2026-10-22 + - git tag testversion-2026-10-22 -a -m "Testversion für die Messrunde" + - git tag -m "Testversion für die Messrunde" testversion-2026-10-22 + - git tag testversion-2026-10-22 -m "Testversion für die Messrunde" + - git tag -am "Testversion für die Messrunde" testversion-2026-10-22 + - git tag -a testversion-2026-10-22 -m "Testversion für die Messrunde" HEAD + - git tag -a testversion-2026-10-22 HEAD -m "Testversion für die Messrunde" + - git tag -m "Testversion für die Messrunde" testversion-2026-10-22 HEAD + explanation: | + Ein annotierter Tag ist ein eigenes Objekt mit Autor, Datum und Nachricht; `-m` allein impliziert `-a`. So steht im Repository, welcher Stand als Testversion rausging. Den Tag nennst du im Protokoll und beim Weitergeben; mit `git push origin testversion-2026-10-22` kommt er auf den Server. Das Namensschema ist ein Platzhalter, du legst es fest. + resources: + - title: Git · git-tag + url: https://git-scm.com/docs/git-tag + + - id: gitignore-ergaenzen + type: gap + title: Die .gitignore des Testgeländes + prompt: | + Ergänze die `.gitignore` so, dass der Cache von Godot und der Export-Ordner `build` im Projektordner nicht ins Repository kommen. + lang: text + code: | + # Godot 4+ specific ignores + ⟦.godot/⟧ + /android/ + # Exporte + ⟦/build/⟧ + gaps: + - label: Cache und Importergebnisse + hint: Der versteckte Ordner, den Godot beim ersten Öffnen anlegt. + accept: [".godot"] + - label: Export-Ordner + hint: Mit Schrägstrich am Anfang gilt das Muster nur im Projektordner. + accept: ["build/", "/build"] + explanation: | + `.godot/` enthält Cache, Importergebnisse und `export_credentials.cfg`; Godot erzeugt ihn bei jedem Öffnen neu. Die ersten drei Zeilen schreibt Godot 4.7.2 selbst, wenn du unter *Projekt → Versionsverwaltung* die Metadaten für Git erstellst. `/build/` ergänzt du für deine Exporte. Ein Schrägstrich am Anfang verankert das Muster am Projektordner, einer am Ende passt nur auf Ordner. + resources: + - title: Godot Docs · Files to exclude from VCS + url: https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html#files-to-exclude-from-vcs + - title: Git · gitignore · Pattern Format + url: https://git-scm.com/docs/gitignore#_pattern_format + + - id: eine-datei-weitergeben + type: bug + title: Der Freund sieht nur eine Fehlermeldung + prompt: | + Du willst Testversionen als eine einzige Datei weitergeben. Dein Freund startet `testgelaende.x86_64` und bekommt `Couldn't load project data at path … Is the .pck file missing?`. Finde in `export_presets.cfg` die Zeile und korrigiere sie. + lang: ini + code: | + [preset.1.options] + + custom_template/debug="" + custom_template/release="" + debug/export_console_wrapper=1 + binary_format/embed_pck=false + texture_format/s3tc_bptc=true + binary_format/architecture="x86_64" + lines: [6] + fix: + - line: 6 + answers: + - binary_format/embed_pck=true + explanation: | + Mit `embed_pck=false` schreibt der Export Programm und `.pck` getrennt; wer nur das Programm bekommt, kann es nicht starten. Mit `true` hängt Godot die PCK an das Programm an. Gemessen mit Godot 4.7.2: 73 534 928 Byte in einer Datei statt 73 519 416 plus 15 508 Byte in zweien, und die eine Datei startete ohne Fehler. Im Dialog heißt die Einstellung *PCK einbetten*. + resources: + - title: Godot Docs · PCK versus ZIP pack file formats + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html#pck-versus-zip-pack-file-formats + + - id: gitignore-zu-streng + type: bug + title: Nach dem Klonen gilt wieder der Standard + prompt: | + Am PC hast du im Import-Dock für deinen Prüfton eine Einstellung geändert und auf *Neuimport* geklickt. Auf dem Laptop klonst du das Repository, und dort gilt für den Prüfton wieder der Standard. Außerdem zeigt `git status` am PC bei neuen Skripten immer nur die `.gd`-Datei, nie die `.gd.uid` daneben. Finde die beiden Zeilen der `.gitignore`, die das verursachen. + lang: text + code: | + # Godot 4+ specific ignores + .godot/ + /android/ + *.import + *.uid + /build/ + lines: [4, 5] + explanation: | + Die Import-Einstellungen stehen in der `.import`-Datei neben der Quelle. Ist sie ignoriert, legt Godot auf dem Laptop eine neue mit Standardwerten an. Die `.uid`-Dateien enthalten die IDs, über die Szenen auf Skripte verweisen; das Godot-Team schreibt, dass sie committet werden müssen, weil sonst auf anderen Rechnern die UID-Verweise brechen und Godot mit einer Warnung auf den Pfad zurückfällt. Beide Zeilen gehören gelöscht, die Dateien danach committet. Ignoriert wird nur, was Godot ohne Verlust neu erzeugt. + resources: + - title: Godot Blog · UID changes coming to Godot 4.4 + url: https://godotengine.org/article/uid-changes-coming-to-godot-4-4/#what-happens-if-i-dont-commit-uid-files-to-version-control + - title: Godot Docs · Import process + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html + + - id: credentials-erklaeren + type: explain + title: Zwei Dateien für einen Export + prompt: | + Godot speichert die Export-Einstellungen in zwei Dateien. Erkläre in wenigen Sätzen, warum, was jeweils drinsteht und was das für Git und für einen zweiten Rechner bedeutet. + lang: ini + code: | + ; export_presets.cfg (Projektordner) + [preset.1] + name="Linux" + export_path="build/linux/testgelaende.x86_64" + + ; .godot/export_credentials.cfg + [preset.1] + script_encryption_key="" + points: + - "`export_presets.cfg` enthält Plattform, Pfad und Optionen ohne Geheimnisse und gehört ins Repository." + - "`export_credentials.cfg` enthält Schlüssel und Passwörter, etwa für Verschlüsselung oder Signatur, und liegt in `.godot/`." + - Weil `.godot/` ignoriert wird, bleiben die Geheimnisse lokal und gelangen nicht in die History. + - Auf einem zweiten Rechner fehlen diese Werte; du kopierst die Datei von Hand oder trägst sie neu ein. + explanation: | + Die Trennung hält Geheimnisse aus Git heraus, ohne dass du die Presets auf jedem Rechner neu anlegen musst. `export_presets.cfg` beschreibt, was gebaut wird, und gehört ins Repository. `.godot/export_credentials.cfg` enthält, was niemand sehen soll, etwa den Schlüssel für eine verschlüsselte PCK oder das Passwort für die Windows-Signatur. Weil die Datei im ignorierten `.godot/` liegt, fehlt sie in jedem Klon; die Doku empfiehlt, sie bei Bedarf von Hand zu kopieren. + resources: + - title: Godot Docs · Exporting projects · Configuration files + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html#configuration-files + + - id: testversion-bauen + type: practice + title: Zwei Testversionen bauen und taggen + prompt: | + Installiere die Exportvorlagen für deine Godot-Version, lege im Testgelände Presets für Linux und Windows an und exportiere beide als Debug-Build aus dem Terminal. Starte den Linux-Export mit `--verbose`, finde sein Log und markiere den Stand mit einem Tag. + lang: text + solution: | + $ mkdir -p build/linux build/windows + $ godot --headless --path . --export-debug "Linux" build/linux/testgelaende.x86_64 + $ godot --headless --path . --export-debug "Windows Desktop" build/windows/testgelaende.exe + $ ls build/linux build/windows + build/linux: + testgelaende.pck testgelaende.sh testgelaende.x86_64 + + build/windows: + testgelaende.console.exe testgelaende.exe testgelaende.pck + $ ./build/linux/testgelaende.x86_64 --verbose + $ ls ~/.local/share/godot/app_userdata/Testgelände/logs/ + $ git tag -a testversion-2026-10-22 -m "Testversion für die Messrunde" + + .gitignore enthält /build/, export_presets.cfg ist committet. + checklist: + - Der Exportvorlagen-Manager zeigt deine Editor-Version und die installierten Vorlagen für Linux und Windows. + - "`export_presets.cfg` liegt im Projektordner, ist committet und enthält die Presets `Linux` und `Windows Desktop`." + - Beide Exporte entstanden aus dem Terminal ohne Fehlermeldung. + - Der Linux-Export startet, und du hast `godot.log` unter `user://logs/` geöffnet. + - "`git status` zeigt nichts aus `build/` und nichts aus `.godot/`." + - Der Tag zeigt auf den exportierten Stand, und du hast ihn notiert. + explanation: | + Die Reihenfolge Vorlagen, Presets, Ordner, Export ist die aus der Lektion. Den Windows-Export erzeugst du unter Linux ohne zusätzliche Werkzeuge; ob er startet, prüfst du auf einem Windows-Rechner. Im Test mit Godot 4.7.2 entstanden beim Debug-Export für Windows ohne eingebettete PCK drei Dateien: `testgelaende.exe`, `testgelaende.pck` und `testgelaende.console.exe`, der Starter mit Konsolenfenster aus der Einstellung *Konsolenwrapper exportieren*. Der Tag verbindet die Dateien, die du weitergibst, mit genau einem Commit. + resources: + - title: Godot Docs · Exporting for Windows + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_windows.html + - title: Godot Docs · Exporting for Linux + url: https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_linux.html + + - id: ablage-regeln + type: practice + title: Deine Regeln für Originale und große Dateien + prompt: | + Leg für dein Projekt fest, wo deine Arbeitsdateien liegen und wie sie gesichert werden. Schreib in fünf bis acht Sätzen auf: welche Werkzeuge du nutzt und welche Dateitypen dabei entstehen, wo die Originale liegen, welche exportierten Dateien ins Godot-Projekt kommen, welche Dateitypen du in Git LFS legst oder warum nicht, und wann du committest. + lang: prose + checklist: + - Jeder Dateityp deiner Werkzeuge ist einem Ort zugeordnet, im Projekt, neben dem Projekt oder hinter `.gdignore`. + - Du hast die Größe deiner größten Arbeitsdatei nachgesehen und notiert. + - Für Git LFS steht eine Entscheidung mit Begründung da, ja oder nein. + - Die Regel für Commits nennt einen Anlass, etwa vor und nach jeder Abstimmungsrunde. + - Du hast die Regeln an einem Ordner deines Projekts geprüft, und `git status` zeigt nichts Ungewolltes. + hints: + - Denk an REAPER-Projekte mit Aufnahmen und an Aseprite- oder Krita-Dateien mit Ebenen. + - "`du -h` zeigt dir im Terminal die Größe von Dateien und Ordnern." + explanation: | + Wo Originale liegen, hängt an deinen Werkzeugen und daran, wie groß ihre Dateien werden; dafür gibt es keine Regel, die für alle passt. Wichtig ist, dass jede Datei einen festen Ort hat und dass Godot nur importiert, was ins Spiel soll. Git LFS lohnt sich für große, oft geänderte Binärdateien, kostet aber Einrichtung und Speicher auf dem Server. Deine Regeln gehören in das Projekt, etwa als Abschnitt in einer README, damit ein Coding-Agent sie ebenfalls befolgt. + resources: + - title: Godot Docs · Ignoring specific folders + url: https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html#ignoring-specific-folders + - title: Git LFS + url: https://git-lfs.com/ diff --git a/content/godot/modules/fehler-eingrenzen.yaml b/content/godot/modules/fehler-eingrenzen.yaml new file mode 100644 index 0000000..4706223 --- /dev/null +++ b/content/godot/modules/fehler-eingrenzen.yaml @@ -0,0 +1,486 @@ +id: fehler-eingrenzen +title: "Fehler eingrenzen: Debugger, Monitore und Profiler lesen" +level: 2 +minutes: 130 +summary: Statt einen Fehler an den Coding-Agenten zurückzugeben, grenzt du ihn selbst ein – mit einer festen Methode, mit Fehlermeldungen und Backtrace, mit Stacktrace und Haltepunkten, mit Monitoren für verlorene Nodes und mit dem Profiler für langsame Funktionen. +place: unterwegs +goals: + - Du gehst bei einem Fehler nach einer festen Methode vor – reproduzieren, verkleinern, eine Änderung, vergleichen, halbieren – und weißt, wann `git bisect` hilft. + - Du liest eine Godot-Fehlermeldung mit Backtrace und findest die Zeile mit der Ursache, nicht nur die, an der es knallt. + - Du nutzt Stacktrace, Variablen und Haltepunkte des Debuggers, um einen Zustand im angehaltenen Spiel zu prüfen. + - Du erkennst am Monitor „Verwaiste Nodes“, dass Nodes verloren gehen, und an Warnungen, wo Code unsauber ist. + - Du findest mit dem Profiler die Funktion, die ein Bild langsam macht, und unterscheidest Gesamt- und Eigenzeit. +resources: + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html + - title: Godot Docs · Debugger panel + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html + - title: Godot Docs · The Profiler + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html + - title: Godot Docs · GDScript warning system + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/warning_system.html +lesson: | + Nach einer Änderung des Coding-Agenten bleibt der Zähler im HUD bei 0, obwohl die Sammelstücke verschwinden. Die Ausgabe ist rot. Der schnelle Weg wäre, alles zurück an den Agenten zu geben: „Geht nicht mehr, bitte reparieren.“ Dann rät er, ändert drei Dinge auf einmal, und mit Glück verschwindet das Symptom. Was die Ursache war, weißt du danach nicht, und beim nächsten Mal fängst du von vorn an. + + Den Fehler kannst du selbst eingrenzen: mit einer Methode, die bei jedem Fehler gleich bleibt, und mit den Werkzeugen, die Godot 4.7 dafür mitbringt. Die Fehlermeldungen und Bilder stammen aus Läufen des Testgeländes mit Godot 4.7.2, in das ein Fehler eingebaut war. + + ## Methode: reproduzieren, verkleinern, vergleichen + + Fünf Schritte, in dieser Reihenfolge: + + 1. **Reproduzieren.** Schreib auf, mit welchen Schritten der Fehler jedes Mal auftritt. Was nur manchmal passiert, lässt sich nicht prüfen. Für Abläufe, die auf den Frame ankommen, gibt es Drehbuch-Skripte und feste Bildraten, wie in [Testszene: Angriff](/combat/testszene-angriff). + 2. **Verkleinern.** Bau eine kleine Szene mit nur den beteiligten Nodes und starte sie mit *Aktuelle Szene abspielen*. Je weniger im Spiel ist, desto weniger kommt als Ursache in Frage. + 3. **Vermuten und eine Änderung.** Schreib eine Vermutung auf und ändere genau eine Sache. Zwei Änderungen auf einmal lassen sich im Ergebnis nicht trennen; das ist dieselbe Regel wie beim Abstimmen in [Tuning als Experiment](/combat/tuning). + 4. **Vergleichen.** Vorher und nachher mit derselben Probe: dasselbe Log, dieselben Schritte, derselbe Monitor. + 5. **Halbieren.** Lief es gestern noch, sucht `git bisect` den Commit, der den Fehler brachte, in wenigen Schritten ([Fehlersuche in der History](/git/bisect-historie)). In einer einzelnen Datei halbierst du von Hand: Die Hälfte auskommentieren, testen, die schuldige Hälfte weiter teilen. + + Aus der Webentwicklung kennst du die Werkzeuge dazu: Die **Ausgabe** entspricht der Konsole, der **Debugger** mit Haltepunkten dem Sources-Panel, der **Profiler** dem Performance-Panel und die **Monitore** grob dem Memory-Panel. Der Vergleich bricht an einer Stelle: Dein Spiel läuft in einem eigenen Prozess. Was du im Editor siehst, sind Daten, die das Spiel über eine Debug-Verbindung schickt ([Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html)). + + > [!PRAXIS] + > Gibst du einen Fehler doch an einen Coding-Agenten, dann mit Material: die Schritte zum Reproduzieren, erwartetes und tatsächliches Verhalten, die vollständige Fehlermeldung mit Backtrace und was du schon ausgeschlossen hast. Bitte um genau eine Änderung und prüfe sie mit derselben Probe. Dann bleibt die Ursache nachvollziehbar, auch im Commit. + + ## Fehlermeldungen lesen: die erste Meldung zählt + + Im Testgelände war in `hud.gd` der Pfad zum Label falsch, `$Count` statt `$Counter`. Beim Start und beim ersten Einsammeln schrieb Godot 4.7.2 in die Ausgabe: + + ```text + ERROR: Node not found: "Count" (relative to "/root/Main/HUD"). + at: get_node (scene/main/node.cpp:1975) + GDScript backtrace (most recent call first): + [0] @implicit_ready (res://hud.gd:5) + SCRIPT ERROR: Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'null instance'. + at: show_total (res://hud.gd:9) + GDScript backtrace (most recent call first): + [0] show_total (res://hud.gd:9) + [1] collect (res://player.gd:28) + [2] _on_body_entered (res://pickup.gd:10) + ``` + + So liest du das: + + - **`ERROR`** meldet die Engine, hier `get_node()`. Die Zeile `at:` nennt eine Stelle im C++-Quelltext von Godot; die brauchst du selten. + - **`SCRIPT ERROR`** meldet GDScript: Eine Zeile deines Codes ließ sich nicht ausführen. + - **`GDScript backtrace (most recent call first)`** listet die Aufrufkette. `[0]` ist die Zeile, an der es passiert ist, darunter steht, wer sie aufgerufen hat: Das Pickup rief `collect()` auf, das Signal `collected` erreichte `show_total()`. + - **Die Ursache steht oft weiter oben.** Die zweite Meldung knallt in Zeile 9, weil `counter` `null` ist. `null` ist es, weil der Pfad in Zeile 5 nicht stimmt. Die erste Meldung nennt diese Ursache. Lies deshalb von oben nach unten und behebe die erste Meldung zuerst. + + `@implicit_ready` ist der Teil von `_ready`, den Godot für `@onready`-Variablen erzeugt. Den Backtrace gibt es seit Godot 4.5 im Editor und in Debug-Exporten, in Release-Exporten nur, wenn du ihn einschaltest ([Script backtraces](https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html#script-backtraces)). + + Weitere Meldungen, die du im Testgelände erzeugen kannst, alle mit Godot 4.7.2 nachgestellt: + + - `The InputMap action "move_left" doesn't exist. Did you mean "ui_left"?` – Die Aktion fehlt in der Eingabe-Zuordnung. `player.gd:16` meldet das zweimal pro Tick. + - `Can't change this state while flushing queries. Use call_deferred() or set_deferred() to change monitoring state instead.` – Eine Form wurde in einem Physik-Signal direkt abgeschaltet. + - `Removing a CollisionObject node during a physics callback is not allowed and will cause undesired behavior. Remove with call_deferred() instead.` – Eine Area wurde mit `remove_child()` in `_on_body_entered` aus dem Baum genommen. + - `Invalid access to property or key 'frames' on a base object of type 'Nil'.` – Eine Variable hat keinen Wert, etwa weil ein Node nie zugewiesen wurde. + + > [!MERKE] + > Behebe die erste Meldung zuerst und starte neu. Viele spätere Meldungen sind nur Folgen der ersten. + + > [!WARNING] Symptome zudecken + > Ein `if counter:` vor Zeile 9 lässt die rote Zeile verschwinden, der Zähler bleibt trotzdem bei 0. Solche Wächter verschieben den Fehler nur. Coding-Agenten schlagen sie gern vor, weil danach keine Meldung mehr kommt. Frag nach der Ursache, nicht nach Ruhe in der Ausgabe. + + ## Der Debugger: Stacktrace, Variablen, Haltepunkte + + Läuft das Spiel aus dem Editor, hält es bei einem `SCRIPT ERROR` an der Zeile an. Unten im **Debugger** zeigt der Reiter **Stacktrace** die Meldung, die Aufrufkette und die Variablen des gewählten Aufrufs ([Stack Trace](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html#stack-trace)): + + ![Debugger, Reiter Stacktrace: Stack Frames 0 res://hud.gd:9, 1 res://player.gd:28, 2 res://pickup.gd:10; darunter Locals mit total 1 und Members mit self CanvasLayer und counter null](/media/godot/fehler-eingrenzen/stacktrace@2x.png "Godot 4.7.2, angehalten beim Fehler in hud.gd:9") + + Unter **Members** steht `counter` mit ``, unter **Locals** `total` mit 1. Damit ist klar: Der Wert kam an, das Label fehlt. Klickst du auf Frame 1 oder 2, siehst du die Variablen von `collect()` oder `_on_body_entered()`. Im Reiter **Fehler** stehen alle Meldungen des Laufs mit der Zeit seit dem Start; ein Klick auf den Pfeil klappt Backtrace und C++-Quelle auf: + + ![Reiter Fehler, alle Einträge aufgeklappt: zwei rote Einträge mit der Zeit seit dem Start, aufgeklappt mit C++-Fehler, C++-Quelle und Stacktrace hud.gd:5, darunter show_total mit Stacktrace hud.gd:9, player.gd:28, pickup.gd:10](/media/godot/fehler-eingrenzen/fehler-tab@2x.png "Godot 4.7.2, Reiter Fehler nach Alle ausklappen") + + Anhalten kannst du auch selbst. Ein **Haltepunkt** entsteht per Klick links neben die Zeilennummer im Skript-Editor (roter Punkt) oder mit dem Schlüsselwort `breakpoint` im Code, das mit dem Skript gespeichert wird. Ist das Spiel angehalten, gehst du mit **Hereinspringen**, **Überspringen** und **Hinausspringen** Zeile für Zeile weiter, **Fortfahren** lässt es weiterlaufen. Im Reiter **Auswerter** rechnest du während der Pause mit Variablen des aktuellen Aufrufs. Der Knopf **Fehler-Breaks ignorieren** lässt das Spiel bei Fehlern weiterlaufen; das ist praktisch, wenn ein bekannter Fehler dich nicht bei jedem Test stoppen soll. + + ```gdscript + func collect(value: int) -> void: + total += value + if total == 2: + breakpoint # hält nur beim zweiten Sammelstück an + collected.emit(total) + ``` + + Für Fehler, die man sieht statt liest, hat das Menü **Debuggen** Schalter für Starts aus dem Editor: **Sichtbare Collision-Shapes**, **Sichtbare Pfade**, **Sichtbare Navigation** und **CanvasItems-Redraws debuggen** ([Debug menu options](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#debug-menu-options)). Den Baum des laufenden Spiels zeigt das Szenen-Dock im Modus **Remote**; wie du dort Werte liest und warum Änderungen nach dem Beenden weg sind, steht in [Aktionen und Parameter](/godot/aktionen-parameter). + + ### Warnungen der statischen Prüfung + + Neben Fehlern meldet GDScript **Warnungen**: Code, der läuft, aber vermutlich nicht so, wie gemeint. Im Skript-Editor zeigt ein gelbes Dreieck unten rechts ihre Zahl, im Reiter Fehler stehen sie gelb. Drei echte Warnungen aus einer `hud.gd`, in die ein Agent `var half = total / 2` und eine unbenutzte Variable `label` geschrieben hatte: + + ```text + GDScript::reload: Integer division. Decimal part will be discarded. + GDScript::reload: The local variable "half" is declared but never used in the block. If this is intended, prefix it with an underscore: "_half". + GDScript::reload: The local variable "label" is declared but never used in the block. If this is intended, prefix it with an underscore: "_label". + ``` + + Welche Warnungen aktiv sind und ob sie als Fehler zählen, steht unter *Projekteinstellungen → Debuggen → GDScript* (`debug/gdscript/warnings/…`, [Warning system](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/warning_system.html)). Die Warnung für fehlende Typangaben, „Untyped Declaration“ (`untyped_declaration`), steht im Standard auf `Ignore`. Schaltest du sie ein, zeigt sie dir in KI-Code jede Variable ohne Typ; mit Typen erkennt der Editor mehr Fehler vor dem Start ([Static typing](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/static_typing.html)). Beim Start aus dem Terminal standen diese Warnungen im Test nicht in der Ausgabe, nur im Editor. + + ## Monitore: Objekte, Nodes und verwaiste Nodes + + Die Gruppe **Objekt** im Reiter **Monitore** zählt Objekte, Ressourcen, Nodes im Baum und **Verwaiste Nodes**: Nodes, die es gibt, die aber in keinem Baum hängen ([Performance.OBJECT_ORPHAN_NODE_COUNT](https://docs.godotengine.org/en/stable/classes/class_performance.html#class-performance-constant-object-orphan-node-count)). Ein Node, der aus dem Baum genommen und nie freigegeben wird, bleibt im Speicher. Steigt dieser Wert während des Spiels, verliert ein Skript Nodes; der Wert ist nur in Debug-Läufen verfügbar. + + ![Monitore Objekte 1757, Nodes 3 und Verwaiste Nodes 205; die Kurve der verwaisten Nodes steigt gleichmäßig an, Nodes bleibt flach](/media/godot/fehler-eingrenzen/monitor-verwaist@2x.png "Godot 4.7.2, Messszene, die bei jedem sechsten Physik-Tick einen Node2D erzeugt und nie einhängt") + + Der häufigste Fall im Testgelände: Ein Pickup verschwindet mit `get_parent().remove_child(self)` statt mit `queue_free()`. Gemessen mit Godot 4.7.2 nach einem eingesammelten Pickup: + + - `get_parent().remove_child(self)`: Meldung `Removing a CollisionObject node during a physics callback …`, 3 verwaiste Nodes. + - `get_parent().remove_child.call_deferred(self)`: keine Meldung während des Spiels, 3 verwaiste Nodes. + - `queue_free()`: keine Meldung, 0 verwaiste Nodes. + + Drei, weil das Pickup mit `Body` und `Area` drei Nodes hat. Die zweite Zeile ist die tückische: Sie sieht sauber aus, und erst beim Beenden meldet Godot `WARNING: 6 ObjectDB instances were leaked at exit`, gefolgt vom Hinweis auf `--verbose`. `queue_free()` gibt den Node am Ende des Frames frei, samt Kindern ([Node.queue_free](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-queue-free)). + + Welche Nodes verwaist sind, schreibt `Node.print_orphan_nodes()` in die Ausgabe, eine Zeile pro Node mit Objekt-ID, Name und Typ, etwa `… - Stray Node: Weg (Type: Node2D) (Source:)`. Seit Godot 4.6 gibt es außerdem den Reiter **ObjectDB-Profiler**: Er macht Schnappschüsse aller Objekte und vergleicht zwei davon ([ObjectDB profiler](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/objectdb_profiler.html)). + + ## Profiler: welche Funktion kostet Zeit + + Ruckelt das Spiel und zeigt der Monitor lange Bilder, sagt dir der **Profiler**, wo die Zeit bleibt ([The Profiler](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html)). Er läuft nicht von selbst, weil Messen selbst Zeit kostet: Spiel starten, im Reiter Profiler auf **Start**, eine Weile spielen, mit **Stopp** beenden. Die Messszene für das Bild unten hatte eine absichtlich langsame Funktion: + + ```gdscript title=langsam.gd + extends Node2D + + ## Nur für die Messung: eine absichtlich langsame Funktion im Physik-Takt. + + var total := 0.0 + + + func _physics_process(_delta: float) -> void: + total = count_tiles() + + + func count_tiles() -> float: + var sum := 0.0 + for i in 60000: + sum += sqrt(float(i)) + return sum + ``` + + ![Profiler mit Messung Frame-Zeit (ms) und Zeit Gesamt; unter Script Functions stehen _physics_process mit 2.97 ms und count_tiles mit 2.97 ms, count_tiles ist angehakt](/media/godot/fehler-eingrenzen/profiler@2x.png "Godot 4.7.2, Profiler nach 4 Sekunden Messung") + + Unter **Script Functions** stehen deine Funktionen mit Zeit und Zahl der Aufrufe im gewählten Frame. Die Zeit hängt an der Einstellung **Zeit**: **Gesamt** zählt die Aufrufe in einer Funktion mit, **Eigen** nur ihren eigenen Code. Gemessen mit Godot 4.7.2 in einem zweiten Lauf (jeder Lauf misst etwas anders, im Bild sind es 2,97 ms): Bei Gesamt stand `_physics_process` bei 2,71 ms und `count_tiles` bei 2,70 ms, bei Eigen fiel `_physics_process` auf 0,00 ms, `count_tiles` blieb bei 2,70 ms. Die Zeit steckt also in `count_tiles`, und `_physics_process` wartet nur darauf. Ein Haken vor einer Funktion zeichnet sie in den Graphen rechts; ein Klick in den Graphen wählt den Frame. + + Der Reiter **Visueller Profiler** misst nur das Zeichnen auf CPU und GPU, keine Skripte und keine Physik ([Visual Profiler](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html#visual-profiler)). Für ein einzelnes Codestück reicht oft `Time.get_ticks_usec()` vor und nach dem Aufruf. + + > [!INTERVIEW] + > „Das Spiel hakt alle paar Sekunden. Wie findest du die Ursache?“ Gute Antwort: Erst reproduzieren und im Monitor nachsehen, ob es lange Bilder sind oder ein anderer Effekt. Dann den Profiler starten, zum Ausschlag im Graphen springen und unter Script Functions mit Zeit „Eigen“ die Funktion suchen, deren eigener Code die Zeit braucht. Bevor ich etwas optimiere, messe ich den Zustand, danach messe ich mit derselben Probe noch einmal. + + ## Häufige Fehler + + - **Die letzte Meldung behoben statt der ersten.** Folgefehler verschwinden mit ihrer Ursache. + - **Den Backtrace überlesen.** `[0]` sagt, wo es knallt; die Zeilen darunter, wer den Aufruf ausgelöst hat. + - **Wächter statt Ursache.** `if node:` versteckt einen falschen Pfad, statt ihn zu korrigieren. + - **Mehrere Änderungen auf einmal.** Danach weißt du nicht, welche gewirkt hat. + - **`remove_child()` statt `queue_free()`.** Nodes bleiben verwaist, der Monitor steigt; im Physik-Signal kommt zusätzlich eine Fehlermeldung. + - **Profiler mit Zeit Gesamt gelesen.** Die aufrufende Funktion sieht genauso teuer aus wie die langsame darin; mit Eigen trennst du sie. + - **Warnungen ignoriert.** Ganzzahldivision und unbenutzte Variablen sind oft Reste einer halb fertigen Änderung. + + ## Zusammenfassung + + - Die Methode bleibt gleich: reproduzieren, verkleinern, eine Änderung, vergleichen, halbieren; für alte Stände `git bisect`. + - Fehlermeldungen liest du von oben. `[0]` im Backtrace ist die Stelle, die Ursache steht oft in einer früheren Meldung. + - Bei einem Skriptfehler hält das Spiel an; der Stacktrace zeigt Aufrufkette und Variablen, der Reiter Fehler alle Meldungen. + - Haltepunkte setzt du per Klick oder mit `breakpoint`, dann gehst du mit Hereinspringen und Überspringen weiter. + - Warnungen markieren Code, der läuft, aber vermutlich falsch ist; welche aktiv sind, steht in den Projekteinstellungen. + - Steigen die verwaisten Nodes, verliert ein Skript Nodes. Freigeben mit `queue_free()`. + - Der Profiler zeigt Zeit je Funktion; mit „Eigen“ findest du die, deren eigener Code langsam ist. +exercises: + - id: werkzeug-waehlen + type: choice + title: Welches Werkzeug beantwortet welche Frage? + prompt: | + Welche Zuordnungen stimmen? + options: + - text: Welche Funktion macht ein Bild langsam? – Profiler, Script Functions + correct: true + why: Der Profiler misst Zeit je Funktion und Frame. Das kann kein anderes Werkzeug im Debugger. + - text: Gehen beim Einsammeln Nodes verloren? – Monitore, Verwaiste Nodes + correct: true + why: Der Monitor zählt Nodes außerhalb des Baums. Steigt er bei jedem Pickup, wird etwas nicht freigegeben. + - text: Welchen Wert hat eine Variable in dem Moment, in dem der Fehler passiert? – Visueller Profiler + correct: false + why: Der Visuelle Profiler misst nur das Zeichnen. Variablen zeigt der Stacktrace, wenn das Spiel angehalten ist. + - text: Stimmt die Kollisionsform zur Figur? – Monitore, Physik-2D + correct: false + why: Die Monitore zählen Kollisionspaare, zeigen aber keine Formen. Dafür gibt es *Debuggen → Sichtbare Collision-Shapes*. + explanation: | + Jedes Werkzeug beantwortet eine Art Frage. Zeit je Funktion: Profiler. Zählwerte über die Zeit: Monitore. Zustand in einem bestimmten Moment: Stacktrace mit Variablen, wenn das Spiel an einem Fehler oder Haltepunkt steht. Was man sehen muss, etwa Formen und Pfade, schaltet das Menü Debuggen sichtbar. Wer zuerst die Frage formuliert, greift zum richtigen Werkzeug. + resources: + - title: Godot Docs · Debugger panel + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html + + - id: methode-ordnen + type: order + title: Ein Fehler, fünf Schritte + prompt: | + Der Zähler im HUD bleibt bei 0, und du weißt nicht, seit wann. Bring die Schritte in eine sinnvolle Reihenfolge. + lang: prose + lines: | + Die Schritte aufschreiben, mit denen der Zähler jedes Mal bei 0 bleibt. + Eine kleine Szene mit Figur, einem Pickup und dem HUD bauen. + Eine Vermutung notieren und genau eine Sache ändern. + Mit denselben Schritten testen und das Ergebnis mit vorher vergleichen. + Ursache, Korrektur und Beleg im Protokoll festhalten und committen. + explanation: | + Ohne zuverlässige Reproduktion lässt sich keine Änderung prüfen. Die kleine Szene schließt alles aus, was nicht beteiligt ist. Eine Vermutung mit genau einer Änderung macht das Ergebnis eindeutig, und der Vergleich mit derselben Probe zeigt, ob sie gewirkt hat. Das Protokoll mit Commit sorgt dafür, dass du denselben Fehler später wiedererkennst. Den fünften Schritt der Methode, das Halbieren mit `git bisect`, brauchst du hier nicht: Er hilft, wenn ein älterer Stand noch funktioniert und du den Commit suchst, der den Fehler brachte. + resources: + - title: Git · git-bisect + url: https://git-scm.com/docs/git-bisect + + - id: ursache-finden + type: output + title: Wo steckt die Ursache? + prompt: | + Das ist die Ausgabe nach dem Start und dem ersten Einsammeln. In welcher Datei und Zeile steckt die Ursache beider Meldungen? Antworte im Format `datei.gd:zeile`. + lang: text + code: | + ERROR: Node not found: "Count" (relative to "/root/Main/HUD"). + at: get_node (scene/main/node.cpp:1975) + GDScript backtrace (most recent call first): + [0] @implicit_ready (res://hud.gd:5) + SCRIPT ERROR: Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'null instance'. + at: show_total (res://hud.gd:9) + GDScript backtrace (most recent call first): + [0] show_total (res://hud.gd:9) + [1] collect (res://player.gd:28) + [2] _on_body_entered (res://pickup.gd:10) + expected: | + hud.gd:5 + accept: + - res://hud.gd:5 + - hud.gd 5 + - hud.gd Zeile 5 + explanation: | + Die erste Meldung kommt aus `@implicit_ready`, also beim Belegen der `@onready`-Variablen in Zeile 5: Der Pfad `Count` findet keinen Node, die Variable bleibt `null`. Die zweite Meldung in Zeile 9 ist die Folge, denn dort wird `text` auf `null` gesetzt. `player.gd:28` und `pickup.gd:10` sind nur die Aufrufer. Beide Meldungen stammen aus einem Lauf mit Godot 4.7.2. + resources: + - title: Godot Docs · Logging · Script backtraces + url: https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html#script-backtraces + + - id: fehler-pro-tick + type: output + title: Wie oft meldet sich die fehlende Aktion? + prompt: | + Im Testgelände fehlt in der Eingabe-Zuordnung `move_left`. `player.gd` ruft in jedem Physik-Tick `Input.get_vector()` mit den vier Aktionen auf. Nach 10 Ticks zählst du 20 Zeilen `The InputMap action "move_left" doesn't exist.` Jetzt löschst du zusätzlich `move_up`. Wie viele Zeilen mit `doesn't exist` stehen nach 10 Ticks insgesamt in der Ausgabe? + lang: text + code: | + ERROR: The InputMap action "move_up" doesn't exist. Did you mean "move_down"? + at: get_action_raw_strength (core/input/input.cpp:553) + ERROR: The InputMap action "move_left" doesn't exist. Did you mean "ui_left"? + at: get_action_raw_strength (core/input/input.cpp:553) + ERROR: The InputMap action "move_left" doesn't exist. Did you mean "ui_left"? + at: action_get_deadzone (core/input/input_map.cpp:191) + … + expected: | + 40 + accept: + - 40 Zeilen + explanation: | + 20 Zeilen in 10 Ticks heißen: Jede fehlende Aktion meldet sich pro Tick zweimal. Die `at:`-Zeilen zeigen, warum: `get_vector()` liest für jede Aktion die Stärke und die Deadzone. Mit zwei fehlenden Aktionen sind es 2 × 2 × 10 = 40. Gemessen mit Godot 4.7.2: 20 Zeilen für `move_left` und 20 für `move_up`. Bei 60 Ticks pro Sekunde sind das 240 Zeilen pro Sekunde, die alles andere verdrängen; deshalb lohnt der Blick ganz nach oben. Der Vorschlag `Did you mean "move_down"?` ist nur eine Ähnlichkeitssuche über die Namen, keine Diagnose. + resources: + - title: Godot Docs · Input.get_vector + url: https://docs.godotengine.org/en/stable/classes/class_input.html#class-input-method-get-vector + + - id: verwaiste-zaehlen + type: output + title: Was zeigt der Monitor? + prompt: | + Dieses Testskript läuft als Hauptschleife. Was gibt es in Tick 120 aus? + lang: gdscript + code: | + extends SceneTree + + var frame := 0 + var spawned: Array[Node2D] = [] + + + func _initialize() -> void: + physics_frame.connect(_tick) + var a := Node2D.new() + root.add_child(a) + root.remove_child(a) + + + func _tick() -> void: + frame += 1 + if frame % 6 == 0: + spawned.append(Node2D.new()) + if frame == 120: + var orphans := Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) + print("Verwaiste Nodes: ", orphans) + quit() + expected: | + Verwaiste Nodes: 21.0 + accept: + - "Verwaiste Nodes: 21" + explanation: | + Bis Tick 120 entsteht bei jedem sechsten Tick ein `Node2D`, das sind 20, und keiner hängt im Baum. Dazu kommt der Node, der in `_initialize()` eingehängt und wieder herausgenommen wurde: Herausnehmen gibt ihn nicht frei. `get_monitor()` liefert eine Kommazahl, deshalb `21.0`. Gemessen mit Godot 4.7.2; dasselbe Verhalten zeigt der Monitor im Debugger als steigende Kurve. + resources: + - title: Godot Docs · Performance.OBJECT_ORPHAN_NODE_COUNT + url: https://docs.godotengine.org/en/stable/classes/class_performance.html#class-performance-constant-object-orphan-node-count + + - id: aufgeschoben-ergaenzen + type: gap + title: Sauber verschwinden + prompt: | + Das Pickup soll beim Einsammeln seine Form abschalten und sich danach selbst freigeben, ohne Fehlermeldung und ohne verwaiste Nodes. Ergänze die Lücken. + lang: gdscript + code: | + func _on_body_entered(body: Node2D) -> void: + if body is Player: + body.collect(value) + $Area.⟦set_deferred⟧(&"disabled", true) + ⟦queue_free()⟧ + gaps: + - label: Änderung bis nach der Physik-Abfrage verschieben + hint: Eine Methode von `Object`, die eine Eigenschaft erst am Ende des Frames setzt. + - label: Node freigeben + hint: Gibt den Node samt Kindern am Ende des Frames frei. + accept: ["self.queue_free()"] + explanation: | + Im Signal `body_entered` arbeitet der Physik-Server noch seine Abfragen ab. Eine Form direkt abzuschalten, meldet `Can't change this state while flushing queries`. `set_deferred()` setzt den Wert, wenn die Abfrage fertig ist. `queue_free()` gibt das Pickup mit `Body` und `Area` am Ende des Frames frei; gemessen mit Godot 4.7.2 blieb danach kein verwaister Node übrig. + resources: + - title: Godot Docs · Object.set_deferred + url: https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-set-deferred + - title: Godot Docs · Node.queue_free + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-queue-free + + - id: debug-collisions-starten + type: command + title: Formen sichtbar, aus dem Terminal + prompt: | + Starte das Testgelände aus dem Terminal so, dass alle Kollisionsformen sichtbar sind, wie mit *Debuggen → Sichtbare Collision-Shapes*. + context: | + Du bist im Projektordner. `godot` steht für dein Godot-Binary. + answers: + - godot --debug-collisions + - godot --path . --debug-collisions + - godot --debug-collisions --path . + explanation: | + `--debug-collisions` entspricht dem Menüpunkt und wirkt auch, wenn du das Spiel ohne Editor startest, etwa für einen Lauf mit fester Bildrate oder für den Movie Maker. Der Schalter im Menü Debuggen gilt nur für Starts aus dem Editor heraus. + resources: + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html + + - id: zaehler-bleibt-null + type: bug + title: Der Zähler bleibt bei 0 + prompt: | + Die Sammelstücke verschwinden, der Zähler im HUD bleibt bei 0. Die Ausgabe beginnt mit `ERROR: Node not found: "Count" (relative to "/root/Main/HUD").` Im Szenen-Dock heißt das Label unter `HUD` `Counter`. Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + extends CanvasLayer + + ## Zeigt, wie viele Sammelstücke die Figur hat. + + @onready var counter: Label = $Count + + + func show_total(total: int) -> void: + counter.text = str(total) + lines: [5] + fix: + - line: 5 + answers: + - "@onready var counter: Label = $Counter" + - "@onready var counter: Label = get_node(\"Counter\")" + - "@onready var counter: Label = get_node(^\"Counter\")" + - "@onready var counter := $Counter as Label" + - "@onready var counter: Label = $\"Counter\"" + explanation: | + `$Count` ist die Kurzform von `get_node("Count")` und sucht ein Kind namens `Count`. Das gibt es nicht, also bleibt `counter` `null`, und Zeile 9 scheitert später mit `Invalid assignment … on a base object of type 'null instance'`. Die Ursache steht in Zeile 5, nicht in Zeile 9. Ein `if counter:` in Zeile 9 würde die Meldung verschwinden lassen, den Zähler aber nicht reparieren. + resources: + - title: Godot Docs · Node.get_node + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-get-node + + - id: pickup-leck + type: bug + title: Mit jedem Pickup drei Nodes mehr + prompt: | + Seit einer Änderung des Agenten steigt der Monitor „Verwaiste Nodes“ mit jedem eingesammelten Pickup um 3. Während des Spiels gibt es keine Fehlermeldung, beim Beenden aber `WARNING: 6 ObjectDB instances were leaked at exit`. Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + extends Area2D + + ## Sammelstück: meldet sich bei der Figur und verschwindet. + + @export var value := 1 + + + func _on_body_entered(body: Node2D) -> void: + if body is Player: + body.collect(value) + get_parent().remove_child.call_deferred(self) + lines: [11] + fix: + - line: 11 + answers: + - queue_free() + - self.queue_free() + - call_deferred("queue_free") + - queue_free.call_deferred() + explanation: | + `remove_child()` nimmt das Pickup nur aus dem Baum. Es und seine Kinder `Body` und `Area` bleiben im Speicher, daher drei verwaiste Nodes pro Pickup. `call_deferred` verhindert nur die Fehlermeldung, die ein direktes `remove_child()` im Physik-Signal auslöst. `queue_free()` gibt den Node am Ende des Frames frei. Gemessen mit Godot 4.7.2: mit `remove_child.call_deferred` 3 verwaiste Nodes nach einem Pickup, mit `queue_free()` 0. + resources: + - title: Godot Docs · Node.queue_free + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-queue-free + + - id: profiler-lesen + type: choice + title: Wo bleibt die Zeit? + prompt: | + Der Profiler zeigt diesen Frame mit Zeit „Gesamt“. Welche Aussage stimmt? + + ![Profiler mit Messung Frame-Zeit (ms) und Zeit Gesamt; Script Functions 2.97 ms, darunter _physics_process 2.97 ms und count_tiles 2.97 ms](/media/godot/fehler-eingrenzen/profiler@2x.png "Godot 4.7.2, Profiler der Messszene") + options: + - text: "`count_tiles` braucht die Zeit; `_physics_process` zeigt dieselbe Zahl, weil Gesamt den Aufruf von `count_tiles` mitzählt." + correct: true + why: In einem zweiten Messlauf fiel `_physics_process` mit Zeit „Eigen“ auf 0,00 ms, `count_tiles` blieb bei 2,70 ms. + - text: "`_physics_process` und `count_tiles` kosten zusammen 5,94 ms." + correct: false + why: Gesamt enthält die aufgerufenen Funktionen. Die 2,97 ms von `count_tiles` stecken in den 2,97 ms von `_physics_process`, sie addieren sich nicht. + - text: Die Physik ist der Engpass, weil Physics 2D in der Liste steht. + correct: false + why: Physics 2D steht bei 0,00 ms. Die Zeit liegt in einer Skriptfunktion, die nur im Physik-Takt aufgerufen wird. + explanation: | + Gesamt (Inclusive) misst eine Funktion samt allem, was sie aufruft; Eigen (Self) nur ihren eigenen Code. Deshalb sieht der Aufrufer in Gesamt genauso teuer aus wie die langsame Funktion darin. Gemessen mit Godot 4.7.2 in einem zweiten Lauf: Gesamt `_physics_process` 2,71 ms und `count_tiles` 2,70 ms, Eigen `_physics_process` 0,00 ms und `count_tiles` 2,70 ms. Erst mit Eigen weißt du, welchen Code du dir ansehen musst. + resources: + - title: Godot Docs · The Profiler · Scope of measurement + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html#scope-of-measurement-and-measurement-windows + + - id: stacktrace-erklaeren + type: explain + title: Was der angehaltene Debugger verrät + prompt: | + Das Spiel ist bei diesem Fehler angehalten. Erkläre in wenigen Sätzen, was der Stacktrace zeigt und wie du von hier zur Ursache kommst. + + ![Debugger, Reiter Stacktrace: Stack Frames 0 res://hud.gd:9, 1 res://player.gd:28, 2 res://pickup.gd:10; Locals total 1; Members self CanvasLayer, counter null](/media/godot/fehler-eingrenzen/stacktrace@2x.png "Godot 4.7.2, angehalten bei Invalid assignment … 'null instance' in hud.gd:9") + lang: text + code: | + SCRIPT ERROR: Invalid assignment of property or key 'text' + with value of type 'String' on a base object of type 'null instance'. + points: + - Frame 0 ist die Zeile, an der es scheitert (`hud.gd:9`); Frame 1 und 2 sind die Aufrufer über `collect()` und das Pickup-Signal. + - Der Wert kam an (`total` ist 1), aber `counter` ist ``; das Problem ist das Label, nicht die Zählung. + - "`counter` wird mit `@onready` aus einem Pfad belegt; die Ursache liegt also beim Pfad in der Variablendeklaration, nicht in Zeile 9." + - Als Nächstes prüfst du den Pfad gegen das Szenen-Dock oder liest die erste Meldung im Reiter Fehler. + explanation: | + Der Stacktrace beantwortet zwei Fragen: Wer hat wen aufgerufen, und welche Werte lagen vor. Frame 0 zeigt die Zeile mit dem Fehler, darunter den Weg dorthin: Das Pickup meldete den Körper, `collect()` sendete `collected`, `show_total()` scheiterte. Unter Locals steht `total` mit 1, also stimmt die Zählung. Unter Members steht `counter` mit ``. Weil `counter` per `@onready` aus `$Count` belegt wird, führt der Weg zur Deklaration in Zeile 5; der Reiter Fehler bestätigt das mit `Node not found: "Count"`. + resources: + - title: Godot Docs · Debugger panel · Stack Trace + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html#stack-trace + + - id: fehlerbericht + type: practice + title: Ein Fehlerbericht, den man nachstellen kann + prompt: | + Nimm einen Fehler, der dir in deinem Projekt oder im Testgelände begegnet ist, oder das Beispiel mit `$Count` aus der Lektion, und schreib in vier bis fünf Sätzen einen Bericht, mit dem ein anderer ihn ohne Rückfrage nachstellen kann, ob Mensch oder Coding-Agent. + lang: prose + checklist: + - Die Schritte zum Reproduzieren stehen in der Reihenfolge, in der man sie ausführt. + - Erwartetes und tatsächliches Verhalten stehen getrennt. + - Die erste Fehlermeldung ist zitiert, unterwegs aus der Lektion oder deinen Notizen, mit `[0]` aus dem Backtrace, falls vorhanden. + - Godot-Version und Commit oder Stand des Projekts sind genannt. + - Du hast notiert, was du schon ausgeschlossen hast und wie. + hints: + - Die Godot-Version steht unten rechts im Editor und in der ersten Zeile der Ausgabe. + - Ein guter Bericht enthält keine Vermutung als Tatsache; schreib Vermutungen als solche dazu. + explanation: | + Ein Fehlerbericht ist die Reproduktion in Textform. Mit Schritten, Erwartung, Meldung und Stand kann jemand anderes den Fehler auslösen, ohne dich zu fragen, und eine Korrektur mit derselben Probe prüfen. Für einen Coding-Agenten gilt das genauso: Je genauer der Bericht, desto kleiner und prüfbarer die Änderung, die zurückkommt. + resources: + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html diff --git a/content/godot/modules/instanzen-ressourcen.yaml b/content/godot/modules/instanzen-ressourcen.yaml new file mode 100644 index 0000000..8d5b98a --- /dev/null +++ b/content/godot/modules/instanzen-ressourcen.yaml @@ -0,0 +1,517 @@ +id: instanzen-ressourcen +title: Instanzen, Overrides und geteilte Ressourcen +level: 2 +minutes: 120 +summary: Eine Instanz speichert nur, worin sie von ihrer Vorlage abweicht, und Ressourcen werden geteilt statt kopiert. Wer beides versteht, sagt vorher, welcher Wert gilt und welche Objekte sich mitändern. +place: unterwegs +goals: + - Du erklärst, was in der Eltern-Szene steht, wenn du eine Instanz änderst, und welcher Wert aus Skript, Vorlage und Instanz gilt. + - Du unterscheidest Bearbeitbare Kinder, Lokal machen und geerbte Szenen und erkennst sie in der `.tscn`. + - Du sagst vorher, welche Objekte sich ändern, wenn eine geteilte Ressource geändert wird. + - Du trennst geteilte Ressourcen mit Einzigartig machen, `resource_local_to_scene` oder `duplicate()` und wählst den passenden Weg. + - Du liest eine eigene Ressource wie `MovementStats` als Werte-Container, den mehrere Nodes teilen. +resources: + - title: Godot Docs · Creating instances + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/instancing.html + - title: Godot Docs · Resources + url: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html + - title: Godot Docs · Resource (Klassenreferenz) + url: https://docs.godotengine.org/en/stable/classes/class_resource.html + - title: Godot Docs · Inspector Dock + url: https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html +lesson: | + Du willst eine zweite Fußform für die Figur und duplizierst im Szenen-Dock den Node `Feet`. Dann ziehst du die neue Form breiter, und die alte wird mit breiter. An einem anderen Tag zählt `Pickup2` drei Punkte, obwohl in `pickup.gd` und in `pickup.tscn` etwas anderes steht. Beides folgt aus zwei Regeln. Eine Instanz speichert nur, worin sie von ihrer Vorlage abweicht. Und Ressourcen werden geteilt, nicht kopiert, bis du ausdrücklich eine Kopie verlangst. + + Die Beispiele stammen aus dem Testgelände: `pickup.tscn` ist dreimal in `main.tscn` eingesetzt, `player.tscn` einmal. Wie du Bäume und `.tscn`-Dateien liest, steht in [Szenenbäume lesen](/godot/szenenbaum). Alle Ausgaben stammen aus Läufen mit Godot 4.7.2. + + ## Szene als Vorlage, Instanz als Verwendung + + `pickup.tscn` beschreibt ein Sammelstück: ein `Area2D` mit Skript, einen `Polygon2D` als Körper und eine kreisförmige Kollisionsform. `main.tscn` setzt diese Szene dreimal ein. Jede Verwendung heißt **Instanz**. Änderst du die Vorlage und speicherst, sehen alle Instanzen die Änderung. Änderst du eine Instanz im Inspektor, gilt das nur für sie ([Creating instances](https://docs.godotengine.org/en/stable/getting_started/step_by_step/instancing.html#editing-scenes-and-instances)). + + Aus dem Web kennst du das: Eine Komponente wird einmal definiert und mehrfach verwendet, mit eigenen Props je Verwendung. Der Unterschied: In Godot ist die Abweichung ein gespeicherter Wert in der Eltern-Szene, kein Argument, das zur Laufzeit übergeben wird. Und du kannst mit Bearbeitbare Kinder sogar in die Kinder einer Instanz hineingreifen. In einer Komponentenbibliothek wäre das ein Bruch der Kapselung. + + ## Overrides: nur die Abweichung steht in der Eltern-Szene + + Setzt du im Inspektor `Value` von `Pickup2` auf 3, erscheint daneben der Zurücksetzen-Pfeil: + + ![Inspektor von Pickup2 in main.tscn: Value 3 mit Zurücksetzen-Pfeil, unter Transform die Position −70, 30 ebenfalls mit Pfeil](/media/godot/instanzen-ressourcen/instanz-override@2x.png "Godot 4.7.2, Inspektor der Instanz Pickup2 in main.tscn") + + Bei einer Instanz heißt der Pfeil: Dieser Wert weicht von der Vorlage ab. `Position` trägt ihn auch, denn in `pickup.tscn` steht das Sammelstück bei (0, 0). In `main.tscn` speichert der Editor genau diese zwei Abweichungen, sonst nichts: + + ```ini title=main.tscn + [node name="Pickup2" parent="Objects" unique_id=969255857 instance=ExtResource("3")] + position = Vector2(-70, 30) + value = 3 + ``` + + Ein solcher Wert heißt **Override**. Ein Klick auf den Pfeil setzt ihn auf den Wert der Vorlage zurück, und beim Speichern verschwindet die Zeile. Laut Doku überschreibt eine Änderung an der Instanz immer den Wert der Vorlage ([Creating instances](https://docs.godotengine.org/en/stable/getting_started/step_by_step/instancing.html#editing-scenes-and-instances)). + + **Welcher Wert gilt?** Ein Wert kann an drei Stellen gespeichert sein. In diesem Versuch steht in `pickup.gd` `@export var value := 1`, in `pickup.tscn` steht `value = 5` am Wurzel-Node, und `main.tscn` hat für `Pickup2` den Override `value = 3`: + + ```gdscript + for n in ["Pickup", "Pickup2", "Pickup3"]: + print(n, " ", main.get_node("Objects/" + n).value) + ``` + + ```text + Pickup 5 + Pickup2 3 + Pickup3 5 + ``` + + 1. **Standard im Skript** (`:= 1`): gilt für alles, was den Wert nirgends speichert. + 2. **Wert in der Vorlage** `pickup.tscn` (`value = 5`): gilt für alle Instanzen ohne eigenen Override. + 3. **Override in `main.tscn`** (`value = 3`): gilt nur für `Pickup2`. + 4. **Zuweisung im Code**, etwa in `_ready()`: gilt ab diesem Moment, nach dem Laden. + + Jede Stufe überschreibt die vorige. Für dich als Abstimmenden heißt das: Änderst du die Vorlage, erreicht die Änderung nur Instanzen ohne eigenen Override für diese Eigenschaft. Willst du wissen, wo ein Wert herkommt, folg den Pfeilen: Instanz, dann Vorlage, dann Skript. + + > [!WARNING] Zeilenreihenfolge + > Eigenschaften eines Skripts gelten erst, wenn das Skript am Node hängt. Mit 4.7.2 geprüft: Steht in `pickup.tscn` die Zeile `value = 5` über `script = ExtResource("1")`, ignoriert Godot sie ohne Meldung, und alle Sammelstücke zählen 1. Der Editor schreibt `script` immer vor die Skript-Variablen; das Problem entsteht nur, wenn jemand die Datei von Hand bearbeitet. + + ## Bearbeitbare Kinder, Lokal machen und geerbte Szenen + + Ein Rechtsklick auf eine Instanz im Szenen-Dock bietet zwei Wege an, über die Vorlage hinauszugehen: *Bearbeitbare Kinder* und *Lokal machen*. Den dritten, die geerbte Szene, findest du im Menü *Szene*. + + ![Kontextmenü einer Instanz im Szenen-Dock: Branch als Szene speichern, Lokal machen, Bearbeitbare Kinder, Als Platzhalter laden, Node-Pfad kopieren, Über einzigartigen Namen zugreifen, Im Editor öffnen](/media/godot/instanzen-ressourcen/kontextmenue-instanz@2x.png "Godot 4.7.2, Rechtsklick auf Pickup2 im Szenen-Dock") + + **Bearbeitbare Kinder** (Editable Children) klappt die Instanz im Szenen-Dock auf. Ihre Kinder erscheinen hervorgehoben, und du kannst ihre Eigenschaften überschreiben. Die Verbindung zur Vorlage bleibt. In `main.tscn` steht danach am Ende `[editable path="Objects/Pickup2"]`, und jede Änderung an einem Kind als eigener Abschnitt wie `[node name="Area" parent="Objects/Pickup2" index="1"]`. + + ![Szenen-Dock mit aufgeklapptem Pickup2: darunter Body und Area farbig hervorgehoben, Area ist gewählt](/media/godot/instanzen-ressourcen/bearbeitbare-kinder@2x.png "Godot 4.7.2, Pickup2 mit Bearbeitbare Kinder") + + **Lokal machen** (Make Local) löst die Instanz auf. Mit 4.7.2 nachgestellt an `Pickup3`: `main.tscn` enthält danach `Pickup3` als gewöhnliches `Area2D` mit allen Eigenschaften, seine Kinder, eine eigene Kopie der Kreisform als `[sub_resource]` und eine eigene `[connection]` für `body_entered`. Spätere Änderungen an `pickup.tscn` erreichen `Pickup3` nicht mehr. + + **Geerbte Szene** (Inherited Scene): *Szene → Neue geerbte Szene …* (Strg + Umschalt + N) legt eine neue Szene an, deren Wurzel eine Instanz der Basis ist. Das Ergebnis, mit `value = 5` gespeichert als `pickup_big.tscn`: + + ```ini + [gd_scene format=3 uid="uid://cdm4ofg4ec7r5"] + + [ext_resource type="PackedScene" uid="uid://cjjikuxf00l3x" path="res://pickup.tscn" id="1_3j0kj"] + + [node name="Pickup" unique_id=1372740206 instance=ExtResource("1_3j0kj")] + value = 5 + ``` + + Die Wurzel hat kein `type`, sondern `instance=`. Alles andere erbt sie von `pickup.tscn`, und Änderungen an der Basis kommen weiter an. Das ähnelt einer Unterklasse: Basis plus eigene Abweichungen. + + - **Override an der Instanz**: Die Verbindung zur Vorlage bleibt. Anlass: Eine Verwendung braucht einen anderen Wert. + - **Bearbeitbare Kinder**: Die Verbindung bleibt. Anlass: Eine Verwendung braucht eine Änderung an einem Kind. + - **Geerbte Szene**: Die Verbindung bleibt. Anlass: eine Variante, die du an mehreren Stellen einsetzt. + - **Lokal machen**: Die Verbindung ist weg. Anlass: ein Einzelstück, das sich von der Vorlage lösen soll. + + Welche Varianten dein Spiel braucht, entscheidest du. Wichtig ist, dass du in einer fremden Szene erkennst, welcher Weg gewählt wurde: Override-Zeilen unter `instance=`, `[editable path=…]`, eine Wurzel mit `instance=` oder ein Node ohne Instanz, der aussieht wie eine Kopie. + + ## Ressourcen: Daten, die geteilt werden + + Eine **Ressource** ist ein Datenobjekt: eine Textur, ein Ton, eine Kollisionsform, eine Animation oder eine eigene Klasse wie `MovementStats`. Nodes halten Verweise auf Ressourcen. Lädt Godot eine Ressource von der Festplatte, tut es das nur einmal; jeder weitere `load()` liefert dasselbe Objekt ([Resources](https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html)). Eine Ressource steht entweder in einer eigenen Datei (`.tres`, binär `.res`) oder steckt als `[sub_resource]` in der Szene. Die Kreisform des Sammelstücks steckt in `pickup.tscn`. + + Alle Instanzen einer Szene teilen ihre Unter-Ressourcen. Mit 4.7.2 gemessen, im laufenden Testgelände: + + ```gdscript + var a: CollisionShape2D = main.get_node("Objects/Pickup/Area") + var b: CollisionShape2D = main.get_node("Objects/Pickup2/Area") + print(a.shape == b.shape) + a.shape.radius = 10.0 + print(b.shape.radius) + ``` + + ```text + true + 10.0 + ``` + + Für JavaScript-Entwickler ist das vertraut: `const b = a; b.radius = 10` ändert auch `a.radius`, weil beide auf dasselbe Objekt zeigen. Eine Kopie verlangst du ausdrücklich, dort etwa mit `{ ...a }`, in Godot mit `duplicate()`. Beide kopieren flach: Was im Objekt steckt, bleibt geteilt. + + **Im Editor** zeigt Godot seit 4.6 ein Kettensymbol vor Ressourcen, die nicht nur diesem Node gehören. Der Tooltip sagt, warum: „… wird in 2 Orten verwendet“ heißt geteilt, „… ist extern für die Szene“ heißt, sie kommt aus einer eigenen Datei oder aus der Vorlage einer Instanz. Duplizierst du einen Node mit einer Form, teilen sich Original und Kopie dieselbe Form; das beschreibt auch die Doku ([Inspector Dock](https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html)). Nachgestellt in `pickup.tscn` mit `Area` und Strg + D: + + ![Inspektor von Area2, der Kopie von Area: Im Feld Shape steht ein Kettensymbol vor „Circle“](/media/godot/instanzen-ressourcen/geteilte-form@2x.png "Godot 4.7.2, Tooltip des Kettensymbols: „CircleShape2D wird in 2 Orten verwendet. Linksklick, um es einzigartig zu machen.“") + + Klappst du dagegen mit Bearbeitbare Kinder die Form einer Instanz auf, gehört sie der Vorlage. Der Inspektor meldet „Dieses Objekt ist nur lesbar.“, der Tooltip des Kettensymbols nennt sie „extern für die Szene“. Ändern kannst du sie erst, wenn du sie für diese Instanz einzigartig machst. + + ## Einzigartig machen, `resource_local_to_scene` und `duplicate()` + + Drei Wege trennen eine geteilte Ressource: + + - **Einzigartig machen** (Make Unique): ein Klick auf das Kettensymbol oder der Eintrag im Menü des Ressourcenfelds. Der Node bekommt eine eigene Kopie, im Editor, beim Bearbeiten. Mit 4.7.2 an `Pickup2/Area` nachgestellt, steht danach in `main.tscn` eine neue `[sub_resource type="CircleShape2D" id="CircleShape2D_1bvp3"]` und ein Abschnitt `[node name="Area" parent="Objects/Pickup2" index="1"]` mit `shape = SubResource("CircleShape2D_1bvp3")`. Das Werkzeugmenü des Inspektors hat dazu *Unter-Ressourcen einzigartig machen* für alle Ressourcen eines Nodes. + - **`resource_local_to_scene`**: eine Eigenschaft der Ressource, im Inspektor unter *Resource*. Ist sie an, bekommt jede Instanz der Szene beim Erzeugen ihre eigene Kopie ([resource_local_to_scene](https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-property-resource-local-to-scene)). In der Datei steht dann `resource_local_to_scene = true` unter der `[sub_resource]`. Mit 4.7.2 geprüft: Die Formen der drei Sammelstücke sind dann drei Objekte, und ein Radius von 10 an `Pickup` lässt `Pickup2` bei 5. + - **`duplicate()`** im Code, zur Laufzeit: `$Area.shape = $Area.shape.duplicate()` gibt diesem Node eine Kopie, bevor er sie ändert ([Resource.duplicate](https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-method-duplicate)). + + > [!DEEP] Tiefe Kopien + > `duplicate()` kopiert die Ressource, verschachtelte Ressourcen darin bleiben geteilt. `duplicate(true)` kopiert seit Godot 4.5 nur Ressourcen, die in derselben Datei stecken; wer auch verwiesene Dateien kopieren will, nimmt `duplicate_deep(Resource.DEEP_DUPLICATE_ALL)` ([Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html#core)). Für eine Kollisionsform ohne verschachtelte Ressourcen reicht `duplicate()`. + + Welcher Weg passt, hängt davon ab, wann die Trennung gebraucht wird. Soll eine Instanz dauerhaft anders aussehen, machst du die Ressource im Editor einzigartig. Ändert der Code jede Instanz einzeln zur Laufzeit, passt `resource_local_to_scene` oder `duplicate()` vor der Änderung. + + ## Eigene Ressource als Werte-Container: `MovementStats` + + Geteilte Ressourcen sind kein Unfall, sondern oft genau das Ziel. Das Testgelände legt die Bewegungswerte der Figur in eine eigene Ressourcenklasse, `movement_stats.gd` ([Creating your own resources](https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html#creating-your-own-resources)): + + ```gdscript + class_name MovementStats + extends Resource + + ## Bewegungswerte der Figur. Platzhalter: Die echten Werte bestimmst du. + + @export_range(0, 400, 1, "suffix:px/s") var max_speed := 90.0 + @export_range(0, 5000, 10, "suffix:px/s²") var acceleration := 600.0 + @export_range(0, 5000, 10, "suffix:px/s²") var friction := 900.0 + ``` + + Die Werte liegen in `data/player_stats.tres`, und `player.tscn` verweist mit `stats = ExtResource("2")` darauf. Mit 4.7.2 geprüft: `stats` der Figur ist dasselbe Objekt, das `load("res://data/player_stats.tres")` liefert. Setzt du später mehrere Figuren desselben Typs ein, teilen sie diese Werte, und eine Änderung im Inspektor gilt für alle. Für einen zweiten Typ legst du eine zweite `.tres` mit demselben Skript an. Willst du zur Laufzeit nur eine Figur verändern, gib ihr vorher mit `duplicate()` eine eigene Kopie. + + Warum eine eigene Ressource statt `@export`-Variablen direkt in `player.gd`? Regler und einzeilige Diffs gäbe es dort auch. Der Unterschied: Die Werte stehen in einer eigenen Datei, getrennt von Code und Szene. Mehrere Szenen können dieselbe Datei nutzen, und eine Szene bekommt andere Werte, ohne dass sich ihr Code ändert: Du ziehst eine zweite `.tres` in das Feld `Stats`. Wie du sie abstimmst, behandelt das [Modul zu Eingabeaktionen und Parametern](/godot/aktionen-parameter), die Methode dazu das [Tuning-Modul im Combat-Kurs](/combat/tuning). + + > [!PRAXIS] + > Wenn ein Agent einen neuen Gegnertyp anlegt, frag nach der Datenhaltung: Welche Werte teilen alle Gegner dieses Typs (eine `.tres`), welche weichen je Platzierung ab (Override an der Instanz), und welche ändern sich im Spiel (Variable im Skript, keine Ressource)? Prüf danach die Kettensymbole im Inspektor und lies ihre Tooltips: Sie sagen, ob eine Ressource geteilt wird oder aus einer Datei kommt. + + > [!INTERVIEW] + > „Ein Designer ändert die Lebenspunkte eines einzelnen Gegners im Level, und plötzlich haben alle Gegner im Spiel den neuen Wert. Was ist passiert?“ Gute Antwort: Die Werte liegen in einer Ressource, die alle Gegner teilen, und er hat die Ressource geändert statt der Instanz. Ich zeige ihm das Kettensymbol im Inspektor. Soll nur dieser Gegner abweichen, macht er die Ressource für ihn einzigartig oder setzt einen Override auf eine Eigenschaft der Instanz. Soll sie sich zur Laufzeit je Gegner ändern, braucht der Code `duplicate()` oder `resource_local_to_scene`. + + ## Häufige Fehler + + - **Node dupliziert, Form geteilt.** Original und Kopie ändern sich gemeinsam. Kettensymbol prüfen, Form einzigartig machen. + - **Geteilte Ressource im Code geändert.** Ein Sammelstück wächst, alle wachsen mit. Erst `duplicate()`, dann ändern. + - **Vorlage geändert, eine Instanz bleibt anders.** Sie hat einen Override für diese Eigenschaft. Der Pfeil im Inspektor zeigt ihn, ein Klick entfernt ihn. + - **Standard im Skript geändert, nichts passiert.** Vorlage oder Instanz speichern einen eigenen Wert, der den Standard überschreibt. + - **Lokal gemacht und vergessen.** Die Kopie folgt der Vorlage nicht mehr. Im Szenen-Dock fehlt die Filmklappe. + - **Skript-Variable vor `script =` geschrieben.** In von Hand bearbeiteten Szenen wird der Wert ohne Meldung ignoriert. + - **Gemeinsame `.tres` für zwei Typen.** Die Werte des einen ändern den anderen. Jeder Typ bekommt seine eigene Datei. + + ## Zusammenfassung + + - Eine Szene ist eine Vorlage, jede Verwendung eine Instanz. Änderungen an der Vorlage erreichen alle Instanzen ohne eigenen Override. + - Die Eltern-Szene speichert nur die Abweichungen einer Instanz. Der Zurücksetzen-Pfeil zeigt sie im Inspektor. + - Es gilt: Skript-Standard, dann Vorlage, dann Override, dann Code zur Laufzeit. + - Bearbeitbare Kinder und geerbte Szenen behalten die Verbindung zur Vorlage, Lokal machen kappt sie. + - Ressourcen sind geteilte Datenobjekte. Alle Instanzen einer Szene teilen ihre Unter-Ressourcen, ein duplizierter Node teilt seine Formen mit dem Original. + - Getrennt wird mit Einzigartig machen im Editor, `resource_local_to_scene` je Instanz oder `duplicate()` im Code. + - Eine eigene Ressource wie `MovementStats` bündelt Werte, die mehrere Nodes absichtlich teilen. +exercises: + - id: wo-steht-der-override + type: choice + title: Wo landet die Änderung? + prompt: | + Du wählst in `main.tscn` die Instanz `Pickup2` und setzt im Inspektor `Value` von 1 auf 3. Dann speicherst du. Welche Aussagen stimmen? + options: + - text: In `main.tscn` steht unter dem Abschnitt von `Pickup2` die Zeile `value = 3`. + correct: true + why: Die Eltern-Szene speichert die Abweichungen ihrer Instanzen als Overrides. + - text: "`pickup.tscn` und `pickup.gd` bleiben unverändert." + correct: true + why: Die Änderung gilt nur für diese Instanz, die Vorlage und das Skript fasst der Editor nicht an. + - text: Auch `Pickup` und `Pickup3` zählen jetzt 3. + correct: false + why: Overrides gelten nur für die Instanz, an der sie stehen. Die anderen behalten den Wert der Vorlage. + - text: Neben `Value` erscheint kein Pfeil, weil 3 ein gültiger Wert ist. + correct: false + why: Der Pfeil zeigt jede Abweichung von der Vorlage an, egal ob der Wert gültig ist. + explanation: | + Eine Instanz speichert ihre Abweichungen in der Szene, in der sie steht. Deshalb ist `main.tscn` die einzige geänderte Datei, mit einer neuen Zeile unter `Pickup2`. Der Zurücksetzen-Pfeil im Inspektor macht den Override sichtbar, ein Klick darauf entfernt ihn wieder. + resources: + - title: Godot Docs · Creating instances · Editing scenes and instances + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/instancing.html#editing-scenes-and-instances + + - id: nur-lesbar-deuten + type: choice + title: Was sagt dir dieser Inspektor? + prompt: | + Du hast an `Pickup2` *Bearbeitbare Kinder* eingeschaltet, `Area` gewählt und die Form angeklickt, um ihren Radius zu ändern. Der Inspektor zeigt das: + + ![Inspektor einer CircleShape2D mit dem Hinweis „Dieses Objekt ist nur lesbar.“; Radius 5.0 px ist ausgegraut](/media/godot/instanzen-ressourcen/nur-lesbar@2x.png "Godot 4.7.2, Form von Pickup2/Area mit Bearbeitbare Kinder") + + Was folgt daraus? + options: + - text: Die Form gehört der Vorlage `pickup.tscn`. Um sie nur für `Pickup2` zu ändern, machst du sie zuerst einzigartig. + correct: true + why: Der Tooltip des Kettensymbols nennt sie „extern für die Szene“. Einzigartig machen legt eine eigene Kopie in `main.tscn` an. + - text: Die Form ist kaputt und muss neu angelegt werden. + correct: false + why: Sie funktioniert. Sie gehört nur nicht der Szene, die du gerade bearbeitest. + - text: Du musst `pickup.tscn` als Text bearbeiten, um den Radius nur für `Pickup2` zu ändern. + correct: false + why: Eine Änderung in `pickup.tscn` träfe alle drei Sammelstücke, nicht nur `Pickup2`. + explanation: | + Die Unter-Ressourcen einer Instanz gehören der Vorlage. Würde der Editor sie hier ändern lassen, träfe es alle Instanzen, und gespeichert würde die Änderung trotzdem nirgends, weil `main.tscn` die Form nicht besitzt. Deshalb sperrt er sie. Einzigartig machen erzeugt eine eigene Form für diese Instanz; danach steht sie als `[sub_resource]` in `main.tscn`. Gesehen mit Godot 4.7.2. + resources: + - title: Godot Docs · Inspector Dock + url: https://docs.godotengine.org/en/stable/tutorials/editor/inspector_dock.html + + - id: wer-gewinnt + type: order + title: Wer hat das letzte Wort? + prompt: | + Ordne die Stellen, an denen `value` eines Sammelstücks gesetzt werden kann, vom schwächsten zum stärksten. Die unterste gewinnt. + lang: prose + lines: | + Standardwert im Skript, `@export var value := 1` + Wert am Wurzel-Node der Vorlage `pickup.tscn` + Override an der Instanz in `main.tscn` + Zuweisung im Code in `_ready()` + explanation: | + Beim Laden setzt Godot zuerst den Standard aus dem Skript, dann die Werte der Vorlage, dann die Overrides der Eltern-Szene. Danach läuft `_ready()`, und eine Zuweisung dort überschreibt alles Gespeicherte. Mit 4.7.2 geprüft: Standard 1, Vorlage 5, Override 3 an `Pickup2` und `value = 7` in `_ready()` von `Pickup3` ergeben 5, 3 und 7. + resources: + - title: Godot Docs · Creating instances + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/instancing.html#editing-scenes-and-instances + + - id: werte-vorhersagen + type: output + title: Drei Sammelstücke, drei Quellen + prompt: | + `pickup.gd` hat `@export var value := 1`. In `pickup.tscn` steht am Wurzel-Node `value = 6` (nach der `script`-Zeile). In `main.tscn` hat `Pickup` den Override `value = 1` und `Pickup3` den Override `value = 2`; `Pickup2` hat keinen. Was gibt die Schleife aus? + lang: gdscript + code: | + for n in ["Pickup", "Pickup2", "Pickup3"]: + print(n, " ", main.get_node("Objects/" + n).value) + expected: | + Pickup 1 + Pickup2 6 + Pickup3 2 + hints: + - Wer keinen eigenen Override hat, nimmt den Wert der Vorlage. + - Ein Override darf denselben Wert haben wie der Standard im Skript. + explanation: | + `Pickup2` hat keinen Override und nimmt die 6 aus der Vorlage; der Standard 1 aus dem Skript kommt nicht zum Zug, weil die Vorlage einen Wert speichert. `Pickup` und `Pickup3` haben Overrides. Der Override 1 an `Pickup` ist gleich dem Skript-Standard und trotzdem nötig: Ohne ihn gälte die 6 der Vorlage. Mit 4.7.2 geprüft, auch dass der Editor diesen Override als `value = 1` in `main.tscn` speichert. + resources: + - title: Godot Docs · Creating instances + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/instancing.html + + - id: geteilte-form-vorhersagen + type: output + title: Ein Radius für alle? + prompt: | + Im laufenden Testgelände führt ein Skript diese Zeilen aus. In `pickup.tscn` hat die Kreisform den Radius 5. Was steht in der Ausgabe? + lang: gdscript + code: | + var a: CollisionShape2D = main.get_node("Objects/Pickup/Area") + var b: CollisionShape2D = main.get_node("Objects/Pickup2/Area") + var c: CollisionShape2D = main.get_node("Objects/Pickup3/Area") + a.shape.radius = 10.0 + print(b.shape.radius) + c.shape = c.shape.duplicate() + c.shape.radius = 3.0 + print(a.shape.radius, " ", c.shape.radius) + expected: | + 10.0 + 10.0 3.0 + hints: + - Wie viele Kreisformen gibt es nach dem Laden, eine oder drei? + - "`duplicate()` gibt nur `c` eine eigene Form." + explanation: | + Alle drei Instanzen teilen die eine Kreisform aus `pickup.tscn`. Der Radius 10 an `a` ist deshalb auch der von `b`. Danach bekommt `c` eine eigene Kopie, und ihr Radius 3 ändert nur `c`; `a` und `b` bleiben bei 10. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Resource.duplicate + url: https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-method-duplicate + + - id: lokal-zur-szene-vorhersagen + type: output + title: Eine Zeile mehr in der Vorlage + prompt: | + Jetzt steht in `pickup.tscn` unter der Kreisform `resource_local_to_scene = true`. Was gibt dasselbe Testskript aus? + + ```ini + [sub_resource type="CircleShape2D" id="area"] + resource_local_to_scene = true + radius = 5.0 + ``` + lang: gdscript + code: | + var a: CollisionShape2D = main.get_node("Objects/Pickup/Area") + var b: CollisionShape2D = main.get_node("Objects/Pickup2/Area") + print(a.shape == b.shape) + a.shape.radius = 10.0 + print(b.shape.radius) + expected: | + false + 5.0 + hints: + - "`resource_local_to_scene` kopiert die Ressource für jede Instanz beim Erzeugen." + explanation: | + Mit `resource_local_to_scene` legt Godot beim Instanziieren für jede Instanz eine eigene Kopie an. Die beiden Formen sind verschiedene Objekte, `==` vergleicht hier die Identität und liefert `false`. Der Radius von `b` bleibt bei 5. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Resource.resource_local_to_scene + url: https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-property-resource-local-to-scene + + - id: override-zeilen + type: gap + title: Override und bearbeitbare Kinder im Text + prompt: | + Ergänze `main.tscn` so, dass `Pickup2` 3 Punkte zählt und seine Kinder im Editor bearbeitbar sind. + lang: ini + code: | + [node name="Pickup2" parent="Objects" instance=ExtResource("3")] + position = Vector2(-70, 30) + ⟦value⟧ = ⟦3⟧ + + [⟦editable⟧ path="⟦Objects/Pickup2⟧"] + gaps: + - label: Eigenschaft + hint: Der Name der Variablen in `pickup.gd`. + - label: Wert + hint: Die Punktzahl dieser Instanz. + - label: Art des Abschnitts + hint: So heißt der Abschnitt, den Bearbeitbare Kinder schreibt. + - label: Pfad der Instanz + hint: Ab der Wurzel, ohne deren Namen. + explanation: | + Ein Override ist eine gewöhnliche Eigenschaftszeile unter dem Abschnitt der Instanz, mit dem Namen aus dem Skript. Bearbeitbare Kinder schreibt einen eigenen Abschnitt `[editable path="…"]` am Ende der Datei, mit dem Pfad der Instanz nach derselben Regel wie `parent`. So hat es der Editor in 4.7.2 gespeichert. + resources: + - title: Godot Docs · TSCN file format · The scene tree + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#the-scene-tree + + - id: kopie-im-code + type: gap + title: Erst kopieren, dann ändern + prompt: | + Ergänze die Lücken: Das Sammelstück soll eine eigene Form bekommen, bevor es sie vergrößert, und die Figur soll ihre Werte aus einer Ressource der Klasse beziehen, die `movement_stats.gd` mit `class_name` registriert. + lang: gdscript + code: | + # pickup.gd + func grow() -> void: + $Area.shape = $Area.shape.⟦duplicate()⟧ + $Area.shape.radius = 9.0 + + # player.gd + @export var stats: ⟦MovementStats⟧ + gaps: + - label: Kopie anlegen + hint: Die Methode von `Resource`, die eine Kopie liefert. + accept: + - duplicate(false) + - duplicate(true) + - label: Typ der Ressource + hint: Der `class_name` aus `movement_stats.gd`. + explanation: | + `duplicate()` liefert eine neue Form mit denselben Werten; erst nach der Zuweisung gehört sie diesem Node allein, und der neue Radius trifft die anderen Sammelstücke nicht. `@export var stats: MovementStats` macht ein Ressourcenfeld im Inspektor, das nur Ressourcen dieser Klasse annimmt. `class_name` macht die Klasse im ganzen Projekt unter diesem Namen bekannt. + resources: + - title: Godot Docs · Resource.duplicate + url: https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-method-duplicate + - title: Godot Docs · Creating your own resources + url: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html#creating-your-own-resources + + - id: alle-werden-gross + type: bug + title: Ein großes Sammelstück, drei große Sammelstücke + prompt: | + Ein Agent hat `pickup.gd` um ein großes Sammelstück erweitert. Nur `Pickup3` hat im Inspektor `Big` an. Trotzdem haben alle drei Sammelstücke den Radius 9. Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + @export var value := 1 + @export var big := false + + + func _ready() -> void: + if big: + var shape: CircleShape2D = $Area.shape + shape.radius = 9.0 + $Area.shape = shape + lines: [7] + fix: + - line: 7 + answers: + - "var shape: CircleShape2D = $Area.shape.duplicate()" + - "var shape := $Area.shape.duplicate() as CircleShape2D" + - "var shape: CircleShape2D = $Area.shape.duplicate(true)" + - "var shape: CircleShape2D = get_node(\"Area\").shape.duplicate()" + - "var shape = $Area.shape.duplicate()" + - "var shape: CircleShape2D = $Area.shape.duplicate(false)" + explanation: | + `var shape = $Area.shape` holt keine Kopie, sondern einen zweiten Verweis auf dieselbe Form, die alle Instanzen teilen. Das Zurückschreiben in der letzten Zeile ändert daran nichts. Erst `duplicate()` erzeugt eine eigene Form für dieses Sammelstück. Mit 4.7.2 gemessen: ohne Korrektur Radius 9, 9 und 9, mit Korrektur 5, 5 und 9. + resources: + - title: Godot Docs · Resource.duplicate + url: https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-method-duplicate + + - id: wert-wird-ignoriert + type: bug + title: Der Wert steht da und wirkt nicht + prompt: | + Ein Agent hat in `pickup.tscn` von Hand eingetragen, dass ein Sammelstück 5 Punkte zählt. Das Spiel startet ohne Meldung, aber jedes Sammelstück zählt 1. In `main.tscn` gibt es keine Overrides für `value`. Finde die Zeile, die nicht wirkt. + lang: ini + code: | + [node name="Pickup" type="Area2D"] + collision_layer = 4 + collision_mask = 2 + value = 5 + monitorable = false + script = ExtResource("1") + + [node name="Body" type="Polygon2D" parent="."] + lines: [4] + hints: + - "`value` ist keine Eigenschaft von `Area2D`. Woher kommt sie?" + explanation: | + `value` gibt es erst, wenn `pickup.gd` am Node hängt, und das passiert in Zeile 6. Godot setzt die Eigenschaften in der Reihenfolge der Datei; in Zeile 4 hat der Node noch kein Skript, und der Wert verpufft ohne Meldung. Gehört `value = 5` unter `script = ExtResource("1")`, zählt jedes Sammelstück 5. Der Editor schreibt die Reihenfolge immer richtig, der Fehler entsteht nur beim Bearbeiten von Hand. Mit 4.7.2 nachgestellt. + resources: + - title: Godot Docs · TSCN file format · Entries inside the file + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#entries-inside-the-file + + - id: stats-geteilt + type: bug + title: Der Gegner bremst die Figur + prompt: | + Ein Agent hat einen Gegner angelegt. Stellst du im Inspektor des Gegners `Max Speed` niedriger, läuft auch deine Figur langsamer. Finde die Zeile in `enemy.tscn` und korrigiere sie so, dass der Gegner `data/enemy_stats.tres` nutzt. Die Datei gibt es schon. + lang: ini + code: | + [gd_scene format=3] + + [ext_resource type="Script" path="res://enemy.gd" id="1"] + [ext_resource type="Resource" path="res://data/player_stats.tres" id="2"] + + [node name="Enemy" type="CharacterBody2D"] + script = ExtResource("1") + stats = ExtResource("2") + lines: [4] + fix: + - line: 4 + answers: + - '[ext_resource type="Resource" path="res://data/enemy_stats.tres" id="2"]' + explanation: | + Beide Szenen verweisen auf dieselbe Datei, und Godot lädt eine Ressource nur einmal: Figur und Gegner halten dasselbe Objekt. Im Inspektor des Gegners bearbeitest du also das Objekt aus `player_stats.tres` und änderst die Werte der Figur mit. Die `[ext_resource]`-Zeile verrät es auf einen Blick. Jeder Figurentyp bekommt seine eigene `.tres` mit demselben Skript. + resources: + - title: Godot Docs · Resources + url: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html + + - id: einzigartig-erklaeren + type: explain + title: Was Einzigartig machen gespeichert hat + prompt: | + Nach *Bearbeitbare Kinder* an `Pickup2`, *Einzigartig machen* an seiner Form und Radius 9 steht das in `main.tscn` (Ausschnitt). Erkläre in drei bis fünf Sätzen, was die vier Abschnitte bedeuten und warum `pickup.tscn` unverändert bleibt. + lang: ini + code: | + [sub_resource type="CircleShape2D" id="CircleShape2D_1bvp3"] + radius = 9.0 + + [node name="Pickup2" parent="Objects" instance=ExtResource("3")] + position = Vector2(-70, 30) + + [node name="Area" parent="Objects/Pickup2" index="1"] + shape = SubResource("CircleShape2D_1bvp3") + + [editable path="Objects/Pickup2"] + points: + - Die neue `[sub_resource]` ist die Kopie der Form, sie gehört `main.tscn`; `radius = 9.0` steht dort, weil 9 vom Standard der Klasse (10) abweicht. + - "Der Abschnitt `[node name=\"Area\" parent=\"Objects/Pickup2\" …]` ist ein Override am Kind der Instanz: Er ersetzt dort die Form." + - "`[editable path=…]` merkt sich, dass die Kinder von `Pickup2` im Editor aufgeklappt und überschreibbar sind." + - "`pickup.tscn` bleibt unverändert, `Pickup` und `Pickup3` teilen weiter die Form der Vorlage mit Radius 5." + explanation: | + Einzigartig machen hat für `Pickup2/Area` eine eigene Kreisform angelegt, die jetzt als `[sub_resource]` in `main.tscn` steht, mit dem einzigen Wert, der vom Standard der Klasse `CircleShape2D` abweicht, dem Radius 9 statt 10. Der zusätzliche Abschnitt für `Area` mit `parent="Objects/Pickup2"` überschreibt am Kind der Instanz die Eigenschaft `shape`; `index` legt fest, an welcher Stelle unter den Kindern es steht. `[editable path=…]` ist der Schalter Bearbeitbare Kinder. Die Vorlage berührt das alles nicht, deshalb teilen `Pickup` und `Pickup3` weiter die Form aus `pickup.tscn`. Gespeichert mit Godot 4.7.2; die `unique_id`-Angaben sind hier weggelassen. + resources: + - title: Godot Docs · TSCN file format · Internal resources + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#internal-resources + + - id: datenhaltung-planen + type: practice + title: Geteilt, überschrieben oder im Spiel verändert? + prompt: | + Nimm eine Art von Objekten, die dein Spiel brauchen wird. Notiere in drei bis fünf Sätzen, welcher Wert für alle Objekte dieser Art gilt, welcher je Platzierung im Level abweicht und welcher sich im Spiel ändert, und wo jeder liegen soll: eigene `.tres`, Override an der Instanz oder Variable im Skript. Die Auswahl triffst du. + lang: prose + checklist: + - Eine Art von Objekten ist genannt, mit mindestens drei Werten. + - Jeder Wert hat einen Ort (`.tres`, Override oder Skriptvariable). + - Beim Wert, der sich im Spiel ändert, steht, wie du verhinderst, dass er alle Objekte trifft. + hints: + - Ein Wert, der sich im Spiel ändert, gehört selten in eine geteilte Ressource. + - Frag bei jedem Wert, ob du ihn an einer Stelle für alle ändern willst. + explanation: | + Diese Liste ist die Vorgabe, die du einem Coding-Agenten mitgibst, bevor er Szenen und Ressourcen anlegt. Wer sie vorher schreibt, vermeidet die beiden häufigsten Fehler dieses Moduls: Werte, die versehentlich geteilt werden, und Werte, die an zu vielen Stellen überschrieben werden. Welche Objekte und Werte dein Spiel hat, entscheidest du; die Struktur bleibt dieselbe. + resources: + - title: Godot Docs · Resources · Creating your own resources + url: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html#creating-your-own-resources diff --git a/content/godot/modules/kollision-ebenen.yaml b/content/godot/modules/kollision-ebenen.yaml new file mode 100644 index 0000000..7f40c97 --- /dev/null +++ b/content/godot/modules/kollision-ebenen.yaml @@ -0,0 +1,542 @@ +id: kollision-ebenen +title: "Kollisionen und Trefferzonen: Körper, Formen, Layer und Masken" +level: 2 +minutes: 120 +summary: Welcher Node blockiert und welcher nur meldet, wie eine Form an den Node kommt, was Layer und Maske bedeuten und was eine Fußform in der Draufsicht bewirkt – mit Regeln, die an Godot 4.7.2 nachgemessen sind. +place: unterwegs +goals: + - Du wählst für Wand, Tür, Sammelstück und Figur den passenden Körper oder eine `Area2D` und begründest die Wahl. + - Du liest Layer und Maske im Inspektor, in `.tscn`-Dateien und im Code und rechnest zwischen Ebenennummer und Bitwert um. + - Du sagst für zwei Objekte vorher, ob sie sich blockieren und welches Signal feuert. + - Du erkennst an einem Spielbild mit sichtbaren Kollisionsformen, wo eine Form sitzt und welche Folge das für die Bewegung hat. + - Du findest fehlende Formen, falsche Masken und ausgeschaltetes `monitoring` an Warnungen, Symptomen und Einstellungen. +resources: + - title: Godot Docs · Physics introduction + url: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html + - title: Godot Docs · Using Area2D + url: https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html + - title: Godot Docs · CollisionObject2D + url: https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html +lesson: | + Du läufst im Testgelände über ein Sammelstück, und der Zähler oben links bleibt bei 0. Im Spielbild liegen Figur und Sammelstück übereinander, die Ausgabe zeigt keinen Fehler. Die Ursache steckt fast immer in vier Einstellungen, die ein Coding-Agent beim Anlegen gesetzt hat: Layer und Maske der beiden Objekte, dazu `monitoring` und `monitorable`. Wer sie lesen kann, findet den Fehler in einer Minute. Wer rät, klickt Kästchen an, bis es zufällig geht, und baut dabei den nächsten Fehler ein. + + Die Regeln für Erkennen und Blockieren stammen aus Messungen mit Godot 4.7.2 am Testgelände. Wie Hitboxen und Hurtboxen im Kampf zusammenspielen, steht in [Reichweite, Hitboxen und Hurtboxen](/combat/hitboxen); hier geht es um die Technik darunter. + + ## Körper und Areas: wer blockiert, wer meldet + + Godot kennt vier Arten von Kollisionsobjekten, alle abgeleitet von `CollisionObject2D` ([Physics introduction](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-objects)). `Area2D` blockiert nichts und meldet nur. Die anderen drei, `StaticBody2D`, `RigidBody2D` und `CharacterBody2D`, sind Körper (physics bodies) und blockieren einander. `AnimatableBody2D` ist eine Unterklasse von `StaticBody2D`, deshalb hat sie in der Tabelle eine eigene Zeile. + + | Node | Wer bewegt ihn | Im Testgelände | + | --- | --- | --- | + | `StaticBody2D` | niemand, oder dein Code versetzt ihn | `Crate` | + | `AnimatableBody2D` | Code oder Animation, schiebt andere mit | – | + | `RigidBody2D` | die Physik-Engine mit Kräften und Stößen | – | + | `CharacterBody2D` | dein Code mit `move_and_slide()` | `Player` | + | `Area2D` | blockiert nicht, meldet Überlappungen | `Pickup` | + + Der Unterschied zwischen den ersten beiden zeigt sich erst, wenn sich etwas bewegt. Versetzt du einen `StaticBody2D` per Code, springt er laut Doku an die neue Stelle, ohne Körper auf dem Weg zu bewegen. Ein `AnimatableBody2D` schätzt dabei seine Geschwindigkeit und schiebt andere Körper mit, etwa als Tür oder bewegliche Plattform ([StaticBody2D](https://docs.godotengine.org/en/stable/classes/class_staticbody2d.html), [AnimatableBody2D](https://docs.godotengine.org/en/stable/classes/class_animatablebody2d.html)). Bewegst du ihn mit einem `AnimationPlayer`, nennt die Doku dafür den Modus `Physics` für `callback_mode_process`. Ein `RigidBody2D` passt für Dinge, die nach einem Stoß von selbst weiterrollen oder abprallen; du steuerst ihn mit Kräften, nicht mit Positionen. + + Eine `Area2D` meldet über Signale, wenn ein Körper hineinkommt (`body_entered`) oder eine andere Area (`area_entered`) ([Using Area2D](https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html#overlap-detection)). Das Sammelstück ist so eine Area: Die Figur läuft hindurch, und `pickup.gd` reagiert auf `body_entered`. + + Aus dem Web kennst du den `IntersectionObserver`: Er meldet, dass sich zwei Elemente überschneiden, und verschiebt nichts. So arbeitet eine Area. Ein Körper gleicht eher einem Element, das im Layout Platz einnimmt. Der Vergleich bricht an zwei Stellen: Der Observer beobachtet Elemente, die du ihm einzeln gibst, während Godot über Ebenen filtert, wer wen überhaupt sieht. Und Godot meldet nicht sofort, sondern im nächsten Physik-Frame; das misst [Godot-Start](/combat/godot-start) nach. + + ## Formen: eine Shape je CollisionShape2D, Größe statt Skalierung + + Ein Kollisionsobjekt ohne Form kollidiert mit nichts. Die Form kommt von Kind-Nodes: Ein `CollisionShape2D` trägt genau eine Form-Ressource in seiner Eigenschaft `shape`, ein `CollisionPolygon2D` ein Polygon, das du im Editor zeichnest ([CollisionShape2D](https://docs.godotengine.org/en/stable/classes/class_collisionshape2d.html#class-collisionshape2d-property-shape), [CollisionPolygon2D](https://docs.godotengine.org/en/stable/classes/class_collisionpolygon2d.html)). Brauchst du zwei Formen, hängst du zwei Kinder an. Die häufigsten Form-Ressourcen mit ihren Standardgrößen in Godot 4.7.2: + + | Ressource | Größe steckt in | Standard | + | --- | --- | --- | + | `RectangleShape2D` | `size` | 20 × 20 | + | `CircleShape2D` | `radius` | 10 | + | `CapsuleShape2D` | `radius`, `height` | 10, 30 | + + In der `.tscn` steht die Form als `sub_resource`, der Node verweist darauf. So sieht die Fußform der Figur in `player.tscn` aus: + + ```ini title=player.tscn + [sub_resource type="RectangleShape2D" id="feet"] + size = Vector2(10, 6) + + [node name="Feet" type="CollisionShape2D" parent="."] + position = Vector2(0, -3) + shape = SubResource("feet") + ``` + + Die Form ist eine Ressource. Alle Instanzen von `pickup.tscn` teilen sich deshalb denselben Kreis: Änderst du `radius` in `pickup.tscn`, ändern sich alle Sammelstücke. Was das für Instanzen heißt und wie du eine Form einzigartig machst, steht im [Modul über Instanzen und Ressourcen](/godot/instanzen-ressourcen). + + Die Größe änderst du an der Form selbst, über `size` oder `radius` oder die Griffe der Form im 2D-Bildschirm, nie über `scale` des Nodes. Die Doku warnt ausdrücklich: Die Skalierung im Inspektor soll `(1, 1)` bleiben, eine skalierte Form kann unerwartet kollidieren ([Collision shapes](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-shapes)). + + Fehlt die Form, zeigt das Szenen-Dock ein Warnsymbol neben dem Node: + + ![Szenen-Dock von pickup.tscn mit zwei zusätzlichen Nodes CollisionShape2D und Area2D, beide mit gelbem Warndreieck](/media/godot/kollision-ebenen/szene-warnung.png "Godot 4.7.2, Szenen-Dock: Form ohne Shape-Ressource und Area2D ohne Form-Kind") + + Fährst du mit der Maus über das Dreieck, nennt Godot 4.7.2 für das `CollisionShape2D`: „Ein CollisionShape2D benötigt zum Funktionieren eine zugeordnete Shape. Um es verwenden zu können, muss eine Shape-Ressource dafür erzeugt werden!“ Für die `Area2D` ohne Form-Kind: „Dieser Node besitzt keine Shapes, er kann also nicht mit anderen Objekten kollidieren oder interagieren.“ + + ## Layer und Maske: wo ich bin, wen ich suche + + Jedes Kollisionsobjekt hat zwei Eigenschaften mit je 32 Ebenen. `collision_layer` sind die Ebenen, auf denen es liegt. `collision_mask` sind die Ebenen, die es absucht. Standard ist für beide Ebene 1 ([Collision layers and masks](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks)). Die Klassenreferenz fasst die Regel in einem Satz: „Object A can detect a contact with object B only if object B is in any of the layers that object A scans.“ ([CollisionObject2D.collision_layer](https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-property-collision-layer)) + + > [!MERKE] + > Layer heißt: Hier bin ich. Maske heißt: Die suche ich. Ob gemeldet oder blockiert wird, entscheidet die Maske dessen, der sucht. + + So verhält sich Godot 4.7.2, nachgemessen mit zwei überlappenden Formen und einer Figur, die gegen eine Kiste läuft: + + | Paar | Was passiert | Bedingung | + | --- | --- | --- | + | Area A, Körper B | A sendet `body_entered` | `monitoring` von A an, Maske von A enthält Layer von B | + | Area A, Area B | A sendet `area_entered` | `monitoring` von A an, `monitorable` von B an, Maske von A enthält Layer von B | + | `CharacterBody2D` A läuft mit `move_and_slide()` gegen Körper B | A bleibt stehen | Maske von A enthält Layer von B | + + Die Maske des anderen spielt in diesen drei Fällen keine Rolle. Eine Area meldet auch einen Körper mit Maske 0, und eine Kiste mit Maske 0 hält die Figur genauso auf. Zwischen zwei Areas gilt jede Richtung für sich: Sucht jede die Ebene der anderen, bekommen beide ihr `area_entered`. `monitoring` heißt „ich melde“, `monitorable` heißt „ich darf gefunden werden“ ([Area2D.monitorable](https://docs.godotengine.org/en/stable/classes/class_area2d.html#class-area2d-property-monitorable)). Körper haben kein `monitorable`; eine Area findet jeden Körper, dessen Layer in ihrer Maske liegt. Areas blockieren nie: Auch eine Figur, deren Maske die Ebene der Sammelstücke enthält, läuft hindurch. + + Im Testgelände sind die Ebenen so verteilt: + + | Node | Layer | Maske | Sonst | + | --- | --- | --- | --- | + | `Player` (`CharacterBody2D`) | 2 player | 1 world | – | + | `Crate` (`StaticBody2D`) | 1 world | keine | – | + | `Pickup` (`Area2D`) | 3 pickups | 2 player | `monitorable` aus | + + ![Inspektor des Pickup: Monitoring an, Monitorable aus mit Zurücksetzen-Pfeil, im Raster Layer 3 und Mask 2 hervorgehoben](/media/godot/kollision-ebenen/pickup-inspektor.png "Godot 4.7.2, Inspektor von pickup.tscn: Abschnitte Area2D und Collision") + + Lies es von der Seite des Suchenden. Das Sammelstück sucht Ebene 2, die Figur liegt dort: `body_entered` feuert. Die Figur sucht Ebene 1, die Kiste liegt dort: Die Figur bleibt an ihr stehen. Die Kiste sucht nichts, das muss sie auch nicht, denn sie bewegt sich nie. Und das Sammelstück darf von keiner anderen Area gefunden werden, weil `monitorable` aus ist. Der Zurücksetzen-Pfeil neben Monitorable, Layer und Mask zeigt dir, dass diese Werte vom Standard abweichen. Das Wort „An“ steht neben jedem Kästchen, auch neben einem ausgeschalteten; es zählt nur der Haken. + + ## Nummer, Bitwert und Layer-Namen + + Der Inspektor zeigt Ebenen als Nummern ab 1; das Raster zeigt die ersten 16 von 32. In der `.tscn` und im Code steht dagegen eine Zahl, in der jede Ebene ein Bit ist: Ebene *n* hat den Wert 2^(*n*−1). Ebene 1 ist 1, Ebene 2 ist 2, Ebene 3 ist 4, Ebene 4 ist 8. Mehrere Ebenen ergeben die Summe. So steht das Sammelstück in `pickup.tscn`: + + ```ini title=pickup.tscn + [node name="Pickup" type="Area2D"] + collision_layer = 4 + collision_mask = 2 + monitorable = false + script = ExtResource("1") + ``` + + Werte, die dem Standard entsprechen, schreibt Godot nicht in die Datei. `crate.tscn` enthält deshalb keine Zeile `collision_layer`: Die Kiste liegt auf Ebene 1. Was nicht dasteht, ist der Standard. + + Im Code setzt du einzelne Ebenen mit ihrer Nummer zwischen 1 und 32 ([set_collision_mask_value](https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-method-set-collision-mask-value)). Eine neue Area sucht schon Ebene 1. Willst du nur Ebene 3, leerst du die Maske zuerst: + + ```gdscript + var hitbox := Area2D.new() + print(hitbox.collision_mask) + hitbox.collision_mask = 0 + hitbox.set_collision_mask_value(3, true) + print(hitbox.collision_mask) + ``` + + ```text + 1 + 4 + ``` + + Mit einer Nummer außerhalb von 1 bis 32 meldet Godot 4.7.2 `Collision layer number must be between 1 and 32 inclusive.`, auch bei der Maske. + + > [!WARNING] Nummer oder Bitwert? + > `set_collision_mask_value(4, true)` schaltet Ebene 4 ein, Bitwert 8. `collision_mask = 3` heißt Ebenen 1 und 2, nicht Ebene 3. Wer die beiden Schreibweisen mischt, sucht die falsche Ebene, und Godot meldet keinen Fehler, nur nichts. + + Damit niemand Nummern auswendig lernen muss, gibst du den Ebenen Namen: *Projekt → Projekteinstellungen → Allgemein → Ebenen-Namen → 2D-Physik*. Der Abschnitt ist ohne „Erweiterte Einstellungen“ sichtbar. In `project.godot` steht danach: + + ```ini title=project.godot + [layer_names] + + 2d_physics/layer_1="world" + 2d_physics/layer_2="player" + 2d_physics/layer_3="pickups" + ``` + + Die Namen helfen beim Lesen; gespeichert wird weiter der Bitwert. Eigene Skript-Variablen bekommen dasselbe Raster im Inspektor mit `@export_flags_2d_physics` ([Exporting bit flags](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#exporting-bit-flags)). + + > [!PRAXIS] + > Leg die Ebenentabelle an, bevor ein Agent die ersten Kollisionen baut: Nummer, Name, wer dort liegt, wer sie sucht. Gib sie ihm mit in die Aufgabe und leg sie ins Repository. Bei einem Fehler vergleichst du dann vier Einstellungen mit einer Tabelle, statt sie aus dem Code zu rekonstruieren. + + ## Draufsicht: was eine Fußform bewirkt + + In einer Draufsicht steht der Kopf einer Figur im Bild über ihren Füßen. Das Testgelände hat dafür eine Konvention: Der Ursprung der Figur liegt an den Füßen, und ihre Form `Feet` ist ein flaches Rechteck von 10 × 6 Pixeln direkt darüber. Die Kiste kollidiert ebenso nur mit ihrem unteren Teil `Base` (16 × 8). Zusammen mit der Y-Sortierung im Node `Objects` (siehe [Modul über Szenenbäume](/godot/szenenbaum)) hat das eine Folge: Die Figur kann vor der Kiste stehen und hinter ihr, ihr Kopf überlappt die Kiste im Bild. Wie groß die Formen in deinem Spiel sind, entscheidest du; die Messungen zeigen, was die Wahl bewirkt. + + ![Zwei Ausschnitte aus dem Spiel mit sichtbaren Kollisionsformen: links steht die blaue Figur vor der braunen Kiste und verdeckt ihren unteren Teil, rechts steht sie dahinter und nur ihr Kopf ragt über die Kiste](/media/godot/kollision-ebenen/fussform@2x.png "Godot 4.7.2, Testgelände mit Debuggen → Sichtbare Collision-Shapes, sechsfach vergrößert") + + Gemessen mit Godot 4.7.2: Läuft die Figur von unten auf die Kiste zu, bleibt sie bei y ≈ 16 stehen, weil die Oberkante ihrer Füße (y − 6) die Unterkante der Kistenform (y = 10) erreicht. Ihr Bild reicht dann bis y = 0 und überdeckt die Kiste. Von oben bleibt sie bei y ≈ 2 stehen, und die Kiste verdeckt ihre Beine. Mit einer Form so groß wie die ganze Figur bleibt sie von unten schon bei y ≈ 26 stehen, 10 Pixel früher, und ihr Bild berührt die Kiste nur noch. + + Die Formen siehst du im laufenden Spiel mit *Debuggen → Sichtbare Collision-Shapes* (Visible Collision Shapes); im Terminal startet `godot --debug-collisions` das Projekt mit derselben Anzeige ([Visible collision shapes](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes), [Command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#debugging)). Im Testgelände ist die Fußform nur für Wände und Kisten da. Treffer im Kampf prüfen eigene Areas, Hitbox und Hurtbox, die anders sitzen können; deren Ebenentabelle steht in [Testszene: Angriff mit Frame-Daten](/combat/testszene-angriff). + + > [!DEEP] Floating statt Grounded + > Die Figur steht auf `motion_mode` Floating. In diesem Modus gibt es keinen Boden und keine Decke, jede Kollision zählt als Wand, und beim Gleiten bleibt das Tempo konstant ([CharacterBody2D.motion_mode](https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html#class-characterbody2d-property-motion-mode)). Grounded ist für Plattformer gedacht; dort wird die Figur an Schrägen schneller oder langsamer. + + ## Vorhersagen: welche Signale feuern + + Bevor du etwas startest, bestimmst du für jedes Paar zuerst, wer sucht: die Area, die das Signal senden soll, oder der Körper, der sich bewegt. Dann zwei Prüffragen. Ist beim Suchenden `monitoring` an (nur bei Areas), und darf das Ziel gefunden werden (`monitorable`, nur wenn das Ziel eine Area ist)? Enthält die Maske des Suchenden den Layer des Ziels? Erst wenn beide Antworten Ja lauten, feuert ein Signal oder blockiert ein Körper. + + Ein paar Änderungen am Testgelände, mit Godot 4.7.2 nachgespielt. Die Figur läuft jeweils über das erste Sammelstück: + + | Änderung | Zähler | Warum | + | --- | --- | --- | + | keine | 1 | Maske Pickup 2 trifft Layer Figur 2 | + | Maske des Pickup auf 1 | 0 | das Pickup sucht die Welt, nicht die Figur | + | `monitoring` des Pickup aus | 0 | das Pickup meldet nichts mehr | + | Maske der Figur auf 0 | 1 | das Pickup sucht weiter; die Figur läuft aber durch die Kiste | + + Die letzte Zeile ist die lehrreichste: Wer welche Maske ändert, entscheidet, was sich ändert. Wann ein Signal genau kommt (einen Physik-Frame nach der Überlappung) und warum du in einem Signal Formen nur mit `set_deferred()` umschaltest, misst [Godot-Start](/combat/godot-start) nach. + + > [!INTERVIEW] + > „Was ist der Unterschied zwischen Collision Layer und Collision Mask?“ Gute Antwort: Der Layer sagt, wo ein Objekt liegt, die Maske, welche Ebenen es absucht. Erkannt oder blockiert wird nur, wenn die Maske des Suchenden den Layer des anderen enthält. Beispiel: Ein Sammelstück liegt auf „pickups“ und sucht „player“. Dann ein Satz zur Fehlersuche: Ich prüfe zuerst, wer sucht, dann `monitoring` und `monitorable`, dann Maske gegen Layer, mit sichtbaren Formen im laufenden Spiel. + + ## Häufige Fehler + + - **Maske beim Falschen gesetzt.** Das Pickup findet die Figur nicht, also bekommt die Figur eine Maske für Ebene 3. Gesucht wird aber von der Area aus. Prüf die Maske dessen, der das Signal senden soll. + - **Nummer und Bitwert verwechselt.** `set_collision_mask_value(4, true)` für Ebene 3 oder `collision_mask = 3` für Ebene 3. Beides sucht die falsche Ebene, ohne Fehlermeldung. + - **Standard-Maske vergessen.** Neue Objekte suchen Ebene 1. Wer im Code nur Ebene 3 einschaltet, bekommt Maske 5 und findet nebenbei alle Wände. + - **`monitorable` aus und trotzdem gesucht.** Eine zweite Area, etwa ein Magnet an der Figur, findet die Sammelstücke des Testgeländes nicht, weil sie nicht gefunden werden dürfen. + - **Form fehlt.** Ein `CollisionShape2D` ohne Shape oder eine Area ohne Form-Kind kollidiert mit nichts. Das Warndreieck im Szenen-Dock sagt es dir. + - **Form skaliert.** `scale` am `CollisionShape2D` statt `size` an der Form. Das sieht im Editor richtig aus, laut Doku kann die Form aber unerwartet kollidieren. + - **Form größer als gedacht.** Deckt die Form die ganze Figur ab, obwohl die Figur hinter Kisten treten soll, bleibt sie mit dem Kopf an Kisten und Wänden hängen und kommt nie dahinter. Prüf mit sichtbaren Formen, ob die Form zu deiner Absicht passt. + - **Geteilte Form geändert.** Der Radius eines Sammelstücks ändert sich, und alle anderen ändern sich mit, weil sie dieselbe Ressource benutzen. + + ## Zusammenfassung + + - Körper blockieren (`StaticBody2D`, `AnimatableBody2D`, `RigidBody2D`, `CharacterBody2D`), eine `Area2D` meldet nur, mit `body_entered` und `area_entered`. + - Ein `CollisionShape2D` trägt genau eine Form. Größe über `size` oder `radius`, Skalierung bleibt `(1, 1)`. Formen sind Ressourcen und werden zwischen Instanzen geteilt. + - Layer heißt „hier bin ich“, Maske „die suche ich“. Gemeldet oder blockiert wird nur, wenn die Maske des Suchenden den Layer des anderen enthält; die Maske des anderen zählt bei Areas und bei `move_and_slide()` nicht. + - Eine Area meldet nur mit `monitoring` an. Ist das Ziel selbst eine Area, muss es außerdem `monitorable` an haben. + - Der Inspektor zählt Ebenen ab 1, `.tscn` und `collision_mask = …` speichern Bitwerte: Ebene 3 ist 4. `set_collision_mask_value()` nimmt die Nummer. + - Ebenen bekommen Namen unter *Projekteinstellungen → Ebenen-Namen → 2D-Physik*; Werte, die dem Standard entsprechen, fehlen in der `.tscn`. + - Im Testgelände kollidiert die Figur mit einer flachen Form an den Füßen und kann so hinter Kisten treten; Treffer prüfen eigene Areas. + - *Debuggen → Sichtbare Collision-Shapes* oder `--debug-collisions` zeigt die Formen im laufenden Spiel. +exercises: + - id: koerper-zuordnen + type: choice + title: Welcher Node für welches Ding? + prompt: | + Für einen Testraum brauchst du vier Dinge. Welche Zuordnungen stimmen? + options: + - text: Eine Wand, die nie bewegt wird – `StaticBody2D`. + correct: true + why: Sie blockiert, die Physik bewegt sie nicht. Genau dafür ist der statische Körper da. + - text: Eine Tür, die eine Animation zur Seite schiebt und die Figur dabei mitschiebt – `AnimatableBody2D`. + correct: true + why: Laut Doku schätzt er beim Bewegen seine Geschwindigkeit und schiebt Körper auf dem Weg mit. Ein `StaticBody2D` würde nur springen. + - text: Ein Sammelstück, durch das die Figur hindurchläuft – `CharacterBody2D`. + correct: false + why: Ein Körper blockiert. Für etwas, das nur melden soll, nimmst du eine `Area2D` mit `body_entered`. + - text: Eine Kugel, die nach einem Stoß von selbst weiterrollt und abprallt – `StaticBody2D`. + correct: false + why: Ein statischer Körper bewegt sich nicht von selbst. Rollen und Abprallen rechnet die Physik-Engine für einen `RigidBody2D`. + explanation: | + Die Frage ist immer: Wer bewegt das Ding, und soll es blockieren? Niemand bewegt die Wand, eine Animation bewegt die Tür und soll dabei schieben, die Physik bewegt die Kugel, dein Code die Figur. Was nur melden soll, ist eine `Area2D`. Wer hier den falschen Körper nimmt, merkt es oft erst, wenn sich etwas bewegt: Ein versetzter `StaticBody2D` springt durch die Figur, statt sie zu schieben. + resources: + - title: Godot Docs · Physics introduction · Collision objects + url: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-objects + - title: Godot Docs · AnimatableBody2D + url: https://docs.godotengine.org/en/stable/classes/class_animatablebody2d.html + + - id: einsammeln-ablauf + type: order + title: Vom Überlappen bis zur Zahl + prompt: | + Die Figur läuft im Testgelände über ein Sammelstück. Bring die Schritte in die Reihenfolge, in der sie passieren. Die Skripte `pickup.gd`, `player.gd` und `hud.gd` kennst du aus dem Testgelände; `Player.collected` ist mit `HUD.show_total` verbunden. + lang: prose + lines: | + Der Physik-Schritt findet die Überlappung von Fußform und Kreis + Das Pickup sendet body_entered mit der Figur als body + _on_body_entered prüft, ob body ein Player ist + collect(value) erhöht total der Figur + Die Figur sendet collected mit dem neuen total + show_total schreibt die Zahl ins Label + queue_free entfernt das Pickup am Ende des Frames + explanation: | + Die Physik findet die Überlappung, weil die Maske des Pickup (Ebene 2) den Layer der Figur enthält. Danach läuft alles in einer Kette von Aufrufen: `body_entered` ruft `_on_body_entered`, das ruft `collect()`, das sendet `collected`, und die Verbindung aus `main.tscn` ruft sofort `show_total()`. Erst wenn die Kette zurückkehrt, ruft `pickup.gd` `queue_free()`; Godot entfernt das Pickup am Ende des Frames. Bleibt der Zähler bei 0, prüfst du die Kette von vorn: Ohne Überlappung kein Signal. + resources: + - title: Godot Docs · Using Area2D · Overlap detection + url: https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html#overlap-detection + + - id: maske-als-zahl + type: output + title: Was steht in der Maske? + prompt: | + Was gibt dieser Schnipsel in Godot 4.7 aus? Zwei Zeilen. + lang: gdscript + code: | + var hitbox := Area2D.new() + hitbox.set_collision_mask_value(3, true) + print(hitbox.collision_mask) + hitbox.collision_mask = 3 + print(hitbox.get_collision_mask_value(3)) + expected: | + 5 + false + hints: + - Eine neue Area sucht schon Ebene 1. + - Ebene 3 hat den Bitwert 4. Und welche Ebenen stecken in der Zahl 3? + explanation: | + Eine neue Area hat die Maske 1. `set_collision_mask_value(3, true)` schaltet Ebene 3 dazu, Bitwert 4: zusammen 5. `collision_mask = 3` setzt den Bitwert 3, also die Ebenen 1 und 2. Ebene 3 ist dann aus, daher `false`. Gemessen mit Godot 4.7.2. Wer nur Ebene 3 will, setzt vorher `collision_mask = 0` oder gleich `collision_mask = 4`. + resources: + - title: Godot Docs · CollisionObject2D.set_collision_mask_value + url: https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-method-set-collision-mask-value + + - id: zaehler-vorhersagen + type: output + title: Die Figur auf der falschen Ebene + prompt: | + Im Testgelände stellt jemand in `player.tscn` den Layer der Figur von 2 auf 1 um. Sonst bleibt alles wie in der Tabelle der Lektion. Die Figur läuft über alle drei Sammelstücke. Welche Zahl zeigt der Zähler danach? + lang: text + code: | + Player Layer 1 (vorher 2) Maske 1 + Crate Layer 1 Maske – + Pickup Layer 3 Maske 2 + expected: | + 0 + accept: + - "0 Sammelstücke" + hints: + - Wer sucht hier wen? + - Das Pickup sucht Ebene 2. Liegt dort noch jemand? + explanation: | + Das Pickup sucht mit seiner Maske Ebene 2. Die Figur liegt jetzt auf Ebene 1, also findet es sie nicht, und `body_entered` feuert nie. Die Figur selbst sucht weiter Ebene 1 und bleibt an der Kiste stehen; das hat mit dem Einsammeln nichts zu tun. Gemessen mit Godot 4.7.2: Zähler 0. Solche Fehler entstehen, wenn jemand den Layer für einen anderen Zweck umstellt und nicht nachsieht, wer die Ebene sucht. + resources: + - title: Godot Docs · Physics introduction · Collision layers and masks + url: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks + + - id: magnet-vorhersagen + type: output + title: Ein Magnet an der Figur + prompt: | + Ein Agent hängt eine neue `Area2D` namens `Magnet` unter die Figur: Kreis mit Radius 24, Layer keiner, Maske Ebene 3, `monitoring` an. Die Sammelstücke bleiben wie im Testgelände. Die Figur läuft über ein Sammelstück. Wie oft feuert `area_entered` am Magnet? + lang: text + code: | + Magnet Layer – Maske 3 monitoring an + Pickup Layer 3 Maske 2 monitorable aus + expected: | + 0 + accept: + - 0-mal + - 0 Mal + - keinmal + - Keinmal + - Null + - gar nicht + - "0 mal" + - nie + - Nie + hints: + - Für Area gegen Area reicht die Maske nicht. + - Was heißt `monitorable` aus? + explanation: | + Maske und Layer passen, aber eine Area findet eine andere Area nur, wenn diese `monitorable` an hat. Beim Pickup des Testgeländes ist es aus, also feuert `area_entered` nie. Das Einsammeln selbst klappt weiter, denn das Pickup findet die Figur über `body_entered`. Gemessen mit Godot 4.7.2: Mit `monitorable` aus meldet der Magnet 0-mal, mit `monitorable` an einmal. + resources: + - title: Godot Docs · Area2D.monitorable + url: https://docs.godotengine.org/en/stable/classes/class_area2d.html#class-area2d-property-monitorable + + - id: stopp-position + type: output + title: Wo bleibt die Figur stehen? + prompt: | + Die Figur startet bei (40, 40) und läuft gerade nach oben auf die Kiste bei (40, 10) zu. Bei welchem y bleibt ihr Ursprung stehen? Antworte in ganzen Pixeln. + lang: text + code: | + Figur Ursprung an den Füßen + Feet: Rechteck 10 × 6, Mitte (0, -3) + Kiste Ursprung bei (40, 10), an der Unterkante + Base: Rechteck 16 × 8, Mitte (0, -4) + expected: | + 16 + accept: + - y = 16 + - y=16 + - "16 px" + - 16 Pixel + - y = 16 px + hints: + - Die Kistenform reicht von y = 2 bis y = 10. + - Die Fußform reicht von y − 6 bis y. Wann berührt ihre Oberkante die Unterkante der Kiste? + explanation: | + Die Form der Kiste liegt 8 Pixel hoch über ihrem Ursprung, also von y = 2 bis 10. Die Fußform reicht von y − 6 bis y. Sie stößt an, wenn y − 6 = 10 ist, also bei y = 16. Godot 4.7.2 meldet 16 mit einem Bruchteil eines Pixels Sicherheitsabstand. Das Bild der Figur reicht dann bis y = 0 und überdeckt den unteren Teil der Kiste: Sie steht davor. Eine Form über die ganze Figur hätte sie schon bei y = 26 aufgehalten. + resources: + - title: Godot Docs · RectangleShape2D + url: https://docs.godotengine.org/en/stable/classes/class_rectangleshape2d.html + + - id: pickup-tscn + type: gap + title: Das Sammelstück als Text + prompt: | + Ergänze die drei Werte in `pickup.tscn`: Das Sammelstück liegt auf Ebene 3, sucht die Figur auf Ebene 2 und darf von keiner anderen Area gefunden werden. + lang: ini + code: | + [node name="Pickup" type="Area2D"] + collision_layer = ⟦4⟧ + collision_mask = ⟦2⟧ + monitorable = ⟦false⟧ + script = ExtResource("1") + gaps: + - label: Layer als Bitwert + hint: Ebene n hat den Wert 2 hoch (n − 1). + - label: Maske als Bitwert + hint: Ebene 2 hat den Wert 2. + - label: Gefunden werden + hint: Ein Wahrheitswert, wie GDScript ihn schreibt. + explanation: | + In der `.tscn` stehen Bitwerte: Ebene 3 ist 4, Ebene 2 ist 2. Der Inspektor zeigt dieselben Werte als Kästchen 3 und 2. `monitorable = false` steht nur da, weil der Standard `true` ist; `monitoring` fehlt, weil es auf dem Standard `true` bleibt. Wer `collision_layer = 3` schreibt, legt das Sammelstück auf die Ebenen 1 und 2. + resources: + - title: Godot Docs · Physics introduction · Code example + url: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#code-example + + - id: formen-sichtbar-starten + type: command + title: Formen sehen, ohne Menü + prompt: | + Starte das Testgelände aus dem Terminal so, dass alle Kollisionsformen im laufenden Spiel sichtbar sind, ohne den Haken im Menü *Debuggen* zu setzen. + context: | + Du bist im Projektordner (dort liegt `project.godot`). `godot` steht für dein Godot-4.7-Binary. + answers: + - godot --debug-collisions + - godot --path . --debug-collisions + - godot --debug-collisions --path . + explanation: | + `--debug-collisions` zeigt die Kollisionsformen beim Start, wie *Debuggen → Sichtbare Collision-Shapes* im Editor. Liegt `project.godot` im aktuellen Ordner, findet Godot das Projekt selbst; sonst gibst du es mit `--path` an. Mit Godot 4.7.2 geprüft: In beiden Fällen ist `debug_collisions_hint` des SceneTree an. Praktisch für Testläufe, die du per Skript oder Tastenkürzel startest. + resources: + - title: Godot Docs · Command line tutorial · Debugging + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#debugging + + - id: ganze-figur + type: choice + title: Die Figur kommt nicht an die Kiste + prompt: | + Im Testgelände soll die Figur vor die Kiste treten können, so wie es die Vorlage vorsieht. Nach einer Änderung bleibt sie von unten stehen, bevor ihr Bild die Kiste überlappt. Das Bild zeigt den Moment mit sichtbaren Kollisionsformen. Welche Änderung erklärt das? + + ![Spielbild mit sichtbaren Kollisionsformen: Unter der braunen Kiste steht die blaue Figur, ihre Kollisionsform ist ein hohes Rechteck über die ganze Figur und stößt an die Form der Kiste](/media/godot/kollision-ebenen/ganzkoerper@2x.png "Godot 4.7.2, Testgelände mit sichtbaren Formen, siebenfach vergrößert") + options: + - text: Die Form `Feet` deckt jetzt die ganze Figur ab, nicht mehr nur die Füße wie in der Vorlage. + correct: true + why: Das Rechteck ist so hoch wie die Figur. Es stößt mit dem Kopf an die Kistenform, deshalb bleibt die Figur 10 Pixel früher stehen. + - text: Die Maske der Kiste muss Ebene 2 enthalten, sonst stimmt der Abstand nicht. + correct: false + why: Die Maske der Kiste spielt beim Anstoßen keine Rolle. Ob die Figur stehen bleibt, entscheidet ihre eigene Maske. + - text: Die Y-Sortierung ist aus, deshalb sieht es nur so aus, als stünde die Figur zu weit weg. + correct: false + why: Die Y-Sortierung ändert nur die Zeichenreihenfolge. Die Figur bleibt tatsächlich früher stehen, das zeigt die Form. + - text: Die Kistenform `Base` ist zu klein und muss die ganze Kiste abdecken. + correct: false + why: Dann bliebe die Figur noch früher stehen und könnte auch von oben nie hinter die Kiste. + explanation: | + Die Vorlage legt `Feet` als flaches Rechteck an die Füße, damit die Figur vor und hinter die Kiste treten kann. Mit einer Form über die ganze Figur stößt der Kopf an die Kiste, und die Figur bleibt bei y ≈ 26 statt bei y ≈ 16 stehen, gemessen mit Godot 4.7.2. Welche Form deine Figur bekommt, entscheidest du; hier widerspricht die Änderung der Absicht der Vorlage. Prüf solche Fälle mit sichtbaren Formen: Das Bild allein zeigt nur, dass etwas nicht passt, die Form zeigt, warum. + resources: + - title: Godot Docs · Overview of debugging tools · Visible collision shapes + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes + + - id: magnet-bitwert + type: bug + title: Der Magnet findet nichts + prompt: | + Der Magnet aus einer früheren Aufgabe hat jetzt dieses Skript. Die Sammelstücke liegen auf Ebene 3 „pickups“, und `monitorable` hast du bei ihnen schon eingeschaltet. Trotzdem gibt der Magnet nie etwas aus, auch wenn sich die Formen mit sichtbaren Kollisionsformen deutlich überlappen. Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + extends Area2D + + ## Magnet: meldet Sammelstücke in der Nähe (Testwerkzeug). + + func _ready() -> void: + collision_layer = 0 + collision_mask = 0 + set_collision_mask_value(4, true) + area_entered.connect(_on_area_entered) + + + func _on_area_entered(area: Area2D) -> void: + print("Magnet findet ", area.name) + lines: [8] + fix: + - line: 8 + answers: + - set_collision_mask_value(3, true) + - self.set_collision_mask_value(3, true) + - collision_mask = 4 + - self.collision_mask = 4 + - collision_mask |= 4 + - self.collision_mask |= 4 + explanation: | + `set_collision_mask_value()` nimmt die Ebenennummer, nicht den Bitwert. Mit 4 sucht der Magnet Ebene 4 (Bitwert 8), auf der nichts liegt. Richtig ist Ebene 3, oder direkt der Bitwert `collision_mask = 4`. Mit Godot 4.7.2 geprüft: Die falsche Fassung hat Maske 8 und meldet nichts, die korrigierte Maske 4 und meldet `Magnet findet Pickup`. + resources: + - title: Godot Docs · CollisionObject2D.set_collision_mask_value + url: https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-method-set-collision-mask-value + + - id: pickup-inspektor-fehler + type: bug + title: Der Zähler bleibt bei 0 + prompt: | + Nach einer Änderung am Testgelände bleibt der Zähler bei 0. Mit sichtbaren Formen siehst du, dass der Kreis des Sammelstücks die Fußform überlappt. Der Inspektor des Pickup sieht so aus: + + ![Inspektor des Pickup: Monitoring an, Monitorable aus, Layer 3, im Raster Mask ist Kästchen 1 hervorgehoben und neben Mask fehlt der Zurücksetzen-Pfeil](/media/godot/kollision-ebenen/pickup-maske-1.png "Godot 4.7.2, Inspektor von pickup.tscn nach der Änderung") + + Welche Einstellung verursacht das? Wähle die Zeile und schreib sie richtig. Das Wort „An“ steht neben jedem Kästchen, auch neben einem ausgeschalteten; es zählt nur der Haken. + lang: text + code: | + Monitoring an + Monitorable aus + Layer 3 + Mask 1 + lines: [4] + fix: + - line: 4 + answers: + - Mask 2 + - mask 2 + - Maske 2 + - maske 2 + - "Mask: 2" + - "Maske: 2" + - Mask = 2 + - Maske = 2 + - Mask 2 player + - Mask 2 (player) + - collision_mask = 2 + explanation: | + Das Pickup sucht Ebene 1, die Welt. Die Figur liegt auf Ebene 2, also feuert `body_entered` nie. Der fehlende Zurücksetzen-Pfeil neben Mask verrät es: Die Maske steht wieder auf dem Standard, Ebene 1. `Monitorable` aus ist hier kein Fehler; es verhindert nur, dass andere Areas das Pickup finden. Mit Godot 4.7.2 geprüft: Maske 1 ergibt Zähler 0. + resources: + - title: Godot Docs · Area2D.monitoring + url: https://docs.godotengine.org/en/stable/classes/class_area2d.html#class-area2d-property-monitoring + + - id: kiste-maske-null + type: explain + title: Warum die Kiste niemanden sucht + prompt: | + In `crate.tscn` steht `collision_mask = 0`, und die Figur bleibt trotzdem an der Kiste stehen. Erkläre in zwei bis vier Sätzen, warum das funktioniert und wann die Maske der Kiste doch wichtig würde. + lang: ini + code: | + ; crate.tscn + [node name="Crate" type="StaticBody2D"] + collision_mask = 0 + + ; player.tscn + [node name="Player" type="CharacterBody2D"] + collision_layer = 2 + collision_mask = 1 + points: + - Blockiert wird, wenn die Maske des bewegten Körpers den Layer des anderen enthält; die Figur sucht Ebene 1, die Kiste liegt dort. + - Die Kiste bewegt sich nie, ihre Maske wird beim Anstoßen nicht gefragt. + - Wichtig würde ihre Maske erst, wenn die Kiste selbst etwas finden müsste, etwa als `CharacterBody2D`, der sich bewegt und an Wänden hängen bleiben soll. + explanation: | + Ob ein Körper an einem anderen hängen bleibt, entscheidet die Maske dessen, der sich bewegt. Die Figur sucht Ebene 1, die Kiste liegt dort (der Standard, deshalb fehlt die Zeile `collision_layer`), also bleibt die Figur stehen. Die Kiste bewegt sich nie und muss niemanden finden; Maske 0 macht das sichtbar. Gemessen mit Godot 4.7.2: Mit Maske 0, 1 oder 2 bei der Kiste bleibt die Figur an derselben Stelle stehen, bei y ≈ 16. Würde aus der Kiste ein Körper, der sich selbst bewegt und anstoßen soll, bräuchte sie eine passende Maske. + resources: + - title: Godot Docs · CollisionObject2D.collision_mask + url: https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-property-collision-mask + + - id: ebenen-planen + type: practice + title: Deine Ebenentabelle + prompt: | + Plane die Kollisionsebenen für dein eigenes Spiel, so weit du es heute kennst. Schreib in kurzen Zeilen: Nummer und Name jeder Ebene, welche Objekte dort liegen und welche Objekte sie suchen. Welche Ebenen du brauchst, entscheidest du; die Tabelle des Testgeländes ist nur ein Beispiel für die Form. + lang: prose + checklist: + - Jede Ebene hat eine Nummer und einen Namen, den du in den Projekteinstellungen so eintragen kannst. + - Für jede Art von Objekt steht da, auf welcher Ebene es liegt und welche Ebenen es sucht. + - Bei jeder Area steht, ob sie melden soll (`monitoring`) und ob andere Areas sie finden dürfen (`monitorable`). + - Du hast mindestens ein Paar notiert, das sich absichtlich nicht finden soll, und geprüft, dass deine Tabelle das sicherstellt. + - Kampfflächen (Hitbox, Hurtbox) stehen getrennt von den Formen für Wände und Füße, falls dein Spiel sie braucht. + hints: + - Fang mit dem an, was blockiert, dann mit dem, was meldet. + - Prüf jedes Paar mit den drei Fragen aus der Lektion. + explanation: | + Die Tabelle ist die Vorgabe, die du einem Coding-Agenten mitgibst und mit der du später jeden Kollisionsfehler prüfst. Ob dein Spiel drei oder zehn Ebenen braucht und wie sie heißen, hängt an deinen Objekten und ist deine Entscheidung. Wichtig ist, dass jede Paarung, die etwas auslösen soll, über die Regel „Maske des Suchenden enthält Layer des Ziels“ erklärbar ist. + resources: + - title: Godot Docs · Physics introduction · Collision layers and masks + url: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks diff --git a/content/godot/modules/level-tilemap.yaml b/content/godot/modules/level-tilemap.yaml new file mode 100644 index 0000000..5565099 --- /dev/null +++ b/content/godot/modules/level-tilemap.yaml @@ -0,0 +1,555 @@ +id: level-tilemap +title: "2D-Level aus Tiles: TileSet, TileMapLayer und Y-Sortierung" +level: 2 +minutes: 170 +summary: Aus einem selbst gemachten Prüfmuster ein TileSet bauen, Boden und Wände im Testgelände mit TileMapLayer malen, Wänden Kollision geben, hohe Tiles richtig mit der Figur sortieren und Linien zwischen Tiles erklären und beheben. +place: pc +goals: + - Du legst ein TileSet aus deinem eigenen Tile-Sheet an und stellst Tilegröße, Abstände, Trennung und Regionsgröße passend zum Bild ein. + - Du baust ein Level aus mehreren TileMapLayer-Nodes und malst mit den Werkzeugen der TileMap-Leiste. + - Du gibst Tiles über eine Physik-Ebene im TileSet Kollision, passend zu Layer und Maske der Figur. + - Du sortierst hohe Tiles mit der Figur über `y_sort_enabled` und `y_sort_origin` und sagst vorher, wann die Figur davor oder dahinter steht. + - Du rechnest mit `local_to_map()` und `map_to_local()` zwischen Pixeln und Zellen um und erklärst Linien zwischen Tiles mit Filter, Skalierung und Texture Padding. +resources: + - title: Godot Docs · Using TileSets + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html + - title: Godot Docs · Using TileMaps + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html + - title: Godot Docs · TileMapLayer + url: https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html + - title: Godot Docs · TileSetAtlasSource + url: https://docs.godotengine.org/en/stable/classes/class_tilesetatlassource.html +lesson: | + Der Boden des Testgeländes ist ein grünes `ColorRect`, ein Platzhalter. Für ein Level brauchst du Boden, Wände und Dinge, die höher sind als ein Feld, aus deinen eigenen Tiles. Sobald du damit anfängst, tauchen drei Fehler auf, die jeder kennt, der mit Tiles arbeitet: dünne Linien zwischen den Feldern, eine Figur, die durch Wände läuft, und eine Figur, deren Kopf unter einer Säule verschwindet, obwohl sie davor steht. Alle drei haben eine Einstellung als Ursache, keine Grafik. + + Am PC baust du jetzt den Boden des Testgeländes aus Tiles um, gibst Wänden Kollision und sortierst hohe Tiles mit der Figur. Ein Coding-Agent kann dir die Einrichtung abnehmen. Die Tiles selbst zeichnest du, und wie dein Level aussieht, entscheidest du. Für die Übungen nimmst du ein technisches Prüfmuster, keine gestalteten Tiles. Alle Bilder hier entstanden mit Godot 4.7.2 und einem solchen Prüfmuster; die Tilegröße 16 × 16 ist ein Platzhalter. + + ## TileSet und TileMapLayer: Daten und Bild getrennt + + Zwei Dinge arbeiten zusammen. Das **TileSet** ist eine Ressource: Es kennt die Tilegröße, die Bildquellen, aus denen Tiles kommen, und Ebenen für Physik, Terrains und Navigation ([Using TileSets](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html)). Die Standard-Tilegröße ist 16 × 16 ([TileSet.tile_size](https://docs.godotengine.org/en/stable/classes/class_tileset.html#class-tileset-property-tile-size)). Die häufigste Quelle ist ein Atlas, ein Bild, das Godot in gleich große Felder schneidet. Der **TileMapLayer** ist ein Node, der in jeder Zelle seines Rasters höchstens eine Tile aus diesem TileSet zeigt. + + Seit Godot 4.3 nimmst du für jede Ebene einen eigenen `TileMapLayer`, etwa `Ground` für den Boden, `Walls` für Wände, eine weitere für Deko. Der alte Node `TileMap`, der mehrere Ebenen in sich trug, ist als veraltet markiert ([TileMap](https://docs.godotengine.org/en/stable/classes/class_tilemap.html)). Findest du ihn in einem älteren Projekt, wandelt das TileMap-Panel ihn laut Doku in einzelne `TileMapLayer` um; im Werkzeugmenü heißt der Eintrag in 4.7.2 „Extrahieren von TileMap-Ebenen als einzelne TileMapLayer-Nodes“. + + Im Testgelände ersetzt `Ground` den Node `Floor`, und `Walls` hängt unter `Objects`, damit er mit der Figur sortiert wird: + + ![Szenen-Dock: Main mit den Kindern Ground (TileMapLayer), Objects mit Player, Crate, drei Pickups und Walls (TileMapLayer) sowie HUD mit Counter](/media/godot/level-tilemap/szene-tiles.png "Godot 4.7.2, Szenen-Dock des umgebauten Testgeländes") + + Aus dem Web kennst du Spritesheets mit CSS: Ein Bild, und jede Klasse schneidet einen Ausschnitt heraus. Das TileSet ist dieses Bild samt Ausschnitten, der TileMapLayer ein Raster, das in jeder Zelle nur auf einen Ausschnitt verweist. Ein Element pro Zelle gibt es nicht; Godot zeichnet benachbarte Tiles in Gruppen (`rendering_quadrant_size`, Standard 16). + + Speicher das TileSet als eigene Datei, etwa `res://tiles/testgelaende_tiles.tres`, damit alle Ebenen und Level dasselbe benutzen; die Doku empfiehlt das ausdrücklich ([Specifying the TileSet](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html#specifying-the-tileset-in-the-tilemaplayer)). Die `.tres` bleibt lesbar. Die gemalten Zellen dagegen speichert Godot 4.7.2 als Binärdaten in Base64, und die Kopfzeile der Szene steigt auf `format=4`: + + ```ini title=main.tscn + [gd_scene format=4 uid="uid://ef8tg5r8yt71"] + … + [node name="Walls" type="TileMapLayer" parent="Objects" unique_id=1644474482] + y_sort_enabled = true + tile_map_data = PackedByteArray("AAADAAIAAAAAAAEAAAD9/wIAAAAAAAEAAAD//wQAAAABAAEAAAAAAAQAAAABAAEAAAABAAQAAAABAAEAAAA=") + tile_set = ExtResource("1_lquwl") + ``` + + > [!PRAXIS] + > Ein Diff von `tile_map_data` sagt nichts. Commite Level-Änderungen einzeln mit einer Nachricht, was du gemalt hast, und leg bei Umbauten einen Screenshot in den Pull Request. Das TileSet als `.tres` liest du im Review Zeile für Zeile. + + ## Ein eigenes Prüfmuster als Atlas + + Bevor deine echten Tiles fertig sind, testest du mit einem Prüfmuster, das du in Aseprite oder Krita in wenigen Minuten anlegst. Es soll Fehler sichtbar machen, nicht gut aussehen: Felder in der Tilegröße mit einem gezeichneten Rahmen von 1 Pixel und einer Nummer, ein hohes Feld über zwei Zellen mit einer weißen Grundlinie und ein Feld ohne Rand neben einer Signalfarbe, damit Linien zwischen Tiles auffallen. + + ![Prüfmuster mit acht nummerierten Feldern: 1 und 2 grünlich, 3 und 8 magenta, 4 gelb, 5 ein hohes graues Feld über zwei Zellen mit weißer Unterkante, 6 dunkelgrau, 7 hellgrau ohne Rand](/media/godot/level-tilemap/pruefmuster@2x.png "Prüfmuster für die Bilder dieses Moduls, 64 × 48 Pixel, achtfach vergrößert") + + Exportier es als PNG im Maßstab 1, ohne Trim, damit das Raster stimmt, und leg es nach `res://tiles/`. Dann: + + 1. `TileMapLayer` `Ground` anlegen, im Inspektor bei Tile Set ein neues TileSet erzeugen und aufklappen. + 2. Tile Size einstellen, **bevor** du Tiles erzeugst. Die Doku verlangt das für das automatische Anlegen ([Using a tilesheet](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#using-a-tilesheet)). + 3. Im unteren Panel *TileSet* das PNG in die Liste der Tile-Quellen ziehen und die Frage, ob Tiles automatisch erzeugt werden sollen, mit Ja beantworten. Ganz durchsichtige Bereiche bekommen keine Tile. + + ![TileSet-Panel, Reiter Einrichtung: Atlas mit ID 0, Textur als Vorschaubild des Prüfmusters, Abstände 0, Trennung 0, Textur-Regionsgröße 16 × 16 (Beschriftung abgeschnitten: Textur-Regionsgrö…), Textur-Padding verwenden an (abgeschnitten: Textur-Padding ve…); rechts die Grund-Tiles 1 bis 8, das hohe Feld 5 schon als eine Tile über zwei Zellen](/media/godot/level-tilemap/tileset-atlas.png "Godot 4.7.2, TileSet-Panel mit dem Prüfmuster, Feld 5 schon als große Tile") + + Im Reiter *Einrichtung* stehen die Einstellungen des Atlas. Der deutsche Editor übersetzt hier auch die Eigenschaftsnamen: „Abstände“ ist `margins`, ein Rand außen um das ganze Bild; „Trennung“ ist `separation`, der Abstand zwischen zwei Tiles; „Textur-Regionsgröße“ ist `texture_region_size`, die Größe einer Tile im Bild; „Textur-Padding verwenden“ ist `use_texture_padding`. Im Panel sind die beiden letzten Beschriftungen abgeschnitten. Der gezeichnete Rahmen im Prüfmuster gehört zur Tile und ist kein Abstand. Exportiert dein Programm 1 Pixel Abstand zwischen den Tiles, trägst du bei Trennung 1 ein, sonst verrutscht jede Tile um einen Pixel mehr. + + Das hohe Feld 5 zerlegt das automatische Erzeugen in zwei einzelne Tiles, eine je Zelle; mit Godot 4.7.2 nachgeprüft, entstehen (0, 1) und (0, 2), beide 1 × 1. Für eine Tile über zwei Zellen: + + 1. Im Reiter *Einrichtung* das Radierer-Symbol über der Atlas-Vorschau einschalten (Taste E) und die beiden Tiles von Feld 5 anklicken. Danach den Radierer wieder ausschalten. + 2. Mit gedrückter Umschalttaste auf der oberen leeren Zelle beginnen und über beide Zellen ziehen. Das Panel nennt das selbst: „Shift gedrückt halten um große Tiles zu erzeugen“. Laut Quelltext von 4.7.2 beginnt eine große Tile nur auf einer leeren Zelle, deshalb der erste Schritt. + 3. In der `.tres` steht danach `0:1/size_in_atlas = Vector2i(1, 2)`. + + Godot zeichnet eine solche Tile mittig auf ihre Zelle, also ragt sie 8 Pixel nach unten über die Zelle hinaus. Mit `texture_origin = Vector2i(0, 8)` rückt das Bild 8 Pixel nach oben, und die Grundlinie sitzt auf der Unterkante der Zelle; gemessen mit Godot 4.7.2 ([TileData.texture_origin](https://docs.godotengine.org/en/stable/classes/class_tiledata.html#class-tiledata-property-texture-origin)). Du stellst das an der Tile ein: TileSet-Panel, Reiter *Auswählen*, Feld 5 anklicken, dann in der mittleren Spalte unter *Rendern* der Wert „Texturursprung“. Dort steht auch der „Y-Sortierungsursprung“, den du weiter unten brauchst: + + ![Mittlere Spalte des TileSet-Panels im Reiter Auswählen für Feld 5: Abschnitt Rendern mit Texturursprung x 0, y 8, Modulation, Material, Z-Index 0 und Y-Sortierungsursprung 8 (Beschriftung abgeschnitten: Y-Sortierungsursp…), darunter Terrains, Sonstiges und Physik](/media/godot/level-tilemap/tile-eigenschaften.png "Godot 4.7.2, Eigenschaften der hohen Tile im TileSet-Panel") + + ## Malen: die TileMap-Leiste + + Wählst du `Ground` aus, öffnet sich unten das Panel *TileMap* mit den Reitern Tiles, Muster und Terrains und einer Werkzeugleiste ([Painting modes and tools](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html#painting-modes-and-tools)): + + ![TileMap-Panel: Reiter Tiles, Muster, Terrains, daneben Werkzeuge Auswahl, Malen, Linie, Rechteck, Eimer, Pipette, Radierer sowie Drehen, Spiegeln und Zufall; darunter die Grund-Tiles des Prüfmusters](/media/godot/level-tilemap/tilemap-leiste.png "Godot 4.7.2, TileMap-Panel mit Werkzeugleiste und Atlas") + + | Werkzeug (Godot 4.7.2) | Taste | wofür | + | --- | --- | --- | + | Auswahlwerkzeug | S | Zellen markieren, verschieben, als Muster kopieren | + | Malwerkzeug | D | Zelle für Zelle; mit Umschalt eine Linie, mit Ctrl+Umschalt ein Rechteck | + | Linienwerkzeug | L | gerade Linien | + | Rechteckswerkzeug | R | Flächen | + | Eimerwerkzeug | B | zusammenhängende Fläche füllen | + | Pipettenwerkzeug | P | Tile aus der Karte aufnehmen; geht auch mit gehaltener Ctrl-Taste | + | Radiergummi Tool | E | Zellen leeren; geht auch mit der rechten Maustaste | + + Z und X drehen die gewählte Tile, C und V spiegeln sie. Mit der Würfel-Schaltfläche „Zufällige Tile platzieren“ wählt Godot beim Malen zufällig eine der gewählten Tiles. Jeder Layer hat in jeder Zelle höchstens eine Tile; für Deko über dem Boden brauchst du deshalb einen zweiten Layer. + + Drei weitere Werkzeuge in Kürze. **Muster** (Patterns) speichern gemalte Zellen zum Wiederverwenden, im TileSet, nicht in der Szene ([Patterns](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html#saving-and-loading-premade-tile-placements-using-patterns)). **Terrains** setzen Übergänge automatisch: Du ordnest im TileSet jeder Ecke und Kante einer Tile ein Terrain zu und malst dann nur noch das Terrain ([Creating terrain sets](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#creating-terrain-sets-autotiling)). **Szenen-Tiles** setzen ganze Szenen ins Raster ([Using a collection of scenes](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#using-a-collection-of-scenes)). Navigation legst du ebenfalls im TileSet an. + + ## Kollision aus dem TileSet + + Wände bekommen ihre Kollision nicht am Node, sondern im TileSet. Im Inspektor des TileSet legst du unter *Physics Layers* eine Ebene an. Sie hat einen eigenen Collision Layer und eine Maske, beide standardmäßig Ebene 1. Danach zeichnest du im TileSet-Panel für jede Wand-Tile ein Kollisionspolygon, im Reiter *Auswählen* pro Tile oder im Reiter *Malen* für viele Tiles auf einmal ([Adding collision](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#adding-collision-navigation-and-occlusion-to-the-tileset)). In der `.tres` sieht das so aus: + + ```ini title=tiles/testgelaende_tiles.tres + [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_2f8qm"] + texture = ExtResource("1_p3wfx") + 0:0/0 = 0 + 1:0/0 = 0 + … + 1:1/0 = 0 + 1:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) + 0:1/size_in_atlas = Vector2i(1, 2) + 0:1/0 = 0 + 0:1/0/texture_origin = Vector2i(0, 8) + 0:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) + + [resource] + physics_layer_0/collision_layer = 1 + sources/0 = SubResource("TileSetAtlasSource_2f8qm") + ``` + + `1:1/0` heißt: Tile bei Spalte 1, Zeile 1 im Atlas, Variante 0. Die Punkte des Polygons liegen relativ zur Mitte der Zelle, ein volles 16er-Feld reicht also von −8 bis 8. Für die Säule `0:1` deckt das Polygon im Prüfmuster nur die untere Zelle ab, die Grundfläche. Die Folge kennst du von der Fußform der Figur aus dem [Modul über Kollisionen](/godot/kollision-ebenen): Die Figur kann hinter den oberen Teil laufen. Deckt das Polygon beide Zellen ab, bleibt sie schon am oberen Teil hängen; was deine hohen Tiles tun sollen, entscheidest du. + + Anders als bei Nodes schreibt Godot 4.7.2 die Zeile `physics_layer_0/collision_layer = 1` auch dann in die `.tres`, wenn sie dem Standard 1 entspricht; nachgeprüft mit einer frisch angelegten Physik-Ebene. + + Damit die Figur an den Wänden hängen bleibt, muss ihre Maske den Layer der Physik-Ebene enthalten. Die Figur sucht Ebene 1 „world“, also bekommt die Physik-Ebene Layer 1. Am `TileMapLayer` selbst muss *Collision Enabled* an sein, das ist der Standard ([TileMapLayer.collision_enabled](https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html#class-tilemaplayer-property-collision-enabled)). Gemessen mit Godot 4.7.2: Läuft die Figur von unten auf eine Wandreihe zu, deren Zellen bei y = 80 enden, bleibt sie bei y = 86 stehen. Mit Layer 2 an der Physik-Ebene oder mit Collision Enabled aus läuft sie hindurch. + + ## Y-Sortierung mit hohen Tiles + + Die Säule ist höher als eine Zelle. Steht die Figur dahinter, soll die Säule sie verdecken, steht sie davor, umgekehrt. Dafür muss `Walls` an derselben Y-Sortierung teilnehmen wie die Figur: als Kind von `Objects` und mit eigenem *Y Sort Enabled*. Dann sortiert Godot die Tiles nach ihrer y-Position zwischen die Geschwister ein; die Doku vermerkt, dass die Rendering-Quadranten bei einem sortierten Layer nicht gelten, weil die Tiles nach ihrer Y-Position gruppiert werden ([Multiple TileMapLayers and settings](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html#multiple-tilemaplayers-and-settings)). + + ![Zwei Spielausschnitte mit Y-Sortierung: links steht die blaue Figur hinter der grauen Säule, nur ihr rechter Rand ist neben der Säule zu sehen; rechts steht sie davor und verdeckt den unteren Teil der Säule](/media/godot/level-tilemap/ysort-richtig@2x.png "Godot 4.7.2, Walls unter Objects mit Y Sort Enabled, sechsfach vergrößert") + + Im Inspektor findest du beides, wenn du „y sort“ in das Filterfeld tippst: + + ![Inspektor von Walls mit Filter y sort: unter Rendering Y Sort Origin 0, unter Ordering Y Sort Enabled an mit Zurücksetzen-Pfeil](/media/godot/level-tilemap/walls-ysort.png "Godot 4.7.2, Inspektor von Walls, gefiltert") + + Wo genau eine Tile einsortiert wird, bestimmt ihr Sortierpunkt: die Mitte der Zelle plus `y_sort_origin` der Tile plus `y_sort_origin` des Layers ([TileData.y_sort_origin](https://docs.godotengine.org/en/stable/classes/class_tiledata.html#class-tiledata-property-y-sort-origin), [TileMapLayer.y_sort_origin](https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html#class-tilemaplayer-property-y-sort-origin)). Die Figur wird nach ihrem Ursprung an den Füßen sortiert. Gemessen mit Godot 4.7.2, Säule in der Zellenreihe 2 (y = 32 bis 48, Mitte 40), ohne Kollision: + + | `y_sort_origin` der Säule | Sortierpunkt | Figur bei y = 39 | Figur bei y = 41 | Figur bei y = 49 | + | --- | --- | --- | --- | --- | + | 0 | 40 | dahinter | davor | davor | + | 8 | 48 | dahinter | dahinter | davor | + + > [!MERKE] + > Eine Tile wird an der Mitte ihrer Zelle plus `y_sort_origin` einsortiert, die Figur an ihrem Ursprung. Leg den Sortierpunkt dorthin, wo im Bild der Fuß der Tile steht. + + Wo der Fuß einer Tile liegt, entscheidet deine Zeichnung. Beim Prüfmuster ist es die weiße Grundlinie auf der Unterkante der Zelle, also `y_sort_origin` 8. Deckt die Kollision die ganze untere Zelle ab, fällt der Unterschied nur seitlich neben der Säule auf. + + Fehlt *Y Sort Enabled* an `Walls`, sortiert sich der ganze Layer als ein Stück, an seinem eigenen Ursprung bei y = 0. Dann steht die Figur bei y = 31 hinter der Säule, wird aber darüber gezeichnet, weil 31 größer ist als 0: + + ![Spielausschnitt: Die Figur steht hinter der Säule, wird aber über sie gezeichnet](/media/godot/level-tilemap/ysort-falsch@2x.png "Godot 4.7.2, Walls ohne Y Sort Enabled, sechsfach vergrößert") + + > [!INTERVIEW] + > „Wie sorgst du in einem Top-down-Level dafür, dass Figuren vor und hinter hohen Objekten richtig erscheinen?“ Gute Antwort: Figuren und hohe Objekte hängen unter einem gemeinsamen Node mit Y-Sortierung, die Figuren haben ihren Ursprung an den Füßen, und der Tile-Layer mit den hohen Objekten ist selbst Y-sortiert. Dann setze ich den Sortierpunkt jeder hohen Tile auf ihren Fuß, und wo Figuren hinter ein Objekt treten sollen, liegt die Kollision nur auf der Grundfläche. Prüfen würde ich es an den kritischen Stellen: direkt davor, direkt dahinter, seitlich daneben. + + ## Nähte zwischen Tiles: Ursache und Abhilfe + + Dünne Linien zwischen Tiles, die beim Bewegen oder Zoomen flackern, heißen Nähte oder Bluten (bleeding). Sie entstehen, wenn Godot eine Tile nicht pixelgenau zeichnet und der Filter am Rand der Tile Pixel aus dem Nachbarfeld im Atlas mitmischt. Nachgestellt mit Godot 4.7.2: Boden nur aus Feld 7, dessen Nachbar im Atlas magenta ist, Filter Linear am `Ground`, Kamera-Zoom 1,5. + + ![Zwei Ausschnitte eines grauen Bodens aus Feld 7: links mit Texture Padding aus, zwischen allen Tiles magentafarbene Linien; rechts mit Texture Padding an, keine Linien](/media/godot/level-tilemap/naht@2x.png "Godot 4.7.2, Filter Linear, Zoom 1,5: links ohne, rechts mit Texture Padding, vierfach vergrößert") + + Links ist „Textur-Padding verwenden“ (`use_texture_padding`) aus, rechts an, sonst ist alles gleich. Laut Doku legt Godot mit dieser Einstellung eine interne Textur mit einem zusätzlichen Pixel Rand um jede Tile an; das vermeidet genau diese Linien ([use_texture_padding](https://docs.godotengine.org/en/stable/classes/class_tilesetatlassource.html#class-tilesetatlassource-property-use-texture-padding)). Sie ist standardmäßig an. Mit Filter Nearest zeigte derselbe Test auch ohne Padding keine Linien, dafür ungleich breite Pixel; warum, erklärt das [Modul über Pixelraster](/godot/pixelraster). + + Prüf in dieser Reihenfolge: Texture Padding an? Filter Linear mit Skalierung oder Zoom? Abstände, Trennung und Regionsgröße passend zum Bild? Eine falsche Trennung zeigt keine dünne Naht, sondern an jeder Tile einen Streifen der Nachbarin. + + > [!WARNING] Erst Tile Size, dann Tiles + > Erzeugst du die Tiles automatisch, bevor die Tilegröße stimmt, schneidet Godot das Bild falsch. Nach einer Änderung an Abständen, Trennung oder Regionsgröße können Tiles verloren gehen; die Doku zeigt, wie du sie über das Menü mit den drei Punkten im TileSet-Panel neu erzeugst. + + ## Häufige Fehler + + - **Alles auf einem Layer.** Pro Zelle passt nur eine Tile. Deko über dem Boden braucht einen eigenen `TileMapLayer`. + - **TileSet in der Szene vergraben.** Ein zweites Level kann ein eingebettetes TileSet nicht mitbenutzen. Als `.tres` speichern. + - **Trennung vergessen.** Das Bild hat 1 Pixel zwischen den Tiles, im Atlas steht 0: Jede Tile zeigt einen Streifen der Nachbarin. + - **Kollision auf der falschen Ebene.** Die Figur sucht den Layer der Physik-Ebene nicht und läuft durch Wände, obwohl die Formen sichtbar sind. + - **Kollision über die ganze hohe Tile, obwohl die Figur dahinter laufen soll.** Sie bleibt am oberen Teil hängen. Dafür gehört die Kollision auf die Grundfläche. + - **Walls außerhalb der Y-Sortierung.** Ohne *Y Sort Enabled* oder außerhalb von `Objects` liegt die Figur immer davor oder immer dahinter. + - **Sortierpunkt in der Zellenmitte bei hohen Tiles.** Neben der Säule erscheint die Figur davor, obwohl ihre Füße dahinter stehen. `y_sort_origin` auf den Fuß der Tile setzen. + - **Texture Padding ausgeschaltet.** Mit Linear und Zoom erscheinen Linien in der Farbe der Nachbartiles. + + ## Zusammenfassung + + - Das TileSet ist eine Ressource mit Tilegröße, Quellen und Ebenen; als `.tres` gespeichert teilen es alle Layer. Ein `TileMapLayer` zeigt pro Zelle eine Tile; seit 4.3 ersetzt er den veralteten `TileMap`. + - Ein Atlas schneidet ein Bild mit Abständen, Trennung und Regionsgröße; Tile Size stellst du vor dem automatischen Erzeugen ein. Hohe Tiles bekommen `size_in_atlas` und `texture_origin`. + - Die TileMap-Leiste hat Auswahl (S), Malen (D), Linie (L), Rechteck (R), Eimer (B), Pipette (P) und Radierer (E); Muster, Terrains und Szenen-Tiles kommen dazu. + - Kollision kommt aus der Physik-Ebene des TileSet. Ihr Layer muss in der Maske der Figur liegen. Kollidiert eine hohe Tile nur mit ihrer Grundfläche, kann die Figur hinter sie laufen. + - Für die Sortierung mit der Figur hängt der Layer in der Y-Sortierung und hat selbst *Y Sort Enabled*. Sortiert wird an Zellenmitte plus `y_sort_origin`. + - `local_to_map()` rechnet lokale Pixel in Zellen um, `map_to_local()` gibt die Mitte einer Zelle zurück. + - Linien zwischen Tiles kommen von Filter und Skalierung; Texture Padding (Standard an) verhindert sie. + - `tile_map_data` ist binär; Level-Änderungen dokumentierst du in Commits und Screenshots, das TileSet bleibt als `.tres` lesbar. +exercises: + - id: tileset-oder-layer + type: choice + title: Was gehört wohin? + prompt: | + Welche Aussagen über TileSet und TileMapLayer in Godot 4.7 stimmen? + options: + - text: Kollisionspolygone für Wände legst du im TileSet an, nicht am `TileMapLayer`. + correct: true + why: Die Physik-Ebene und die Polygone gehören zur Tile im TileSet. Der Layer schaltet Kollision nur an oder aus. + - text: Ein `TileMapLayer` kann in einer Zelle zwei Tiles übereinander zeigen. + correct: false + why: Pro Layer und Zelle gibt es höchstens eine Tile. Für Übereinander brauchst du einen zweiten Layer. + - text: Speicherst du das TileSet als `.tres`, können Ground und Walls dasselbe TileSet benutzen. + correct: true + why: Eine Ressource als Datei lässt sich von mehreren Nodes teilen; die Doku empfiehlt das für Projekte mit mehreren Levels. + - text: Neue Projekte sollen `TileMap` mit mehreren Ebenen benutzen, weil `TileMapLayer` nur eine Ebene hat. + correct: false + why: "`TileMap` ist veraltet. Mehrere Ebenen sind mehrere `TileMapLayer`-Nodes." + explanation: | + Das TileSet beschreibt, was eine Tile ist: Bild, Kollision, Sortierpunkt, Terrain. Der `TileMapLayer` beschreibt, wo welche Tile liegt, eine pro Zelle. Diese Trennung macht das TileSet wiederverwendbar: Ändert sich die Kollision einer Wand-Tile, ändert sie sich in jedem Level. + resources: + - title: Godot Docs · Using TileMaps · Multiple TileMapLayers and settings + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html#multiple-tilemaplayers-and-settings + + - id: atlas-anlegen + type: order + title: Vom PNG zur ersten Tile + prompt: | + Bring die Schritte in eine Reihenfolge, die ohne Nacharbeit funktioniert. + lang: prose + lines: | + Prüfmuster als PNG im Maßstab 1 nach res://tiles exportieren + TileMapLayer Ground anlegen + Im Inspektor von Ground ein neues TileSet erzeugen + Tile Size im TileSet passend zum Prüfmuster einstellen + PNG ins TileSet-Panel ziehen und Tiles automatisch erzeugen lassen + Im TileMap-Panel eine Tile wählen und mit dem Malwerkzeug setzen + alternatives: + - [2, 1, 3, 4, 5, 6] + - [2, 3, 1, 4, 5, 6] + - [2, 3, 4, 1, 5, 6] + explanation: | + Das PNG muss im Projekt liegen, bevor du es ins Panel ziehst; wann du es exportierst, ist sonst egal. Fest ist die Mitte: TileSet erzeugen, dann Tile Size, dann automatisch Tiles anlegen. Die Doku verlangt die Tilegröße vor dem automatischen Erzeugen, sonst schneidet Godot das Bild im falschen Raster. Malen kannst du erst, wenn es Tiles gibt. + resources: + - title: Godot Docs · Using TileSets · Using a tilesheet + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#using-a-tilesheet + + - id: atlas-werte + type: choice + title: Ein Sheet mit Rand und Abstand + prompt: | + Dein Programm exportiert ein Tile-Sheet mit Tiles von 16 × 16 Pixeln, 1 Pixel Abstand zwischen den Tiles und 2 Pixeln Rand um das ganze Bild. Welche Einstellungen trägst du im TileSet-Panel unter *Einrichtung* ein? + + ![TileSet-Panel, Reiter Einrichtung, mit den Feldern Abstände, Trennung, Textur-Regionsgröße und Textur-Padding verwenden](/media/godot/level-tilemap/tileset-atlas.png "Godot 4.7.2, TileSet-Panel") + options: + - text: Abstände 2, Trennung 1, Textur-Regionsgröße 16 × 16 + correct: true + why: Abstände sind der Rand außen (`margins`), Trennung der Abstand zwischen den Tiles (`separation`), die Regionsgröße die Tile selbst. + - text: Abstände 1, Trennung 2, Textur-Regionsgröße 16 × 16 + correct: false + why: Vertauscht. Mit Trennung 2 verrutscht jede Tile um einen Pixel mehr nach rechts und unten. + - text: Abstände 2, Trennung 1, Textur-Regionsgröße 17 × 17 + correct: false + why: Der Abstand gehört nicht zur Tile. Mit 17 × 17 enthielte jede Tile eine Spalte und Zeile des Abstands. + - text: Abstände 0, Trennung 0, Textur-Regionsgröße 16 × 16; Godot erkennt Rand und Abstand selbst + correct: false + why: Das automatische Erzeugen schneidet im eingestellten Raster. Rand und Abstand musst du angeben. + explanation: | + Das Raster eines Atlas ist: Rand außen, dann Tile, Abstand, Tile, Abstand … Godot rechnet die Position jeder Tile aus diesen drei Werten. Stimmt einer nicht, sitzt jede weitere Tile weiter daneben, und am Rand jeder Tile erscheint ein Streifen der Nachbarin. Am einfachsten ist ein Export ohne Rand und Abstand; wenn dein Programm beides schreibt, trägst du es hier ein. + resources: + - title: Godot Docs · TileSetAtlasSource.separation + url: https://docs.godotengine.org/en/stable/classes/class_tilesetatlassource.html#class-tilesetatlassource-property-separation + + - id: zelle-berechnen + type: output + title: Pixel in Zellen + prompt: | + `walls` ist ein `TileMapLayer` mit Tile Size 16 × 16 an der Position (0, 0). Was gibt Godot 4.7 aus? + lang: gdscript + code: | + print(walls.local_to_map(Vector2(37, -5))) + print(walls.map_to_local(Vector2i(3, 2))) + expected: | + (2, -1) + (56.0, 40.0) + accept: + - | + (2,-1) + (56.0,40.0) + - | + (2, -1) + (56, 40) + - | + (2,-1) + (56,40) + hints: + - "`local_to_map` teilt durch 16 und rundet ab, auch bei negativen Zahlen." + - "`map_to_local` liefert die Mitte der Zelle, nicht die Ecke." + - "`print` zeigt einen `Vector2` mit Nachkommastelle: `(56.0, 40.0)`." + explanation: | + 37 / 16 = 2,3, abgerundet 2; −5 / 16 = −0,3, abgerundet −1. Die Zelle (3, 2) reicht von (48, 32) bis (64, 48), ihre Mitte ist (56, 40). `local_to_map` gibt ganze Zellen als `Vector2i`, `map_to_local` einen `Vector2` mit Kommazahlen zurück. Gemessen mit Godot 4.7.2. Laut Doku ignoriert `map_to_local` den `texture_origin` einzelner Tiles; bei der Säule liegt das Bild also höher als dieser Punkt. + resources: + - title: Godot Docs · TileMapLayer.local_to_map + url: https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html#class-tilemaplayer-method-local-to-map + - title: Godot Docs · TileMapLayer.map_to_local + url: https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html#class-tilemaplayer-method-map-to-local + + - id: kacheln-je-bild + type: output + title: Wie viele Tiles sieht man? + prompt: | + Das Testgelände rechnet in 320 × 180 Pixeln, die Tiles sind 16 × 16 (Platzhalter). Die linke obere Bildecke liegt genau auf einer Tile-Ecke. Wie viele Tiles sind ganz oder teilweise im Bild? + lang: text + code: | + Bild 320 × 180 px + Tile 16 × 16 px + expected: | + 240 + accept: + - 240 Tiles + - 240 Kacheln + - 20 × 12 + - 20 x 12 + - 20 × 12 = 240 + - 20 x 12 = 240 + - 20 * 12 = 240 + hints: + - Rechne Spalten und Zeilen getrennt. + - 180 / 16 geht nicht auf. Auch eine angeschnittene Zeile zählt. + explanation: | + 320 / 16 = 20 Spalten. 180 / 16 = 11,25, also 12 Zeilen, die unterste nur zu einem Viertel sichtbar. 20 × 12 = 240. Verschiebt sich die Kamera um einen Bruchteil, kommt am Rand eine weitere Spalte dazu und je nach Verschiebung eine Zeile, bis zu 21 × 13. Solche Zahlen brauchst du, wenn du ein Level auf Bildschirmgrößen planst; welche Tilegröße und Spielauflösung du nimmst, entscheidest du mit deiner Pixelart. + resources: + - title: Godot Docs · TileSet.tile_size + url: https://docs.godotengine.org/en/stable/classes/class_tileset.html#class-tileset-property-tile-size + + - id: vor-oder-hinter + type: output + title: Vor oder hinter der Säule? + prompt: | + Die Säule liegt in der Zelle (3, 2) eines Y-sortierten `Walls`, Zellen 16 × 16, also y = 32 bis 48. Sie hat hier keine Kollision. Die Figur steht neben ihr, ihr Ursprung an den Füßen liegt bei y = 44, ihr Bild überlappt die Säule. Steht die Figur im Bild davor oder dahinter, einmal mit `y_sort_origin` 0 und einmal mit 8 an der Säule? Antworte in zwei Zeilen mit „davor“ oder „dahinter“. + lang: text + code: | + Säule Zelle (3, 2), Mitte y = 40 + Figur y = 44 + Fall 1 y_sort_origin = 0 + Fall 2 y_sort_origin = 8 + expected: | + davor + dahinter + accept: + - | + Davor + Dahinter + - | + vorn + hinten + - | + Vorn + Hinten + - | + vor der Säule + hinter der Säule + - | + Vor der Säule + Hinter der Säule + hints: + - Der Sortierpunkt der Tile ist die Zellenmitte plus `y_sort_origin`. + - Wer den größeren y-Wert hat, wird später gezeichnet, also obendrauf. + explanation: | + Mit 0 liegt der Sortierpunkt der Säule bei 40. Die Figur bei 44 hat den größeren Wert und wird darüber gezeichnet: davor. Mit 8 liegt der Punkt bei 48, auf der Grundlinie; jetzt ist die Säule weiter unten, und die Figur erscheint dahinter. Gemessen mit Godot 4.7.2 an einem Pixel der Säule. Welcher Fall richtig aussieht, hängt davon ab, wo im Bild der Fuß der Säule steht. + resources: + - title: Godot Docs · TileData.y_sort_origin + url: https://docs.godotengine.org/en/stable/classes/class_tiledata.html#class-tiledata-property-y-sort-origin + + - id: zelle-pruefen + type: gap + title: Steht die Figur auf einer Wand-Zelle? + prompt: | + Ein Testskript soll ausgeben, ob in der Zelle unter der Figur eine Tile von `walls` liegt. `walls` und `player` sind gesetzt. Ergänze die drei Methoden. + lang: gdscript + code: | + var cell := walls.⟦local_to_map⟧(walls.⟦to_local⟧(player.global_position)) + if walls.⟦get_cell_source_id⟧(cell) == -1: + print("frei: ", cell) + else: + print("Tile in ", cell) + gaps: + - label: Pixel zu Zelle + hint: Die Methode des Layers, die lokale Pixel in Zellen umrechnet. + - label: Global zu lokal + hint: Eine Methode von `Node2D`; die Doku nennt sie bei `local_to_map`. + - label: Quelle der Zelle + hint: Liefert −1, wenn die Zelle leer ist. + explanation: | + `local_to_map` erwartet lokale Koordinaten des Layers; liegt der Layer nicht bei (0, 0), rechnest du globale Positionen erst mit `to_local` um, wie die Doku es empfiehlt. `get_cell_source_id` gibt die ID der Quelle zurück oder −1 für eine leere Zelle; mit Godot 4.7.2 geprüft. So prüft ein Skript Untergründe oder Fallen, ohne Kollisionen zu brauchen. + resources: + - title: Godot Docs · TileMapLayer.get_cell_source_id + url: https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html#class-tilemaplayer-method-get-cell-source-id + + - id: walls-tscn + type: gap + title: Walls im Text + prompt: | + Ergänze die Zeilen in `main.tscn`, damit `Walls` mit der Figur sortiert wird. + lang: ini + code: | + [node name="Walls" type="TileMapLayer" parent="⟦Objects⟧"] + ⟦y_sort_enabled⟧ = true + tile_set = ExtResource("1_lquwl") + gaps: + - label: Elternknoten + hint: Der Node, unter dem auch die Figur hängt und der selbst Y-sortiert. + - label: Eigenschaft + hint: Die Eigenschaft aus CanvasItem → Ordering, im Code mit Unterstrichen. + explanation: | + Nur Geschwister unter demselben Y-sortierten Elternknoten werden miteinander sortiert. `parent="Objects"` setzt `Walls` neben die Figur, `y_sort_enabled = true` lässt die Tiles nach ihrer y-Position mitsortieren. Fehlt die zweite Zeile, sortiert sich der ganze Layer an seinem Ursprung. Die `tile_map_data`-Zeile ist hier weggelassen; sie enthält die gemalten Zellen. + resources: + - title: Godot Docs · Using TileMaps · Multiple TileMapLayers and settings + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html#multiple-tilemaplayers-and-settings + + - id: wand-ebene + type: bug + title: Durch die Wand + prompt: | + Nach einem Umbau läuft die Figur durch die Wände. Mit *Debuggen → Sichtbare Collision-Shapes* siehst du die Formen der Wand-Tiles, und *Collision Enabled* an `Walls` ist an. Die Figur hat Layer 2 und Maske 1. Finde in der TileSet-Datei die Zeile und korrigiere sie. + lang: ini + code: | + [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_2f8qm"] + texture = ExtResource("1_p3wfx") + 1:1/0 = 0 + 1:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) + + [resource] + physics_layer_0/collision_layer = 2 + sources/0 = SubResource("TileSetAtlasSource_2f8qm") + lines: [7] + fix: + - line: 7 + answers: + - physics_layer_0/collision_layer = 1 + explanation: | + Die Wand-Tiles liegen auf Ebene 2. Die Figur sucht mit ihrer Maske nur Ebene 1, also bleibt sie nicht hängen, obwohl die Formen da sind und angezeigt werden. Gemessen mit Godot 4.7.2: Mit Layer 2 läuft die Figur durch die Wandreihe, mit Layer 1 bleibt sie bei y = 86 stehen. Es ist dieselbe Regel wie bei jedem Körper: Die Maske des Suchenden muss den Layer des anderen enthalten. + resources: + - title: Godot Docs · Using TileSets · Adding collision + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#adding-collision-navigation-and-occlusion-to-the-tileset + + - id: naht-padding + type: bug + title: Linien im Boden + prompt: | + Für einen Test mit Kamera-Zoom 1,5 hast du am `Ground` bewusst den Filter Linear eingestellt. Seitdem siehst du zwischen allen Tiles farbige Linien wie links im Bild: + + ![Grauer Boden mit magentafarbenen Linien zwischen allen Tiles, daneben derselbe Boden ohne Linien](/media/godot/level-tilemap/naht@2x.png "Godot 4.7.2, links der Fehler, rechts nach der Korrektur") + + Zoom und Filter sollen bleiben. Welche Zeile verursacht die Linien? Korrigiere sie. + lang: ini + code: | + ; tiles/testgelaende_tiles.tres + [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_2f8qm"] + texture = ExtResource("1_p3wfx") + use_texture_padding = false + ; main.tscn + [node name="Ground" type="TileMapLayer" parent="."] + texture_filter = 2 + lines: [4] + fix: + - line: 4 + answers: + - use_texture_padding = true + explanation: | + Bei Zoom 1,5 und Filter Linear mischt der Filter am Rand jeder Tile Pixel aus dem Nachbarfeld im Atlas mit; hier ist das ein magentafarbenes Feld. Texture Padding legt um jede Tile einen zusätzlichen Pixel, und die Linien verschwinden; so war es im Test mit Godot 4.7.2. Der Standard ist `true`, deshalb fehlt die Zeile normalerweise ganz. `texture_filter = 2` ist Linear und war hier gewollt. + resources: + - title: Godot Docs · TileSetAtlasSource.use_texture_padding + url: https://docs.godotengine.org/en/stable/classes/class_tilesetatlassource.html#class-tilesetatlassource-property-use-texture-padding + + - id: sortierung-erklaeren + type: explain + title: Warum die Figur über der Säule liegt + prompt: | + Im Bild steht die Figur mit den Füßen bei y = 31 hinter der Säule (deren Grundlinie bei y = 48), wird aber über sie gezeichnet. `Walls` hängt unter `Objects`. Erkläre in drei bis fünf Sätzen, was hier passiert und was du einstellst, damit es stimmt. + + ![Die Figur steht hinter der Säule, wird aber über sie gezeichnet](/media/godot/level-tilemap/ysort-falsch@2x.png "Godot 4.7.2, Walls ohne Y Sort Enabled") + lang: ini + code: | + [node name="Objects" type="Node2D" parent="."] + y_sort_enabled = true + + [node name="Walls" type="TileMapLayer" parent="Objects"] + tile_set = ExtResource("1_lquwl") + points: + - "`Objects` sortiert seine Kinder nach y; `Walls` ist ohne eigenes `y_sort_enabled` dabei ein einziges Stück mit Sortierpunkt y = 0." + - Die Figur hat y = 31, also größer als 0, und wird nach dem ganzen Layer gezeichnet; das gilt überall, wo ihr y größer als 0 ist. + - Mit `y_sort_enabled = true` an `Walls` sortiert Godot die Tiles nach ihrer y-Position ein, an Zellenmitte plus `y_sort_origin`. + - Für die Säule legst du `y_sort_origin` auf ihren Fuß, beim Prüfmuster 8, damit die Figur seitlich daneben richtig erscheint. + explanation: | + Ohne eigenes *Y Sort Enabled* ist `Walls` für `Objects` ein einzelnes Kind mit seiner Position (0, 0). Die Figur bei y = 31 kommt danach und liegt deshalb über jeder Tile des Layers; eine Figur oberhalb von y = 0 läge unter allen. Mit `y_sort_enabled = true` werden die Tiles nach ihrer y-Position einsortiert, an Zellenmitte plus `y_sort_origin`. Gemessen mit Godot 4.7.2: Mit Sortierung erscheint die Figur bei y = 31 hinter der Säule, bei y = 55 davor. Den Sortierpunkt setzt du auf den Fuß der Tile, den deine Zeichnung festlegt. + resources: + - title: Godot Docs · TileMapLayer.y_sort_origin + url: https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html#class-tilemaplayer-property-y-sort-origin + + - id: boden-und-waende + type: practice + title: Das Testgelände aus Tiles + prompt: | + Bau das Testgelände um. Leg dir zuerst ein eigenes Prüfmuster an (Tilegröße wie deine geplanten Tiles oder 16 × 16 als Platzhalter, gezeichneter Rahmen von 1 Pixel, Nummern, ein hohes Feld mit Grundlinie, ein Feld ohne Rahmen neben einer Signalfarbe). Ersetze dann `Floor` durch einen `TileMapLayer` `Ground` als erstes Kind von `Main`, leg `Walls` unter `Objects` an, mach aus dem hohen Feld eine Tile über zwei Zellen und gib den Wand-Tiles Kollision. Beim hohen Feld wählst du die Kollision nach deiner Absicht; im Prüfmuster liegt sie auf der Grundfläche, damit die Figur dahinter laufen kann. Prüf Kollision und Sortierung im laufenden Spiel. Wo du malst, entscheidest du. + lang: ini + solution: | + ; So sah der Umbau in Godot 4.7.2 aus (Ausschnitte, Werte Platzhalter). + + ; main.tscn + [node name="Ground" type="TileMapLayer" parent="."] + tile_map_data = PackedByteArray("…") + tile_set = ExtResource("1_lquwl") + + [node name="Objects" type="Node2D" parent="."] + y_sort_enabled = true + + [node name="Walls" type="TileMapLayer" parent="Objects"] + y_sort_enabled = true + tile_map_data = PackedByteArray("…") + tile_set = ExtResource("1_lquwl") + + ; tiles/testgelaende_tiles.tres + 1:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) + 0:1/size_in_atlas = Vector2i(1, 2) + 0:1/0/texture_origin = Vector2i(0, 8) + 0:1/0/y_sort_origin = 8 + 0:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) + + [resource] + physics_layer_0/collision_layer = 1 + checklist: + - Das Prüfmuster ist dein eigenes PNG unter `res://tiles/`, im Maßstab 1 und ohne Trim exportiert. + - "`Floor` ist weg, `Ground` ist das erste Kind von `Main`, und beide Layer benutzen dasselbe TileSet als `.tres`." + - Abstände, Trennung und Regionsgröße im Atlas passen zu deinem Bild; keine Tile zeigt einen Streifen ihrer Nachbarin. + - Die Physik-Ebene des TileSet liegt auf Ebene 1, und die Figur bleibt an Wand-Tiles stehen; mit *Sichtbare Collision-Shapes* geprüft. + - Das hohe Feld ist eine Tile mit „Größe im Atlas“ 1 × 2, und seine Kollision passt zu deiner Absicht; mit Kollision auf der Grundfläche erscheint die Figur dahinter, davor und seitlich daneben richtig. + - Du hast mit `print(walls.local_to_map(walls.to_local(player.global_position)))` einmal die Zelle unter der Figur ausgegeben und mit dem Bild verglichen. + explanation: | + Der Umbau verbindet alles aus diesem Modul: Atlas aus deinem Bild, zwei Layer mit einem geteilten TileSet, Kollision aus der Physik-Ebene, Sortierung über `Objects`. Die Werte in der Lösung sind die des Prüfmusters, nicht deines Spiels. `y_sort_origin = 8` steht dort, weil die Grundlinie des hohen Felds auf der Unterkante der Zelle liegt; bei deinen Tiles legt deine Zeichnung den Fuß fest. Prüf jede Einstellung im laufenden Spiel; im Editor sieht ein falsch sortierter Layer oft richtig aus, solange die Figur nicht daneben steht. + resources: + - title: Godot Docs · Using TileMaps + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html + - title: Godot Docs · Using TileSets · Adding collision + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html#adding-collision-navigation-and-occlusion-to-the-tileset + + - id: ebenen-fuer-dein-level + type: practice + title: Welche Layer braucht dein Level? + prompt: | + Plane für ein Level deines Spiels die `TileMapLayer`-Nodes: Name, was darauf liegt, ob er Kollision hat und ob er mit Figuren sortiert wird. Schreib dazu, welche Tiles höher als eine Zelle sind und wo ihr Fuß liegt. Welche Ebenen und Tiles du brauchst, entscheidest du; es gibt keine richtige Anzahl. + lang: prose + checklist: + - Jeder Layer hat einen Namen und eine Aufgabe in einem Satz. + - Bei jedem Layer steht, ob er Kollision braucht und auf welcher Kollisionsebene sie liegt. + - Bei jedem Layer steht, ob er unter dem Y-sortierten Node liegt oder darunter bzw. darüber gezeichnet wird. + - Für jede hohe Tile ist notiert, welcher Teil kollidiert und wo der Sortierpunkt liegt. + - Du hast notiert, was du mit einem Prüfmuster testest, bevor du die echten Tiles zeichnest. + hints: + - Geh vom Boden nach oben vor und frag bei jeder Ebene, ob die Figur davor oder dahinter stehen kann. + explanation: | + Die Ebenenliste ist deine Vorgabe für das Level und für einen Agenten, der dir Werkzeuge baut. Sie macht die technischen Fragen früh sichtbar: welche Layer sortiert werden, welche kollidieren, wo hohe Tiles ihren Fuß haben. Wie dein Level aussieht und welche Ebenen es dafür braucht, entscheidest du beim Zeichnen. + resources: + - title: Godot Docs · Using TileMaps + url: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html diff --git a/content/godot/modules/pixelraster.yaml b/content/godot/modules/pixelraster.yaml new file mode 100644 index 0000000..6e78a88 --- /dev/null +++ b/content/godot/modules/pixelraster.yaml @@ -0,0 +1,483 @@ +id: pixelraster +title: "Pixelraster und Skalierung: was auf dem Bildschirm landet" +level: 2 +minutes: 120 +summary: Was zwischen Spielauflösung und Monitor passiert, wie du ganzzahlige Faktoren und schwarze Ränder ausrechnest und wie du unscharfe, ungleiche und zitternde Pixel an echten Aufnahmen aus Godot 4.7 erkennst. +place: unterwegs +goals: + - Du erklärst, wie Godot ein Spielbild im Stretch Mode `viewport` und `canvas_items` auf den Bildschirm bringt und an welcher Stelle der Texturfilter wirkt. + - Du rechnest für eine Spielauflösung und einen Bildschirm den ganzzahligen Faktor und die Breite der schwarzen Ränder aus. + - Du sagst vorher, was Stretch Aspect `keep` und `expand` mit Rändern und sichtbarer Fläche machen. + - Du erkennst an einem Screenshot unscharfe Pixel, ungleich breite Pixel, Mixels, gedrehte Pixelart und einen nicht ganzzahligen Kamera-Zoom und nennst die Einstellung dahinter. + - Du erklärst, warum Kameraglättung zusammen mit Pixel-Snap eine Figur um einen Pixel zittern lässt. +resources: + - title: Godot Docs · Multiple resolutions + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html + - title: Godot Docs · ProjectSettings · display/window/stretch/aspect + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-display-window-stretch-aspect + - title: Godot Docs · Camera2D + url: https://docs.godotengine.org/en/stable/classes/class_camera2d.html + - title: Godot Docs · Upgrading to Godot 4.7 · Changed defaults + url: https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html#changed-defaults +lesson: | + Du zeichnest eine Figur, 16 Pixel breit, jede Kante sitzt. Im Spiel ist sie auf dem Monitor verwaschen, auf dem Laptop sind manche Pixel breiter als andere, und sobald die Kamera ihr folgt, zuckt sie um einen Pixel vor und zurück. An der Zeichnung liegt nichts davon. Es passiert zwischen dem Bild, das Godot rechnet, und dem Bild, das der Monitor zeigt. + + Die Einstellungen kennst du aus [Godot-Start](/combat/godot-start): 320 × 180, Stretch Mode `viewport`, Scale Mode `integer`, Filter `Nearest`, Snap. Hier geht es um das Warum und die Folgen: was Godot an welcher Stelle rechnet, wie viel Rand auf welchem Monitor bleibt und woran du an einem Screenshot erkennst, welche Einstellung nicht stimmt. Alle Bilder sind echte Aufnahmen aus Godot 4.7.2 mit dem Renderer Compatibility. Die 320 × 180 des Testgeländes sind ein Platzhalter. Welche Auflösung dein Spiel bekommt, entscheidest du mit deiner Pixelart. + + ## Das Prüfmuster: ein Bild, an dem jeder Fehler auffällt + + An einer Figur übersiehst du, ob ein Pixel fehlt oder doppelt ist. Ein technisches Prüfmuster zeigt es sofort. Das Muster in diesem Modul ist 32 × 32 Pixel groß: ein Rahmen aus einer Pixelreihe, ein Schachbrett aus einzelnen Pixeln, senkrechte und waagrechte Streifen von je einem Pixel und zwei Diagonalen. + + ![Prüfmuster 32 × 32 Pixel, zwölffach vergrößert: weißer Rahmen, oben links ein Schachbrett aus Einzelpixeln, oben rechts senkrechte Streifen, unten links waagrechte Streifen, unten rechts zwei Diagonalen in Weiß und Hellblau](/media/godot/pixelraster/pruefmuster@2x.png "Das Prüfmuster dieses Moduls, zwölffach vergrößert") + + Jeder Skalierungsfehler verrät sich daran: Ein ungleicher Faktor macht Streifen verschieden breit, ein weicher Filter macht aus Schwarz und Weiß Grau, eine Drehung zerreißt das Schachbrett. So ein Muster legst du in Aseprite oder Krita in zwei Minuten selbst an und speicherst es als PNG im Projekt. Es ist ein Messwerkzeug, keine Grafik für dein Spiel. + + ## Vom Spielbild zum Monitor: viewport und canvas_items + + Zwischen Szene und Monitor liegen zwei Schritte: Godot zeichnet die Szene in einer Auflösung, dann wird das Ergebnis auf die Fenstergröße gebracht. Der Stretch Mode legt fest, wann skaliert wird ([Stretch Mode](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-mode)). + + - **`viewport`**: Godot zeichnet die ganze Szene in 320 × 180 und vergrößert danach das fertige Bild. Aus jedem Spielpixel wird ein Block aus Bildschirmpixeln. + - **`canvas_items`**: Godot zeichnet jeden Node direkt in Fenstergröße. Ein Sprite mit 32 Pixeln wird bei Faktor 4 als 128 Bildschirmpixel gezeichnet. Dabei entscheidet der Texturfilter, was zwischen zwei Texturpixeln entsteht. + + Daraus folgt, wo der **Texturfilter** wirkt. `Linear` mischt beim Vergrößern die vier nächsten Texturpixel, `Nearest` nimmt das nächste ([DefaultCanvasItemTextureFilter](https://docs.godotengine.org/en/stable/classes/class_viewport.html#class-viewport-constant-default-canvas-item-texture-filter-linear)). Im Modus `canvas_items` vergrößert jedes Sprite selbst, also wird es mit `Linear` weich. Im Modus `viewport` zeichnet Godot das Muster 1:1 in das kleine Bild. Dort spielt der Filter nur eine Rolle, wenn ein Sprite skaliert, gedreht oder zwischen zwei Pixeln gezeichnet wird. + + ![Zwei Aufnahmen des Prüfmusters bei Faktor 4 im Modus canvas_items: links mit Filter Linear, Schachbrett und Streifen sind grau verschwommen; rechts mit Nearest, alle Kanten scharf](/media/godot/pixelraster/filter-linear-nearest@2x.png "Godot 4.7.2, Stretch Mode canvas_items, Fenster 1280 × 720") + + Gemessen mit Godot 4.7.2 im Fenster 1280 × 720: Im Modus `canvas_items` mit `Linear` hat der Ausschnitt um das Muster 99 verschiedene Farben, mit `Nearest` 5: die vier Farben des Musters und den Hintergrund. Im Modus `viewport` sind es auch mit `Linear` nur 5. Das Vergrößern des fertigen Bildes mischt keine Farben, auch nicht bei Faktor 3,5. Das gilt für Compatibility und Forward+, beide gemessen. + + > [!WARNING] Neue Projekte starten in 4.7 anders + > Ein Projekt, das du in Godot 4.7 neu anlegst, bekommt Stretch Mode `canvas_items` und Stretch Aspect `expand` ([Changed defaults](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html#changed-defaults)). Zusammen mit dem Standardfilter `Linear` sieht Pixelart dann aus wie links im Bild. Fehlt eine Zeile in `project.godot`, gilt der Standard der Klasse `ProjectSettings`: `disabled` und `keep`. Im Testgelände steht kein `aspect`; `ProjectSettings.get_setting("display/window/stretch/aspect")` liefert dort `keep`. + + Für Webentwickler: `viewport` entspricht einem ``, das per CSS auf 1280 × 720 gestreckt wird, mit `image-rendering: pixelated`. `canvas_items` ähnelt einer Seite, die mit `transform: scale(4)` vergrößert wird: Der Browser rechnet jedes Element in voller Auflösung neu. Der Vergleich bricht beim Filter: In Godot gilt er pro Node oder für das ganze Projekt, nicht pro CSS-Regel. + + ## Ganzzahlige Faktoren und schwarze Ränder ausrechnen + + Mit Scale Mode `integer` rundet Godot den Faktor ab ([Stretch Scale Mode](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-scale-mode)). Der Faktor ist die kleinere der beiden Divisionen, abgerundet. Den Rest füllt Godot mit Schwarz, je zur Hälfte an beiden Seiten: + + ```text + fx = Bildschirmbreite / Spielbreite + fy = Bildschirmhöhe / Spielhöhe + Faktor = abrunden(kleinerer Wert von fx, fy) + Bild = Spielauflösung × Faktor + Rand = (Bildschirm − Bild) / 2 je Seite + ``` + + Zwei Spielauflösungen zum Rechnen, keine Empfehlung: 320 × 180 aus dem Testgelände und 384 × 216. + + | Bildschirm | 320 × 180 | 384 × 216 | + | --- | --- | --- | + | 1920 × 1080 | × 6 | × 5 | + | 2560 × 1440 | × 8 | × 6, 128 / 72 | + | 3840 × 2160 | × 12 | × 10 | + | 1366 × 768 | × 4, 43 / 24 | × 3, 107 / 60 | + | 1280 × 800 | × 4, 0 / 40 | × 3, 64 / 76 | + + Hinter dem Faktor steht der Rand in Pixeln, links und rechts / oben und unten; ohne Angabe gibt es keinen. 1280 × 800 ist der Bildschirm des Steam Deck. Gemessen mit Godot 4.7.2 an Fenstern in diesen Größen: 320 × 180 bei 1366 × 768 ergibt 43 und 24 Pixel Rand, bei 1280 × 800 oben und unten 40, 384 × 216 bei 1366 × 768 ergibt 107 und 60. Die übrigen Zeilen sind nach der Formel gerechnet. + + ![Ganzes Spielfenster 1366 × 768 mit schmalem schwarzem Rand links, rechts, oben und unten um das Spielbild](/media/godot/pixelraster/raender-1366x768@2x.png "Godot 4.7.2, 320 × 180 im Fenster 1366 × 768, Faktor 4, auf die Hälfte verkleinert") + + Die Doku nennt 640 × 360 als Ausgangspunkt, weil es auf 720p, 1080p, 1440p und 2160p ohne Rand passt ([Desktop game](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#desktop-game)). Für 320 × 180 gilt dasselbe mit doppeltem Faktor. Was du wählst, hängt an deiner Pixelart: Eine kleinere Auflösung heißt größere Pixel und weniger sichtbare Welt. + + > [!MERKE] + > Faktor = kleinere Division, abgerundet. Rand = Rest durch zwei. Ein Faktor, der nicht aufgeht, wird nie gestreckt, sondern mit Schwarz aufgefüllt. + + > [!WARNING] Ein Pixel kostet einen Faktor + > Laut Doku lässt der Fenstermodus Fullscreen unter Windows unten eine Pixelzeile frei. Aus 1080 werden 1079 Pixel Höhe, und `integer` rundet 1079 / 180 = 5,99 auf 5 ab: 1600 × 900 statt 1920 × 1080. Die Doku empfiehlt deshalb Exclusive Fullscreen ([Stretch Scale Mode](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-scale-mode)). Die Rechnung stammt aus der Doku-Angabe, unter Linux ist sie nicht nachgemessen. + + ## Seitenverhältnis: keep, expand und die sichtbare Fläche + + Der Stretch Aspect bestimmt, was passiert, wenn der Bildschirm ein anderes Seitenverhältnis hat als das Spiel ([Stretch Aspect](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-aspect)). Mit `keep` bleibt die Spielfläche gleich, der Rest wird schwarz. Mit `expand` wächst die Spielfläche in eine Richtung, der Spieler sieht mehr Welt. `keep_width` und `keep_height` wachsen nur nach unten oder nur zur Seite. + + Gemessen mit 320 × 180 und Scale Mode `integer`: + + | Fenster | `keep` | `expand` | + | --- | --- | --- | + | 1280 × 800 | 320 × 180, oben und unten 40 px Schwarz | 320 × 200, kein Rand | + | 1366 × 768 | 320 × 180, Rand 43 / 24 | 320 × 180, Rand 43 / 24 | + + Die Spalte zeigt `get_viewport().get_visible_rect().size`. Bei 1280 × 800 füllt `expand` die 40 Pixel mit 20 zusätzlichen Spielpixeln Welt. Bei 1366 × 768 ändert `expand` nichts: Das Seitenverhältnis ist fast gleich, und der Rest kommt vom Abrunden des Faktors. Diesen Rest füllt auch `expand` nicht. + + Mit `expand` muss deine Welt größer sein als 320 × 180, sonst sieht der Spieler über den Levelrand. Ein HUD in einem `CanvasLayer` bleibt an den Ecken, an denen es verankert ist. Gemessen bei 1280 × 800: Ein Element oben links bleibt bei y = 4, eines unten rechts rückt mit `expand` von y = 166 auf y = 186. Ob alle Spieler dieselbe Fläche sehen oder breitere Bildschirme mehr zeigen, ist eine Entscheidung für dein Spiel. + + ## Fehlerbilder am Prüfmuster erkennen + + Die folgenden Bilder zeigen das Muster in einem Projekt mit 320 × 180, `viewport` und Filter `Nearest`. Jeweils eine Einstellung weicht ab. + + ### Ungleich breite Pixel: Faktor nicht ganzzahlig + + ![Streifenfeld des Prüfmusters zweimal: oben bei Faktor 4 sind weiße und schwarze Streifen gleich breit, unten bei Faktor 3,5 sind die weißen Streifen breiter als die schwarzen](/media/godot/pixelraster/faktor-4-und-3-5@2x.png "Godot 4.7.2, viewport, Fenster 1280 × 720 mit integer und 1120 × 630 mit fractional, Ausschnitt neunfach vergrößert") + + Bei Faktor 3,5 kann ein Spielpixel nicht 3,5 Bildschirmpixel breit sein. Gemessen sind die Pixel abwechselnd 4 und 3 breit. In den Streifen sieht das aus, als wären Weiß und Schwarz verschieden dick. Ursache: Scale Mode `fractional`, der Standard der Klasse. + + ### Mixels: Sprites in verschiedenen Maßstäben + + ![Zwei Prüfmuster nebeneinander: links in Originalgröße, rechts doppelt so groß mit doppelt so großen Pixeln](/media/godot/pixelraster/mixel@2x.png "Godot 4.7.2, Spielbild 320 × 180 sechsfach vergrößert, rechtes Sprite mit scale 2") + + Ein Sprite mit `scale` 2 hat doppelt so große Pixel wie alles andere. Im Bild ist jeder Pixel scharf, und trotzdem passen die Raster nicht zusammen. Solche gemischten Pixelgrößen heißen **Mixels**. Sie entstehen auch, wenn eine Grafik in einer anderen Auflösung gezeichnet wurde als der Rest. Mit `scale` 1,5 kommen ungleich breite Pixel dazu, weil 1,5 nicht aufgeht. + + ### Gedrehte Pixelart + + ![Prüfmuster um 15 Grad gedreht: links im Modus viewport mit Treppen und zerrissenem Schachbrett, rechts im Modus canvas_items mit feinen, schräg liegenden Pixeln](/media/godot/pixelraster/drehung@2x.png "Godot 4.7.2, Sprite mit rotation 15°, Faktor 4") + + Drehst du ein Sprite im Modus `viewport`, sucht Godot für jeden Pixel des kleinen Bildes das nächste Texturpixel. Manche Pixel kommen doppelt, andere fehlen, das Schachbrett zerreißt. Im Modus `canvas_items` dreht Godot in voller Auflösung: Die Pixel bleiben ganz, liegen aber schräg und sind feiner als das Raster drumherum, also wieder Mixels. Viele Pixelart-Spiele zeichnen Drehungen deshalb als eigene Frames. Ob du drehst, eigene Frames zeichnest oder beides mischst, entscheidest du. + + ## Kamera: Zoom, Pixel-Snap und Glättung + + `Camera2D.zoom` vergrößert die Welt im kleinen Bild ([zoom](https://docs.godotengine.org/en/stable/classes/class_camera2d.html#class-camera2d-property-zoom)); der Standard ist `(1, 1)`. Im Modus `viewport` gilt dieselbe Regel wie beim Faktor: `zoom` 2 macht aus jedem Weltpixel 2 × 2 Spielpixel, `zoom` 1,5 macht manche Pixel 1 und andere 2 breit. Zoom 2 halbiert außerdem die sichtbare Welt auf 160 × 90. + + ![Prüfmuster mit Kamera-Zoom 2 und 1,5: bei 2 gleichmäßig, bei 1,5 sind Schachbrett und Streifen unregelmäßig](/media/godot/pixelraster/kamera-zoom@2x.png "Godot 4.7.2, viewport, Camera2D.zoom 2 und 1,5, Bild 320 × 180 achtfach vergrößert") + + **Pixel-Snap** rundet beim Zeichnen die Position jedes Nodes auf ganze Pixel ([snap_2d_transforms_to_pixel](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-rendering-2d-snap-snap-2d-transforms-to-pixel)). Gemessen an vier Kopien des Musters bei Faktor 4, linker Rand in Bildschirmpixeln: + + | `position.x` | `canvas_items`, Snap aus | `viewport`, Snap an | + | --- | --- | --- | + | 144 | 576 | 576 | + | 144,25 | 577 | 576 | + | 144,5 | 578 | 580 | + | 144,75 | 579 | 580 | + + Ohne Snap rückt das Sprite in Viertelpixeln vor, eine Figur gleitet also über das Raster. Mit Snap springt sie um ganze Spielpixel, ab ,5 wird aufgerundet. Im Modus `canvas_items` mit Snap ergab dieselbe Messung ebenfalls 576, 576, 580, 580. + + **Kameraglättung** (`position_smoothing_enabled`, Standard aus) lässt die Kamera der Figur hinterherziehen. Zusammen mit Snap zittert die Figur dann. Gemessen mit dem Prüfaufbau: Figur mit 90 px/s nach rechts, `Camera2D` als Kind, linke Kante der Figur in Pixeln des Spielbilds, Bild für Bild: + + ```text + Glättung aus: 154 154 154 154 154 154 154 154 + Glättung an: 170 171 170 171 170 171 170 171 + ``` + + Ohne Glättung steht die Figur still in der Mitte, und die Welt rückt abwechselnd um 2 und 1 Pixel weiter. Mit Glättung bleibt die Kamera rund 16 Pixel zurück. Godot rundet Kamera und Figur getrennt, ihr Abstand ist mal ein ganzer Pixel mehr, mal weniger, und die Figur springt jedes Bild hin und her. Die Doku warnt: Snap macht Bewegung weniger glatt, „especially when Camera2D smoothing is enabled“. Ob du Glättung willst, entscheidest du. Wenn ja, prüf sie in Bewegung, nicht im Standbild. + + > [!PRAXIS] + > Leg eine Prüfszene ins Projekt: Muster, eine laufende Testfigur, sichtbares Raster. Starte sie auf jedem neuen Gerät und nach jeder Änderung an Projekteinstellungen oder Kamera. Ein Screenshot davon im Fehlerbericht oder in der Aufgabe für einen Coding-Agenten sagt mehr als „sieht unscharf aus“. Im [Abschlussmodul des Kurses](/godot/werkstatt-messen) gehören solche Einstellungen zur Fehlerkiste. + + > [!INTERVIEW] + > „Unsere Pixelart sieht auf manchen Monitoren unscharf oder ungleichmäßig aus. Wie gehst du vor?“ Gute Antwort: Ich mache einen Screenshot mit einem Prüfmuster und schaue, welches Fehlerbild es ist. Grau statt Schwarz-Weiß heißt Filter `Linear` oder ein skaliertes Sprite. Ungleich breite Streifen heißen nicht ganzzahliger Faktor, Zoom oder Maßstab. Dann prüfe ich Stretch Mode, Scale Mode, Filter und Kamera-Zoom in `project.godot` und in der Szene und rechne für die betroffene Monitorgröße den Faktor nach. + + ## Häufige Fehler + + - **Neues 4.7-Projekt unverändert übernommen.** Es startet mit `canvas_items`, `expand` und Filter `Linear`. Pixelart wird weich. Prüfe `project.godot`, bevor du Grafiken beurteilst. + - **Filter an jedem Sprite einzeln gesetzt.** Wer im Projekt `Linear` lässt und Sprites auf `Nearest` stellt, vergisst eins. Stell den Projektstandard um, Details in [Eigene Sprites importieren](/godot/sprites-importieren). + - **`fractional` für ein Pixelart-Spiel.** Volle Fläche, aber Pixel mit 3 und 4 Bildschirmpixeln Breite. + - **Ränder für einen Fehler gehalten.** Bei 1366 × 768 bleibt mit 320 × 180 immer ein Rand, auch mit `expand`. Das ist der Preis gleich großer Pixel. + - **Kamera-Zoom oder `scale` mit Kommazahl.** Ungleiche Pixel trotz `integer`, denn der Scale Mode betrifft nur das Fenster, nicht die Kamera. + - **Glättung im Standbild beurteilt.** Das Zittern um einen Pixel siehst du nur in Bewegung. + + ## Zusammenfassung + + - `viewport` zeichnet in Spielauflösung und vergrößert dann das ganze Bild ohne Farben zu mischen; `canvas_items` zeichnet jeden Node in Fenstergröße, dort entscheidet der Filter. + - Neue Projekte starten in 4.7 mit `canvas_items` und `expand`; ohne Eintrag gelten `disabled` und `keep`. + - Faktor = kleinere Division abgerundet, Rand = Rest durch zwei. 320 × 180 bei 1366 × 768: Faktor 4, Rand 43 und 24 Pixel. + - `expand` zeigt mehr Welt, wenn das Seitenverhältnis abweicht. Den Rest aus dem Abrunden füllt es nicht. + - Weich heißt Filter, ungleich breit heißt Faktor, Zoom oder Maßstab mit Kommazahl, verschiedene Pixelgrößen heißen Mixels. + - Snap rundet Positionen auf ganze Spielpixel. Kameraglättung mit Snap lässt eine laufende Figur um einen Pixel zittern. + - Die Spielauflösung und den Umgang mit Rändern, Drehung und Glättung entscheidest du; das Prüfmuster zeigt dir die Folgen. +exercises: + - id: weiches-muster + type: choice + title: Grau statt Schwarz und Weiß + prompt: | + Ein Kollege schickt dir diesen Ausschnitt aus seinem Projekt. Das Prüfmuster liegt mit `scale` 1 in der Szene, die Kamera hat `zoom` 1. Welche Einstellungen erklären das Bild? Mehrere Antworten richtig. + + ![Prüfmuster mit verschwommenen Kanten: Das Schachbrett ist ein grau gepunktetes Feld, Streifen und Diagonalen haben graue Säume](/media/godot/pixelraster/diagnose-weich@2x.png "Godot 4.7.2, Ausschnitt aus einem Fenster mit 960 × 540, fünffach vergrößert") + options: + - text: Stretch Mode `canvas_items` + correct: true + why: In diesem Modus zeichnet Godot das Sprite direkt in Fenstergröße. Dabei wird es vergrößert, und der Filter bestimmt die Zwischenpixel. + - text: Texturfilter `Linear` im Projekt oder am Node + correct: true + why: "`Linear` mischt die vier nächsten Texturpixel. Aus einem Schachbrett aus Einzelpixeln wird Grau." + - text: Scale Mode `fractional` + correct: false + why: Ein nicht ganzzahliger Faktor macht Pixel ungleich breit, aber nicht weich. Hier sind alle Kanten verschwommen. + - text: Stretch Mode `viewport` mit Filter `Nearest` + correct: false + why: Dann zeichnet Godot das Muster 1:1 und vergrößert das fertige Bild ohne Farbmischung. Gemessen bleiben es die vier Farben des Musters plus Hintergrund. + explanation: | + Weiche Kanten bei einem Sprite mit `scale` 1 entstehen, wenn das Sprite beim Zeichnen vergrößert wird und dabei gemischt wird. Das passiert im Modus `canvas_items` mit Filter `Linear`, dem Standard der Klasse. Im Modus `viewport` vergrößert Godot erst das fertige kleine Bild, und das mischt keine Farben. Das Bild ist mit `canvas_items`, `Linear` und Faktor 3 aufgenommen; der Ausschnitt hat 38 Farben statt 5 (vier im Muster, eine im Hintergrund). + resources: + - title: Godot Docs · Multiple resolutions · Stretch Mode + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-mode + - title: Godot Docs · Viewport · DefaultCanvasItemTextureFilter + url: https://docs.godotengine.org/en/stable/classes/class_viewport.html#class-viewport-constant-default-canvas-item-texture-filter-linear + + - id: mixel-erkennen + type: choice + title: Zwei Muster, zwei Fehler + prompt: | + Beide Muster stehen in derselben Szene: Stretch Mode `viewport`, Scale Mode `integer`, Filter `Nearest`, Faktor 4. Welche Aussagen treffen auf das rechte Muster zu? Mehrere Antworten richtig. + + ![Links ein Prüfmuster in Originalgröße, rechts ein größeres, bei dem die Streifen abwechselnd breit und schmal sind](/media/godot/pixelraster/diagnose-mixel@2x.png "Godot 4.7.2, Spielbild 320 × 180 siebenfach vergrößert") + options: + - text: Seine Pixel sind größer als die des linken Musters, das sind Mixels. + correct: true + why: Zwei Pixelgrößen in einem Bild sind die Definition von Mixels. + - text: Sein `scale` ist keine ganze Zahl, deshalb sind die Streifen ungleich breit. + correct: true + why: Das Muster hat `scale` 1,5. Aus einem Texturpixel wird mal ein, mal zwei Spielpixel. + - text: Der Texturfilter steht auf `Linear`. + correct: false + why: Dann wären Zwischenfarben zu sehen. Das Bild hat nur die vier Farben des Musters und den Hintergrund. + - text: Das Fenster ist nicht ganzzahlig skaliert. + correct: false + why: Dann wäre auch das linke Muster ungleichmäßig. Das linke ist sauber, der Fehler sitzt im rechten Sprite. + explanation: | + Das rechte Muster hat `scale` 1,5. Damit trifft es zwei Fehlerbilder auf einmal: Seine Pixel sind größer als alle anderen (Mixels), und weil 1,5 nicht aufgeht, sind sie abwechselnd 1 und 2 Spielpixel breit. Das linke Muster im selben Bild zeigt, dass Fenster, Faktor und Filter stimmen. Ein Vergleichsobjekt mit `scale` 1 im selben Screenshot grenzt den Fehler auf einen Node ein. + resources: + - title: Godot Docs · Node2D.scale + url: https://docs.godotengine.org/en/stable/classes/class_node2d.html#class-node2d-property-scale + + - id: weg-des-bildes + type: order + title: Der Weg eines Bildes im Modus viewport + prompt: | + Projekt mit 320 × 180, `viewport`, `integer` und Snap, Laptop mit 1366 × 768. Bring die Schritte in die Reihenfolge, in der aus einem Tastendruck ein Bild auf dem Monitor wird. + lang: prose + lines: | + Die Eingabe „rechts“ kommt an + _physics_process rechnet die neue Position der Figur, x = 144,25 + Godot zeichnet die Szene in das Bild mit 320 × 180 und rundet die Figur dabei auf x = 144 + Das fertige Bild wird um Faktor 4 auf 1280 × 720 vergrößert + Godot setzt es mittig ins Fenster, links und rechts bleiben je 43 Pixel schwarz + Der Monitor zeigt das Bild + alternatives: + - [1, 2, 3, 5, 4, 6] + explanation: | + Die Position bleibt eine Kommazahl; erst beim Zeichnen rundet Snap sie auf einen ganzen Spielpixel. Dann entsteht das kleine Bild, und erst das fertige Bild wird vergrößert. Was im Fenster übrig bleibt, wird Rand. Ob Godot erst vergrößert oder erst den Platz im Fenster festlegt, siehst du von außen nicht; deshalb gilt auch die Reihenfolge mit vertauschtem viertem und fünftem Schritt. Ein Fehler in der Szene (Zoom, `scale`, Drehung) entsteht im kleinen Bild, ein Fehler beim Faktor erst beim Vergrößern. Daran erkennst du später, wo du suchen musst. + resources: + - title: Godot Docs · Multiple resolutions · Stretch Mode + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-mode + + - id: faktor-1366 + type: output + title: Faktor auf dem Laptop + prompt: | + Spielauflösung 320 × 180, Scale Mode `integer`, Laptop mit 1366 × 768 im Vollbild. Um welchen Faktor vergrößert Godot? Antworte mit der Zahl. + lang: text + code: | + Spiel: 320 × 180 + Bildschirm: 1366 × 768 + expected: | + 4 + accept: + - × 4 + - ×4 + - x4 + - 4x + - 4× + - 4 × + - x 4 + - Faktor 4 + explanation: | + 1366 / 320 = 4,27 und 768 / 180 = 4,27. Die kleinere Division abgerundet ist 4. Das Spielbild wird 1280 × 720 groß, gemessen mit Godot 4.7.2. Mit `fractional` wäre der Faktor 4,27, und die Pixel wären mal 4, mal 5 Bildschirmpixel breit. + resources: + - title: Godot Docs · Multiple resolutions · Stretch Scale Mode + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-scale-mode + + - id: rand-steam-deck + type: output + title: Rand auf dem Steam Deck + prompt: | + Spielauflösung 384 × 216, Scale Mode `integer`, Stretch Aspect `keep`, Bildschirm 1280 × 800. Wie viele Pixel ist der schwarze Rand **oben** hoch? Antworte mit der Zahl. + lang: text + code: | + Spiel: 384 × 216 + Bildschirm: 1280 × 800 + expected: | + 76 + accept: + - 76 px + - 76px + - 76 Pixel + hints: + - Rechne zuerst den Faktor aus beiden Richtungen und nimm den kleineren, abgerundet. + - Das Bild ist 216 × Faktor hoch; der Rest verteilt sich auf oben und unten. + explanation: | + 1280 / 384 = 3,33 und 800 / 216 = 3,70, also Faktor 3. Das Spielbild ist 1152 × 648 groß. Senkrecht bleiben 800 − 648 = 152 Pixel, also 76 oben und 76 unten; waagrecht 128, also 64 links und rechts. Bei 320 × 180 wären es Faktor 4 und nur oben und unten je 40 Pixel. Welche Auflösung du nimmst, entscheidet die Rechnung nicht, aber sie zeigt dir den Preis auf jedem Gerät. + resources: + - title: Godot Docs · Multiple resolutions · Stretch Scale Mode + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-scale-mode + + - id: sichtbare-flaeche + type: output + title: Wie viel Welt bei expand? + prompt: | + Projekt mit 320 × 180, Stretch Mode `viewport`, Scale Mode `integer`, Stretch Aspect `expand`. Das Fenster ist 1280 × 800 groß. Was gibt diese Zeile in Godot 4.7.2 aus? + lang: gdscript + code: | + print(get_viewport().get_visible_rect().size) + expected: | + (320.0, 200.0) + accept: + - (320, 200) + - (320,200) + - (320.0,200.0) + - 320 × 200 + - 320 x 200 + - 320x200 + hints: + - Der Faktor bleibt 4, denn die Breite 1280 / 320 lässt nicht mehr zu. + - "`expand` füllt die freie Höhe mit Spielpixeln statt mit Schwarz: 800 / 4." + explanation: | + Faktor 4 ergibt sich aus der Breite. Mit `keep` blieben oben und unten je 40 Pixel schwarz. `expand` lässt die Spielfläche stattdessen auf 800 / 4 = 200 Spielpixel Höhe wachsen. Der Spieler sieht 20 Zeilen mehr Welt, und ein HUD, das unten verankert ist, rückt mit. Gemessen mit Godot 4.7.2; bei 1366 × 768 liefert dieselbe Zeile `(320.0, 180.0)`, weil dort der Rest aus dem Abrunden kommt, nicht aus dem Seitenverhältnis. + resources: + - title: Godot Docs · Viewport.get_visible_rect + url: https://docs.godotengine.org/en/stable/classes/class_viewport.html#class-viewport-method-get-visible-rect + - title: Godot Docs · Multiple resolutions · Stretch Aspect + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#stretch-aspect + + - id: streifen-breite + type: output + title: Wie breit sind die Streifen? + prompt: | + Diese Aufnahme stammt aus einem Fenster mit 800 × 450, Spielauflösung 320 × 180, Stretch Mode `viewport`, Scale Mode `fractional`. Wie viele Bildschirmpixel breit sind die einzelnen Spielpixel? Nenne die beiden Breiten, die vorkommen, kleinere zuerst. + + ![Prüfmuster mit unregelmäßigen Streifen: manche Streifen sind schmaler als ihre Nachbarn](/media/godot/pixelraster/diagnose-streifen@2x.png "Godot 4.7.2, Ausschnitt aus dem Fenster 800 × 450, sechsfach vergrößert") + lang: text + code: | + Fenster: 800 × 450 + Spiel: 320 × 180 + Faktor: ? + expected: | + 2 und 3 + accept: + - 2, 3 + - 2 3 + - 2/3 + - 2 oder 3 + - 2–3 + - 2-3 + - 2 und 3 px + - 2 und 3 Pixel + - 2 und 3 Bildschirmpixel + hints: + - 800 / 320 = ? + - Ein Bildschirm kennt keine halben Pixel. Wie verteilt sich ein Faktor mit ,5 auf zwei Nachbarpixel? + explanation: | + 800 / 320 = 2,5. Zwei Spielpixel nebeneinander müssen 5 Bildschirmpixel füllen, also wird einer 2 und einer 3 breit. Gemessen an der Aufnahme wechseln sich 2 und 3 ab. Mit `integer` wäre der Faktor 2 und das Bild 640 × 360 mit Rand, aber alle Pixel gleich. + resources: + - title: Godot Docs · ProjectSettings · display/window/stretch/scale_mode + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-display-window-stretch-scale-mode + + - id: stretch-zeilen + type: gap + title: Ränder statt mehr Welt + prompt: | + Ergänze die Zeilen, die du von Hand in `project.godot` einträgst: Godot soll das fertige Spielbild vergrößern, nur um ganze Faktoren skalieren und bei abweichendem Seitenverhältnis schwarze Ränder zeigen statt mehr Welt. + lang: ini + code: | + [display] + + window/size/viewport_width=320 + window/size/viewport_height=180 + window/stretch/mode="⟦viewport⟧" + window/stretch/aspect="⟦keep⟧" + window/stretch/scale_mode="⟦integer⟧" + gaps: + - label: Stretch Mode + hint: Erst klein zeichnen, dann das ganze Bild vergrößern. + - label: Stretch Aspect + hint: Seitenverhältnis halten, Rest schwarz. + - label: Scale Mode + hint: Der Faktor wird abgerundet. + explanation: | + `viewport` zeichnet in 320 × 180 und vergrößert das fertige Bild. `keep` hält Breite und Höhe der Spielfläche fest und füllt den Rest schwarz. `integer` rundet den Faktor ab. `keep` ist der Standard der Klasse; speichert Godot die Projekteinstellungen, verschwindet die Zeile deshalb wieder, gemessen mit Godot 4.7.2. Fehlt sie, gilt trotzdem `keep`. Ein neues 4.7-Projekt schreibt dagegen `expand` hinein, und diese Zeile bleibt stehen. Die Werte für Breite und Höhe sind die Platzhalter des Testgeländes. + resources: + - title: Godot Docs · ProjectSettings · display/window/stretch/aspect + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-display-window-stretch-aspect + + - id: kamera-zoom-finden + type: bug + title: Das Schachbrett hat Löcher + prompt: | + Im Spiel sieht das Prüfmuster so aus, obwohl `project.godot` `viewport`, `integer` und Filter `Nearest` enthält und das Sprite `scale` 1 hat. Hier ist der Ausschnitt aus `main.tscn`. Finde die Zeile, die das Bild erklärt. + + ![Prüfmuster mit grobem, löchrigem Schachbrett, verschieden breiten Streifen und unterbrochenen Diagonalen](/media/godot/pixelraster/diagnose-zoom@2x.png "Godot 4.7.2, Ausschnitt aus dem Spielbild 320 × 180, 18-fach vergrößert") + lang: ini + code: | + [node name="Pattern" type="Sprite2D" parent="."] + position = Vector2(200, 100) + texture = ExtResource("1_lbr83") + centered = false + + [node name="Camera2D" type="Camera2D" parent="."] + anchor_mode = 0 + zoom = Vector2(0.75, 0.75) + lines: [8] + explanation: | + Mit `zoom` 0,75 wird die Welt im kleinen Bild auf drei Viertel verkleinert. Aus 32 Texturpixeln werden 24 Spielpixel; jedes vierte Texturpixel fällt weg, das Schachbrett bekommt Lücken, und Streifen verschmelzen. Der Scale Mode `integer` hilft nicht, er betrifft nur das Fenster. Gleichmäßig bleiben Pixel bei ganzzahligem Zoom wie 1 oder 2. Welcher Ausschnitt der Welt zu deinem Spiel passt, entscheidest du; gemessen ist hier nur, was ein Zoom mit Kommazahl mit dem Raster macht. + resources: + - title: Godot Docs · Camera2D.zoom + url: https://docs.godotengine.org/en/stable/classes/class_camera2d.html#class-camera2d-property-zoom + + - id: gleiten-statt-springen + type: bug + title: Die Figur gleitet in Viertelpixeln + prompt: | + Ein KI-Agent hat das Projekt aufgesetzt. Die Kanten der Figur sind scharf, aber beim langsamen Laufen rückt sie in Schritten weiter, die kleiner sind als ein Pixel ihrer Grafik; neben der Wand sieht man sie halb über der Fuge. Finde die Zeile und korrigiere sie. + lang: ini + code: | + [display] + + window/size/viewport_width=320 + window/size/viewport_height=180 + window/stretch/mode="canvas_items" + window/stretch/scale_mode="integer" + + [rendering] + + textures/canvas_textures/default_texture_filter=0 + lines: [5] + fix: + - line: 5 + answers: + - window/stretch/mode="viewport" + explanation: | + Im Modus `canvas_items` zeichnet Godot in Fenstergröße. Gemessen rückt ein Sprite bei Faktor 4 dann um je einen Bildschirmpixel weiter, wenn sich `position.x` um 0,25 ändert, also in Viertelpixeln der Grafik. Der Filter `Nearest` hält die Kanten scharf, verhindert das aber nicht. Mit `viewport` entsteht das Bild in 320 × 180, und eine Position kann nur auf ganzen Spielpixeln landen. Snap (`rendering/2d/snap/snap_2d_transforms_to_pixel=true`) hätte das Gleiten hier ebenfalls beseitigt; gedrehte und skalierte Sprites zeichnet `canvas_items` aber weiter in Fensterauflösung. + resources: + - title: Godot Docs · ProjectSettings · display/window/stretch/mode + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-display-window-stretch-mode + + - id: zittern-erklaeren + type: explain + title: Warum zittert die Figur? + prompt: | + Die Figur läuft mit 90 px/s nach rechts, die `Camera2D` hängt als Kind an ihr, Pixel-Snap ist an. Gemessen wurde die linke Kante der Figur im Spielbild, Bild für Bild. Erkläre in wenigen Sätzen, woher der Unterschied kommt. + lang: text + code: | + position_smoothing_enabled = false + 154 154 154 154 154 154 154 154 + + position_smoothing_enabled = true + 170 171 170 171 170 171 170 171 + points: + - Ohne Glättung bewegt sich die Kamera genau mit der Figur; die Figur bleibt an derselben Stelle im Bild, nur die Welt rückt. + - Mit Glättung hinkt die Kamera hinterher; der Abstand zwischen Kamera und Figur ist eine Kommazahl. + - Godot rundet Kamera und Figur getrennt auf ganze Pixel; je nach Nachkommastelle ergibt das mal 170, mal 171. + - Bei 1,5 px pro Frame wechselt das jedes Bild, das Auge sieht ein Zittern; im Standbild fällt es nicht auf. + explanation: | + Ohne Glättung ist die Kamera an die Figur gekoppelt, beide haben dieselbe Position, und die Figur steht immer bei 154. Mit Glättung zieht die Kamera mit Abstand hinterher, im Messlauf rund 16 Pixel. Snap rundet die Figur und die Kamera jeweils für sich. Weil die Figur 1,5 Pixel pro Frame läuft, wechseln die Nachkommastellen jedes Bild, und die gerundete Differenz springt zwischen 16 und 17. Die Doku nennt diesen Fall bei Snap ausdrücklich. Ob du Glättung nutzt, entscheidest du; wenn ja, beurteile sie in Bewegung. + resources: + - title: Godot Docs · ProjectSettings · snap_2d_transforms_to_pixel + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-rendering-2d-snap-snap-2d-transforms-to-pixel + - title: Godot Docs · Camera2D.position_smoothing_enabled + url: https://docs.godotengine.org/en/stable/classes/class_camera2d.html#class-camera2d-property-position-smoothing-enabled + + - id: aufloesung-notieren + type: practice + title: Deine Bildschirme, deine Rechnung + prompt: | + Schreib eine kurze Notiz für dein Spiel, 3 bis 5 Sätze. Nimm eine oder zwei Spielauflösungen, die du für deine Pixelart in Betracht ziehst, und die Bildschirme, auf denen du testen wirst (dein Monitor, ein Laptop, gern das Steam Deck). Rechne für jede Kombination Faktor und Rand aus. Notiere dazu, ob du `keep` oder `expand` ausprobieren willst und woran du es beurteilen wirst. Eine richtige Auflösung gibt es hier nicht. + lang: prose + checklist: + - Die Notiz nennt mindestens eine Spielauflösung als Platzhalter, die du gewählt hast. + - Für jeden genannten Bildschirm stehen Faktor und Rand in Pixeln da, nach der Formel aus der Lektion. + - Du hast notiert, ob du `keep` oder `expand` testen willst und was du dabei prüfst (Rand, sichtbare Fläche, HUD). + - Du hast festgehalten, mit welchem Prüfmuster oder Screenshot du das Ergebnis auf dem Gerät kontrollierst. + hints: + - "Faktor = abrunden(min(Bildschirmbreite / Spielbreite, Bildschirmhöhe / Spielhöhe))." + - Wenn du noch keine Auflösung hast, nimm zwei Kandidaten und vergleiche sie auf denselben Bildschirmen. + explanation: | + Die Notiz trennt Rechnung und Entscheidung. Faktor und Rand liegen fest, sobald Auflösung und Bildschirm feststehen; ob dich ein Rand von 43 Pixeln stört, ob breitere Bildschirme mehr Welt zeigen sollen und wie groß deine Pixel werden, entscheidest du. Mit der Notiz kannst du einem Coding-Agenten genau sagen, was er in `project.godot` einstellen soll, und später nachsehen, warum. + resources: + - title: Godot Docs · Multiple resolutions · Desktop game + url: https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#desktop-game diff --git a/content/godot/modules/projekt-erkunden.yaml b/content/godot/modules/projekt-erkunden.yaml new file mode 100644 index 0000000..8725a61 --- /dev/null +++ b/content/godot/modules/projekt-erkunden.yaml @@ -0,0 +1,789 @@ +id: projekt-erkunden +title: Ein fremdes Projekt erkunden und das Testgelände aufbauen +level: 1 +minutes: 160 +summary: Ein festes Vorgehen, um ein Godot-Projekt zu verstehen, das jemand anderes gebaut hat. Du übst es am Testgelände, das du aus Textdateien selbst anlegst und auf dem die späteren Module aufbauen. +place: pc +goals: + - Du erkundest ein fremdes Projekt in festen Schritten, von `project.godot` bis zum laufenden Spiel. + - Du legst das Testgelände aus Textdateien an und richtest die Eingabeaktionen in der Eingabe-Zuordnung ein. + - Du liest echte Fehlermeldungen und findest über den GDScript-Backtrace die Zeile, die sie auslöst. + - Du untersuchst das laufende Spiel im Remote-Baum und im Remote-Inspektor und weißt, was davon erhalten bleibt. + - Du findest Verbindungen, Gruppen und Signale im Editor und im Terminal. + - Du beschreibst ein Projekt als Projektkarte in eigenen Worten. +resources: + - title: Godot Docs · Input examples + url: https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html + - title: Godot Docs · Using signals + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html + - title: Godot Docs · Game embedding + url: https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html +lesson: | + Ein Coding-Agent hat dir ein Projekt gebaut, oder du lädst ein Beispielprojekt herunter. Bevor du irgendeinen Wert änderst, willst du wissen: Was startet? Woraus besteht es? Wo stehen die Werte, und wer reagiert auf wen? Wer drauflos klickt, ändert schnell die Vorlage statt einer Instanz oder sucht eine Verbindung im falschen Dock. + + Dieses Modul gibt dir dafür ein festes Vorgehen und übt es am **Testgelände**, das du hier aus Textdateien anlegst. Die späteren Module des Kurses bauen darauf auf: Sprites, Animationen, Töne, Tiles und Messungen hängen an genau diesem Projekt. Du brauchst Godot 4.7.2 am PC ([Godot-Start](/combat/godot-start)) und einen Texteditor. Docks, Dateien und Szenenbäume kennst du aus [Der Editor als Werkstatt](/godot/editor-anatomie), [Szenenbäume lesen](/godot/szenenbaum) und [Instanzen und Ressourcen](/godot/instanzen-ressourcen). + + ## Vorgehen: in sieben Schritten zur Projektkarte + + 1. **`project.godot` lesen.** Hauptszene (`run/main_scene`), Autoloads (`[autoload]`), Eingabeaktionen (`[input]`), die Namen der Kollisionsebenen (`[layer_names]`, in den Projekteinstellungen unter *Ebenen-Namen*). Die Abschnitte zeigt dir ein Befehl auf einen Blick, hier für das Testgelände direkt nach dem Anlegen: + + ```console + $ grep -n "^\[" project.godot + 3:[application] + 9:[display] + 18:[layer_names] + 24:[rendering] + ``` + + 2. **Starten und zusehen.** F5 startet die Hauptszene. Achte auf das untere Panel: `print()`-Zeilen erscheinen unter *Ausgabe*, Fehler des Spiels im *Debugger*. + 3. **Hauptszene öffnen und den Baum lesen.** Sie steht im Dateisystem-Dock in Blau. Instanzen folgst du über die Filmklappe „Im Editor öffnen“. + 4. **Skripte überfliegen.** `class_name` zeigt eigene Typen, `signal` die Ereignisse, `@export` die Werte, die im Inspektor einstellbar sind: + + ```console + $ grep -rn --include="*.gd" -e class_name -e "^signal" -e @export . + ./movement_stats.gd:1:class_name MovementStats + ./movement_stats.gd:6:@export_range(0, 400, 1, "suffix:px/s") var max_speed := 90.0 + ./movement_stats.gd:7:@export_range(0, 5000, 10, "suffix:px/s²") var acceleration := 600.0 + ./movement_stats.gd:8:@export_range(0, 5000, 10, "suffix:px/s²") var friction := 900.0 + ./player.gd:1:class_name Player + ./player.gd:6:signal collected(total: int) + ./player.gd:8:@export var stats: MovementStats + ./player.gd:9:@export var debug_log := false + ./pickup.gd:5:@export var value := 1 + ``` + + Die Reihenfolge der Dateien kann bei dir anders sein; `grep` liest sie in der Reihenfolge des Dateisystems. + 5. **Verbindungen und Gruppen prüfen**, im Signale-Dock und im Gruppen-Dock. + 6. **Suchen**, mit *Projekt → In Dateien suchen …* (Strg + Umschalt + F) oder im Terminal. + 7. **Zur Laufzeit hineinsehen**, im Remote-Baum und im Remote-Inspektor. + + Das Ergebnis schreibst du als **Projektkarte** auf: welche Szene startet, welche Szenen sie einsetzt, wo die Werte liegen, welche Signale welche Reaktion auslösen. Das ist die letzte Übung dieses Moduls. + + > [!PRAXIS] + > Lass dir von einem Agenten zu jedem größeren Umbau eine kurze Projektkarte schreiben und vergleiche sie mit dem, was du im Editor siehst. Weicht etwas ab, hat entweder die Karte oder das Projekt einen Fehler. Beides willst du wissen, bevor du abstimmst. + + ## Das Testgelände anlegen + + Das Testgelände ist absichtlich neutral: ein Rechteck als Figur, eine Kiste, drei Sammelstücke, ein Zähler. Alle Werte sind Platzhalter. Leg den Ordner `~/godot/testgelaende` mit dem Unterordner `data` an und darin diese zehn Dateien mit genau diesem Inhalt. Die Skripte sind mit Tabs eingerückt. Leerzeichen gingen auch, aber nicht gemischt in einer Datei; dann meldet Godot `Parse Error: Used space character for indentation instead of tab as used before in the file.` Achte beim Kopieren darauf, dass dein Editor die Tabs nicht umwandelt. + + **`project.godot`** (ohne `[input]`, die Aktionen legst du gleich im Editor an): + + ```ini + config_version=5 + + [application] + + config/name="Testgelände" + run/main_scene="res://main.tscn" + config/features=PackedStringArray("4.7", "GL Compatibility") + + [display] + + window/size/viewport_width=320 + window/size/viewport_height=180 + window/size/window_width_override=1280 + window/size/window_height_override=720 + window/stretch/mode="viewport" + window/stretch/scale_mode="integer" + + [layer_names] + + 2d_physics/layer_1="world" + 2d_physics/layer_2="player" + 2d_physics/layer_3="pickups" + + [rendering] + + textures/canvas_textures/default_texture_filter=0 + renderer/rendering_method="gl_compatibility" + renderer/rendering_method.mobile="gl_compatibility" + 2d/snap/snap_2d_transforms_to_pixel=true + ``` + + **`main.tscn`**, die Hauptszene: + + ```ini + [gd_scene format=3] + + [ext_resource type="PackedScene" path="res://player.tscn" id="1"] + [ext_resource type="PackedScene" path="res://crate.tscn" id="2"] + [ext_resource type="PackedScene" path="res://pickup.tscn" id="3"] + [ext_resource type="Script" path="res://hud.gd" id="4"] + + [node name="Main" type="Node2D"] + + [node name="Floor" type="ColorRect" parent="."] + offset_left = -200.0 + offset_top = -120.0 + offset_right = 200.0 + offset_bottom = 120.0 + color = Color(0.22, 0.3, 0.22, 1) + + [node name="Objects" type="Node2D" parent="."] + y_sort_enabled = true + + [node name="Player" parent="Objects" instance=ExtResource("1")] + + [node name="Crate" parent="Objects" instance=ExtResource("2")] + position = Vector2(40, 10) + + [node name="Pickup" parent="Objects" instance=ExtResource("3")] + position = Vector2(-50, -20) + + [node name="Pickup2" parent="Objects" instance=ExtResource("3")] + position = Vector2(-70, 30) + + [node name="Pickup3" parent="Objects" instance=ExtResource("3")] + position = Vector2(70, 40) + + [node name="HUD" type="CanvasLayer" parent="."] + script = ExtResource("4") + + [node name="Counter" type="Label" parent="HUD"] + offset_left = 4.0 + offset_top = 2.0 + offset_right = 44.0 + offset_bottom = 18.0 + text = "0" + + [connection signal="collected" from="Objects/Player" to="HUD" method="show_total"] + ``` + + **`player.tscn`**, die Figur mit Fußform und Kamera: + + ```ini + [gd_scene format=3] + + [ext_resource type="Script" path="res://player.gd" id="1"] + [ext_resource type="Resource" path="res://data/player_stats.tres" id="2"] + + [sub_resource type="RectangleShape2D" id="feet"] + size = Vector2(10, 6) + + [node name="Player" type="CharacterBody2D"] + collision_layer = 2 + collision_mask = 1 + motion_mode = 1 + script = ExtResource("1") + stats = ExtResource("2") + + [node name="Body" type="Polygon2D" parent="."] + color = Color(0.35, 0.65, 0.95, 1) + polygon = PackedVector2Array(-6, -16, 6, -16, 6, 0, -6, 0) + + [node name="Feet" type="CollisionShape2D" parent="."] + position = Vector2(0, -3) + shape = SubResource("feet") + + [node name="Camera2D" type="Camera2D" parent="."] + ``` + + **`crate.tscn`**, ein festes Hindernis: + + ```ini + [gd_scene format=3] + + [sub_resource type="RectangleShape2D" id="base"] + size = Vector2(16, 8) + + [node name="Crate" type="StaticBody2D"] + collision_mask = 0 + + [node name="Body" type="Polygon2D" parent="."] + color = Color(0.6, 0.45, 0.3, 1) + polygon = PackedVector2Array(-8, -16, 8, -16, 8, 0, -8, 0) + + [node name="Base" type="CollisionShape2D" parent="."] + position = Vector2(0, -4) + shape = SubResource("base") + ``` + + **`pickup.tscn`**, ein Sammelstück: + + ```ini + [gd_scene format=3] + + [ext_resource type="Script" path="res://pickup.gd" id="1"] + + [sub_resource type="CircleShape2D" id="area"] + radius = 5.0 + + [node name="Pickup" type="Area2D"] + collision_layer = 4 + collision_mask = 2 + monitorable = false + script = ExtResource("1") + + [node name="Body" type="Polygon2D" parent="."] + color = Color(0.95, 0.8, 0.2, 1) + polygon = PackedVector2Array(0, -10, 4, -5, 0, 0, -4, -5) + + [node name="Area" type="CollisionShape2D" parent="."] + position = Vector2(0, -5) + shape = SubResource("area") + + [connection signal="body_entered" from="." to="." method="_on_body_entered"] + ``` + + **`player.gd`**: + + ```gdscript + class_name Player + extends CharacterBody2D + + ## Figur des Testgeländes: läuft analog in alle Richtungen und sammelt ein. + + signal collected(total: int) + + @export var stats: MovementStats + @export var debug_log := false + + var total := 0 + var facing := Vector2.DOWN + + + func _physics_process(delta: float) -> void: + var input := Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down") + var rate := stats.acceleration if input != Vector2.ZERO else stats.friction + velocity = velocity.move_toward(input * stats.max_speed, rate * delta) + if input != Vector2.ZERO: + facing = input + move_and_slide() + if debug_log: + print(Engine.get_physics_frames(), " speed ", snappedf(velocity.length(), 0.1)) + + + func collect(value: int) -> void: + total += value + collected.emit(total) + ``` + + **`movement_stats.gd`**: + + ```gdscript + class_name MovementStats + extends Resource + + ## Bewegungswerte der Figur. Platzhalter: Die echten Werte bestimmst du. + + @export_range(0, 400, 1, "suffix:px/s") var max_speed := 90.0 + @export_range(0, 5000, 10, "suffix:px/s²") var acceleration := 600.0 + @export_range(0, 5000, 10, "suffix:px/s²") var friction := 900.0 + ``` + + **`pickup.gd`**: + + ```gdscript + extends Area2D + + ## Sammelstück: meldet sich bei der Figur und verschwindet. + + @export var value := 1 + + + func _on_body_entered(body: Node2D) -> void: + if body is Player: + body.collect(value) + queue_free() + ``` + + **`hud.gd`**: + + ```gdscript + extends CanvasLayer + + ## Zeigt, wie viele Sammelstücke die Figur hat. + + @onready var counter: Label = $Counter + + + func show_total(total: int) -> void: + counter.text = str(total) + ``` + + **`data/player_stats.tres`**: + + ```ini + [gd_resource type="Resource" script_class="MovementStats" format=3] + + [ext_resource type="Script" path="res://movement_stats.gd" id="1"] + + [resource] + script = ExtResource("1") + max_speed = 90.0 + acceleration = 600.0 + friction = 900.0 + ``` + + Öffne das Projekt mit `godot -e --path ~/godot/testgelaende` oder im Projektmanager über *Importieren*. Beim ersten Öffnen legt Godot `.godot/` und die `.uid`-Dateien an. Die Szenen sind von Hand geschrieben und haben weder `uid` noch `unique_id`; beides ergänzt der Editor beim ersten Speichern. Leg das Projekt deshalb vorher in Git an ([Git-Einstieg](/git/git-einstieg), `.godot/` gehört nicht hinein), dann siehst du diesen Diff getrennt von deinen eigenen Änderungen. + + Was die Teile tun, in einem Satz je Datei: `player.gd` beschleunigt die Figur mit `move_toward()` auf `stats.max_speed` und bremst mit `stats.friction`. `pickup.gd` ruft beim Betreten `collect()` an der Figur auf und löscht sich mit `queue_free()`. `collect()` zählt hoch und sendet `collected`, und `main.tscn` verbindet dieses Signal mit `show_total()` in `hud.gd`. Die Kiste ist ein `StaticBody2D` auf Ebene 1, gegen den die Figur läuft; Ebenen und Masken erklärt das [Modul zu Kollisionsebenen](/godot/kollision-ebenen). + + ## Eingabeaktionen in der Eingabe-Zuordnung + + Starte das Projekt jetzt mit F5, bevor du Aktionen anlegst. Die Figur steht still, und neben *Debugger* erscheint eine rote Zahl, die schnell wächst. Im Terminal gestartet sieht der Anfang so aus: + + ```text + ERROR: The InputMap action "move_down" doesn't exist. Did you mean "ui_down"? + at: get_action_raw_strength (core/input/input.cpp:553) + GDScript backtrace (most recent call first): + [0] _physics_process (res://player.gd:16) + ERROR: The InputMap action "move_up" doesn't exist. Did you mean "ui_up"? + at: get_action_raw_strength (core/input/input.cpp:553) + GDScript backtrace (most recent call first): + [0] _physics_process (res://player.gd:16) + ``` + + So liest du eine Godot-Fehlermeldung: Die Zeile mit `ERROR:` sagt, was schiefging. `at:` nennt die Stelle in Godots C++-Quelltext, die dir selten hilft. Der **GDScript-Backtrace** nennt deine Datei und Zeile, hier `player.gd:16`, den Aufruf von `Input.get_vector()`. „Did you mean“ schlägt einen ähnlichen Namen vor. Die Meldung kommt in jedem Physik-Frame neu, deshalb wächst die Zahl am Debugger so schnell: + + ![Unteres Panel des Editors: Reiter Ausgabe, Debugger mit roter Zahl 3335, Audio, Animation, Shader-Editor](/media/godot/projekt-erkunden/debugger-zaehler@2x.png "Godot 4.7.2, Testgelände ohne Eingabeaktionen nach einigen Sekunden") + + Jetzt die Aktionen. Öffne *Projekt → Projekteinstellungen …* und den Reiter **Eingabe-Zuordnung** (Input Map). Trag in das Feld „Neue Aktion hinzufügen“ `move_left` ein und klick *Hinzufügen*. Das `+` rechts neben der Aktion öffnet den Dialog für ein Ereignis. Drück im Feld „Warte auf Eingabe“ die Taste A. Unter *Zusätzliche Einstellungen* steht danach „Tastencode (Lateinäquivalent)“; stell es auf *Physischer Tastencode (Position auf der US-QWERTY-Tastatur)* und bestätige mit OK. + + ![Dialog für ein neues Ereignis nach dem Druck auf A: oben „A or A (Physisch)“, unten unter Zusätzliche Einstellungen die Auswahl „Tastencode (Lateinäquivalent)“, darunter OK und Abbrechen](/media/godot/projekt-erkunden/ereignis-dialog@2x.png "Godot 4.7.2, Ereignis-Dialog der Eingabe-Zuordnung, Voreinstellung vor dem Umstellen") + + Füg danach die Pfeiltaste links hinzu, ebenfalls als physischen Tastencode, und unter *Joypad-Achsen* den Eintrag mit „Linker Stick links, Joystick 0 links“. Dasselbe für `move_right` (D, Pfeil rechts, Stick rechts), `move_up` (W, Pfeil hoch, Stick hoch) und `move_down` (S, Pfeil runter, Stick runter). Die Totzone bleibt auf 0.2 ([Input examples](https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html)). + + ![Projekteinstellungen, Reiter Eingabe-Zuordnung: Aktionen move_left, move_right, move_up, move_down mit Totzone 0.2, darunter je eine Buchstabentaste und eine Pfeiltaste als „Physisch“ und eine Joypad-Achse des linken Sticks für alle Geräte](/media/godot/projekt-erkunden/eingabe-zuordnung@2x.png "Godot 4.7.2, Projekteinstellungen → Eingabe-Zuordnung im Testgelände") + + Der physische Tastencode meint die Position der Taste, nicht ihre Beschriftung. Warum das auf einer deutschen Tastatur zählt und was Totzonen bewirken, steht im [Modul zu Eingabeaktionen und Parametern](/godot/aktionen-parameter). Nach dem Schließen steht in `project.godot` ein Abschnitt `[input]` mit vier Einträgen `move_left={ … }` und so weiter. Godot schreibt die Datei dabei neu und setzt seinen Kommentarkopf davor. + + ## Starten, prüfen, Ausgaben lesen + + F5 noch einmal. Die Figur läuft mit Tastatur und Stick in alle Richtungen, die Kamera folgt ihr, und jedes eingesammelte Stück zählt den Zähler um eins hoch. Für Messungen schaltest du `Debug Log` im Inspektor der Figur ein, in `player.tscn` für alle oder als Override an der Instanz in `main.tscn`. Dann schreibt `player.gd` in jedem Physik-Frame die Framenummer und das Tempo in die Ausgabe. Ein Lauf, in dem Pfeil rechts von Frame 1 bis 12 gedrückt war (Ausschnitt): + + ```text + 1 speed 10.0 + 2 speed 20.0 + 3 speed 30.0 + … + 9 speed 90.0 + 10 speed 90.0 + … + 13 speed 75.0 + 14 speed 60.0 + ``` + + Im Editor sind die Framenummern größer, weil das Spiel schon läuft, bevor du drückst. Was diese Zahlen über die Platzhalterwerte verraten, rechnest du im [Modul zu Eingabeaktionen und Parametern](/godot/aktionen-parameter) nach. + + > [!MERKE] + > Lies den Backtrace von oben: Der oberste Eintrag `[0]` ist die Zeile, in der der Fehler auftrat, die Einträge darunter zeigen, wer sie aufgerufen hat. + + ## Zur Laufzeit hineinsehen: Remote-Baum und Remote-Inspektor + + Während das Spiel läuft, hat das Szenen-Dock oben zwei Knöpfe: **Lokal** zeigt die Szene, wie sie gespeichert ist, **Remote** den Baum des laufenden Spiels ([Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html)). Der Remote-Baum beginnt bei `root` und zeigt die Instanzen aufgeklappt, mit `Body`, `Feet` und `Camera2D` unter `Player`: + + ![Szenen-Dock im Remote-Modus: root, darunter Main mit Floor, Objects, Player mit Body, Feet, Camera2D, Crate mit Body und Base, Pickup mit Body und Area](/media/godot/projekt-erkunden/remote-baum@2x.png "Godot 4.7.2, Remote-Baum des laufenden Testgeländes") + + Wählst du dort `Player`, zeigt der Inspektor das laufende Objekt; oben steht „Remote-Player“ mit einer Objektnummer. Ganz oben unter *Members* stehen auch Variablen, die nicht exportiert sind, hier `total` und `facing`: + + ![Remote-Inspektor der Figur: Abschnitt Members mit Total 0 und Facing (0, 1), darunter Node mit Path, dann Player mit Stats und Debug Log und CharacterBody2D](/media/godot/projekt-erkunden/remote-inspektor@2x.png "Godot 4.7.2, Remote-Inspektor von Player im laufenden Spiel") + + Das ist dein Elements-Panel aus den DevTools. Und wie dort gilt: Änderst du hier einen Wert, wirkt er sofort und ist nach dem Beenden weg ([Game embedding](https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#doc-game-embedding-interaction-mode)). Gefällt dir ein Wert, trägst du ihn danach im lokalen Inspektor oder in der `.tres` ein. Sammelst du ein Stück ein, verschwindet es aus dem Remote-Baum, weil `pickup.gd` es mit `queue_free()` löscht ([Node.queue_free](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-queue-free)). + + > [!WARNING] Remote kostet Zeit + > Der Tooltip des Remote-Knopfs warnt: Solange der Remote-Baum offen ist, bringt jede Aktualisierung das Projekt zum Ruckeln. Schalt auf *Lokal* zurück, bevor du Bildrate oder Timing beurteilst. + + ## Verbindungen, Gruppen und die Suche + + Das **Signale-Dock** listet für den gewählten Node alle Signale, gegliedert nach Klassen, und darunter ihre Verbindungen: + + ![Signale-Dock mit gewähltem Player: Signal collected(total: int), darunter die Verbindung ../../HUD :: show_total](/media/godot/projekt-erkunden/signale-dock@2x.png "Godot 4.7.2, Signale-Dock für Player in main.tscn") + + `../../HUD :: show_total` heißt: Empfänger ist der Node zwei Ebenen höher und dann `HUD`, aufgerufen wird `show_total`. Wählst du `Pickup`, steht dort `body_entered` mit einer Verbindung zu `_on_body_entered` am Pickup selbst; sie stammt aus `pickup.tscn`. Zusammen ergibt sich die Kette, die den Zähler bewegt: + + ```text + Pickup.body_entered Signal, verbunden in pickup.tscn + → Pickup._on_body_entered(body) + → Player.collect(value) direkter Aufruf im Code + → Player.collected Signal, verbunden in main.tscn + → HUD.show_total(total) + → Counter.text + ``` + + Der mittlere Schritt ist ein gewöhnlicher Methodenaufruf und steht in keinem Dock, nur im Code. Ein Signal entspricht einem Event mit Callback im Web; anders als dort liegt die Verbindung oft in der Szene statt im Code ([Using signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html)). + + **Gruppen** sind Etiketten für Nodes, ähnlich einer CSS-Klasse, über die Code alle Mitglieder auf einmal findet ([Groups](https://docs.godotengine.org/en/stable/tutorials/scripting/groups.html)). Das Testgelände hat keine. Fügst du im Gruppen-Dock die Wurzel von `pickup.tscn` der Gruppe `pickups` hinzu, steht ihre Kopfzeile danach so in der Datei: `[node name="Pickup" type="Area2D" groups=["pickups"]]`, in 4.7.2 gespeichert zusätzlich mit `unique_id`. Weil die Gruppe an der Vorlage hängt, gehören alle drei Instanzen dazu, und `get_tree().get_nodes_in_group(&"pickups")` liefert alle drei ([SceneTree.get_nodes_in_group](https://docs.godotengine.org/en/stable/classes/class_scenetree.html#class-scenetree-method-get-nodes-in-group)). + + **Suchen.** *In Dateien suchen* durchsucht in 4.7.2 standardmäßig nur `.gd`- und `.gdshader`-Dateien; so steht es in der Projekteinstellung `editor/script/search_in_file_extensions` ([ProjectSettings](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-editor-script-search-in-file-extensions)). Verbindungen in `.tscn`-Dateien findet die Suche deshalb nicht. Im Terminal schon: + + ```console + $ grep -rn collected --exclude-dir=.godot . + ./player.gd:6:signal collected(total: int) + ./player.gd:28: collected.emit(total) + ./main.tscn:44:[connection signal="collected" from="Objects/Player" to="HUD" method="show_total"] + ``` + + > [!INTERVIEW] + > „Du übernimmst ein Godot-Projekt, das du nicht kennst. Was machst du in der ersten Stunde?“ Gute Antwort: Ich lese `project.godot` auf Hauptszene, Autoloads, Aktionen und Ebenen, starte das Spiel und schaue in Ausgabe und Debugger. Dann folge ich dem Baum der Hauptszene in die Instanzen, überfliege die Skripte nach `class_name`, `signal` und `@export` und prüfe Verbindungen im Signale-Dock und per `grep` in den Szenen. Zum Schluss sehe ich im Remote-Baum nach, was zur Laufzeit existiert, und schreibe mir eine Projektkarte, bevor ich etwas ändere. + + ## Häufige Fehler + + - **Aktion fehlt oder ist vertippt.** Die Figur steht, der Debugger zählt in jedem Frame einen Fehler. „Did you mean“ nennt oft den vertippten Namen. + - **Signal mit falscher Zahl von Argumenten gesendet.** `Method expected 1 argument(s), but called with 0.` Sender und Empfänger müssen gleich viele Werte erwarten. + - **Im Remote-Inspektor abgestimmt und nichts übertragen.** Nach dem Beenden ist der Wert weg. Schreib ihn sofort in den lokalen Inspektor oder die `.tres`. + - **Remote-Baum offen beim Messen.** Er bringt das Spiel zum Ruckeln. Vor Messungen auf Lokal schalten. + - **Verbindung mit *In Dateien suchen* gesucht.** Die Suche schaut standardmäßig nicht in `.tscn`. Signale-Dock oder `grep` nehmen. + - **Nicht committet vor dem ersten Speichern.** Der Diff mit `uid` und `unique_id` vermischt sich mit deinen Änderungen. + + ## Zusammenfassung + + - Ein fremdes Projekt liest du in festen Schritten: `project.godot`, Start, Hauptszene, Skripte, Verbindungen und Gruppen, Suche, Laufzeit. + - Das Testgelände besteht aus zehn Textdateien. Die Eingabeaktionen legst du in *Projekteinstellungen → Eingabe-Zuordnung* an, mit physischem Tastencode und Stick-Achse. + - Fehlermeldungen liest du über den GDScript-Backtrace bis zu deiner Datei und Zeile. Fehler des Spiels zählt der Debugger. + - Der Remote-Baum zeigt das laufende Spiel mit aufgeklappten Instanzen, der Remote-Inspektor auch nicht exportierte Variablen. Änderungen dort gehen beim Beenden verloren. + - Das Signale-Dock zeigt Verbindungen je Node; direkte Methodenaufrufe siehst du nur im Code. + - *In Dateien suchen* durchsucht standardmäßig nur Skripte und Shader, `grep` auch Szenen. +exercises: + - id: was-steht-in-project-godot + type: choice + title: Was verrät project.godot? + prompt: | + Du hast die Eingabeaktionen angelegt. Welche Fragen beantwortet dir `project.godot` des Testgeländes jetzt direkt? + options: + - text: Welche Szene F5 startet. + correct: true + why: "`run/main_scene` im Abschnitt `[application]`." + - text: Wie die Kollisionsebenen 1 bis 3 heißen. + correct: true + why: Die Namen stehen unter `[layer_names]`. + - text: Welche Tasten `move_left` auslösen. + correct: true + why: Nach dem Anlegen in der Eingabe-Zuordnung stehen Aktionen und Ereignisse unter `[input]`. + - text: Wie viele Sammelstücke die Hauptszene enthält. + correct: false + why: Das steht in `main.tscn`. `project.godot` nennt nur die Hauptszene, nicht ihren Inhalt. + - text: Wie schnell die Figur läuft. + correct: false + why: "`max_speed` liegt in `data/player_stats.tres` bzw. als Standard in `movement_stats.gd`." + explanation: | + `project.godot` hält alles, was für das ganze Projekt gilt: Hauptszene, Fenster und Darstellung, Eingabeaktionen, Ebenen-Namen, Autoloads. Was in einer Szene steckt und welche Werte eine Figur hat, steht in den Szenen und Ressourcen. Deshalb ist die Datei der erste Schritt beim Erkunden, aber nicht der letzte. + resources: + - title: Godot Docs · File system · project.godot + url: https://docs.godotengine.org/en/stable/tutorials/scripting/filesystem.html#project-godot + + - id: remote-aussagen + type: choice + title: Was der Remote-Baum zeigt + prompt: | + Das Testgelände läuft, im Szenen-Dock ist *Remote* gewählt. Welche Aussagen stimmen? + options: + - text: Unter `Player` stehen `Body`, `Feet` und `Camera2D`, obwohl `Player` im lokalen Baum zugeklappt ist. + correct: true + why: Zur Laufzeit ist eine Instanz ein gewöhnlicher Teilbaum. + - text: Im Remote-Inspektor siehst du unter *Members* auch `total` und `facing`, die nicht exportiert sind. + correct: true + why: Der Remote-Inspektor zeigt die Variablen des laufenden Skripts. + - text: Setzt du dort `Debug Log` an, steht der Wert nach dem Beenden in `player.tscn`. + correct: false + why: Änderungen am laufenden Spiel gehen beim Beenden verloren. In die Datei kommt nur, was du im lokalen Inspektor änderst. + - text: Der Remote-Baum ist kostenlos und kann beim Messen der Bildrate offen bleiben. + correct: false + why: Laut Tooltip bringt jede Aktualisierung das Projekt zum Ruckeln. Vor Messungen auf Lokal schalten. + explanation: | + Der Remote-Baum ist die Laufzeitsicht: alle Nodes, auch die in Instanzen, und im Inspektor auch die Variablen, die du im Editor nicht siehst. Er ist ein Werkzeug zum Nachsehen und Ausprobieren, kein Ort zum Speichern. Was dort gut aussieht, überträgst du von Hand in die Szene oder Ressource. + resources: + - title: Godot Docs · Game embedding + url: https://docs.godotengine.org/en/stable/tutorials/editor/game_embedding.html#doc-game-embedding-interaction-mode + + - id: erkundung-ordnen + type: order + title: Erst lesen, dann ändern + prompt: | + Bring die Schritte beim Erkunden eines fremden Projekts in eine sinnvolle Reihenfolge. + lang: prose + lines: | + project.godot auf Hauptszene, Autoloads, Aktionen und Ebenen lesen + Das Spiel mit F5 starten und Ausgabe und Debugger beobachten + Die Hauptszene öffnen und ihren Baum bis in die Instanzen lesen + Skripte nach class_name, signal und @export durchsehen + Verbindungen im Signale-Dock und per grep in den Szenen prüfen + Im Remote-Baum nachsehen, was zur Laufzeit existiert + Die Projektkarte schreiben und erst dann Werte ändern + alternatives: + - [1, 3, 2, 4, 5, 6, 7] + - [1, 2, 3, 5, 4, 6, 7] + - [1, 3, 2, 5, 4, 6, 7] + explanation: | + `project.godot` sagt dir, was startet und welche Aktionen es gibt, das brauchst du vor dem ersten Start. Ob du zuerst startest oder zuerst den Baum liest, ist egal, ebenso die Reihenfolge von Skripten und Verbindungen; diese Paare darfst du tauschen. Der Remote-Baum zeigt, was du vorher im Baum und in den Skripten gelesen hast, zur Laufzeit, und kommt deshalb danach. Geändert wird erst, wenn die Karte steht. + resources: + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html + + - id: suche-zaehlen + type: output + title: Was findet die Suche? + prompt: | + Im Testgelände suchst du mit *Projekt → In Dateien suchen …* nach `collected`, mit den Standardeinstellungen von Godot 4.7.2. Wie viele Trefferzeilen zeigt die Suche? Zur Erinnerung die Treffer im Terminal: + lang: text + code: | + $ grep -rn collected --exclude-dir=.godot . + ./player.gd:6:signal collected(total: int) + ./player.gd:28: collected.emit(total) + ./main.tscn:44:[connection signal="collected" from="Objects/Player" to="HUD" method="show_total"] + expected: | + 2 + accept: + - 2 Treffer + - 2 Zeilen + - zwei + - zwei Treffer + - zwei Zeilen + hints: + - Welche Dateiendungen durchsucht *In Dateien suchen* standardmäßig? + explanation: | + Die Projekteinstellung `editor/script/search_in_file_extensions` steht in 4.7.2 auf `gd` und `gdshader`. Die Suche findet also die zwei Zeilen in `player.gd`, aber nicht die Verbindung in `main.tscn`. Willst du Szenen mit durchsuchen, ergänzt du dort `tscn`, oder du nimmst `grep`. Der Standardwert ist mit 4.7.2 ausgelesen. + resources: + - title: Godot Docs · ProjectSettings · search_in_file_extensions + url: https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-editor-script-search-in-file-extensions + + - id: gruppe-zaehlen + type: output + title: Eine Gruppe an der Vorlage + prompt: | + Du fügst die Wurzel von `pickup.tscn` im Gruppen-Dock der Gruppe `pickups` hinzu und speicherst. Dann läuft dieses Skript, nachdem `main.tscn` geladen und in den Baum gehängt wurde. Was gibt es aus? + lang: gdscript + code: | + print(get_tree().get_nodes_in_group(&"pickups").size()) + expected: | + 3 + hints: + - Die Gruppe steht in der Kopfzeile des Wurzel-Nodes der Vorlage. + explanation: | + Die Gruppe ist Teil der Vorlage: `[node name="Pickup" type="Area2D" … groups=["pickups"]]`. Jede Instanz bringt sie mit, also sind alle drei Sammelstücke Mitglied. So findet Code alle Sammelstücke, ohne ihre Pfade zu kennen, ähnlich wie `querySelectorAll(".pickup")`. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Groups + url: https://docs.godotengine.org/en/stable/tutorials/scripting/groups.html + + - id: signale-im-terminal + type: command + title: Alle Signale auf einen Blick + prompt: | + Liste alle Zeilen, die mit `signal` beginnen, aus allen `.gd`-Dateien des Projekts, rekursiv und mit Zeilennummern. + context: | + Du bist im Projektordner `~/godot/testgelaende`. + answers: + - grep -rn "^signal" --include="*.gd" . + - grep -rn --include="*.gd" "^signal" . + - grep -rn "^signal" . --include="*.gd" + - grep -rn "^signal" --include "*.gd" . + - grep -rn --include "*.gd" "^signal" . + - grep -rn "^signal" --include="*.gd" + - grep -rn --include="*.gd" "^signal" + - grep -rn -e "^signal" --include="*.gd" . + - grep -rn --include="*.gd" -e "^signal" . + - grep -Rn "^signal" --include="*.gd" . + - grep -Rn --include="*.gd" "^signal" . + - grep -Rn "^signal" . --include="*.gd" + output: | + ./player.gd:6:signal collected(total: int) + explanation: | + `-r` sucht rekursiv, `-n` zeigt Zeilennummern, `^` verankert das Muster am Zeilenanfang, sodass `collected.emit()` nicht mitkommt. `--include="*.gd"` beschränkt die Suche auf Skripte. Die Anführungszeichen sind in der fish-Shell Pflicht: Ohne sie meldet fish `No matches for wildcard '--include=*.gd'` und startet `grep` gar nicht. Im Testgelände gibt es genau ein eigenes Signal; mit `.` als Pfad stellt `grep` jedem Treffer `./` voran. + resources: + - title: Godot Docs · Using signals · Custom signals + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html#custom-signals + + - id: skript-lesen + type: gap + title: Was player.gd nach außen anbietet + prompt: | + Ergänze die Schlüsselwörter, an denen du beim Überfliegen eines Skripts erkennst, was es anbietet: einen eigenen Typ, ein Signal, einen Wert im Inspektor, das Senden des Signals. + lang: gdscript + code: | + ⟦class_name⟧ Player + extends CharacterBody2D + + ⟦signal⟧ collected(total: int) + + ⟦@export⟧ var stats: MovementStats + + var total := 0 + + + func collect(value: int) -> void: + total += value + collected.⟦emit⟧(total) + gaps: + - label: Eigener Typ + hint: Macht `Player` im ganzen Projekt als Typ bekannt. + - label: Ereignis + hint: Deklariert ein Ereignis, das andere Nodes verbinden können. + - label: Wert im Inspektor + hint: Eine Annotation mit `@`. + - label: Senden + hint: Die Methode eines Signals, die alle Verbindungen aufruft. + explanation: | + `class_name` registriert den Typ, deshalb kann `pickup.gd` `body is Player` prüfen und der Inspektor nur passende Ressourcen für `stats` anbieten. `signal` deklariert ein Ereignis mit seinen Argumenten, `.emit()` sendet es an alle verbundenen Methoden. `@export` macht die Variable im Inspektor einstellbar. Diese vier Wörter verraten dir in jedem fremden Skript, was es nach außen anbietet. + resources: + - title: Godot Docs · GDScript exported properties + url: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html + - title: Godot Docs · Using signals + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html + + - id: aktion-vertippt + type: bug + title: Die Figur läuft nach links, rechts und oben + prompt: | + Nach dem Anlegen der Aktionen läuft die Figur nach links, rechts und oben, aber nicht nach unten. Im Debugger steht in jedem Frame: + + ```text + ERROR: The InputMap action "move_down" doesn't exist. Did you mean "move_dwon"? + at: get_action_raw_strength (core/input/input.cpp:553) + GDScript backtrace (most recent call first): + [0] _physics_process (res://player.gd:16) + ``` + + Finde die Zeile in `project.godot` und korrigiere sie. Die Ereignisse sind gekürzt. + lang: ini + code: | + [input] + + move_left={ + "deadzone": 0.2, + "events": [ … ] + } + move_right={ + "deadzone": 0.2, + "events": [ … ] + } + move_up={ + "deadzone": 0.2, + "events": [ … ] + } + move_dwon={ + "deadzone": 0.2, + "events": [ … ] + } + lines: [15] + fix: + - line: 15 + answers: + - move_down={ + explanation: | + Der Code fragt `move_down` ab, angelegt ist `move_dwon`. Godot findet die Aktion nicht und schlägt den ähnlichsten vorhandenen Namen vor. Am einfachsten korrigierst du das in der Eingabe-Zuordnung per Doppelklick auf den Namen; in der Datei ist es diese eine Zeile. Mit 4.7.2 nachgestellt. + resources: + - title: Godot Docs · InputMap + url: https://docs.godotengine.org/en/stable/classes/class_inputmap.html + + - id: signal-ohne-wert + type: bug + title: Der Zähler bleibt bei 0 + prompt: | + Ein Agent hat `collect()` umgebaut. Seitdem bleibt der Zähler bei 0, und beim ersten eingesammelten Stück meldet der Debugger: + + ```text + ERROR: Error calling from signal 'collected' to callable: 'CanvasLayer(hud.gd)::show_total': Method expected 1 argument(s), but called with 0. + at: emit_signalp (core/object/object.cpp:1311) + GDScript backtrace (most recent call first): + [0] collect (res://player.gd:28) + [1] _on_body_entered (res://pickup.gd:10) + ``` + + Finde die Zeile und korrigiere sie. + lang: gdscript + code: | + func collect(value: int) -> void: + total += value + collected.emit() + lines: [3] + fix: + - line: 3 + answers: + - collected.emit(total) + - emit_signal("collected", total) + - emit_signal(&"collected", total) + explanation: | + `show_total(total: int)` erwartet einen Wert, `collected.emit()` sendet keinen. Godot ruft die Methode deshalb nicht auf, und der Zähler bleibt stehen. Der Backtrace zeigt den Weg: `pickup.gd:10` ruft `collect`, und in `player.gd:28` wird gesendet. Die Deklaration `signal collected(total: int)` sagt, was gesendet werden soll; erzwungen wird es beim Senden nicht. Mit 4.7.2 nachgestellt. + resources: + - title: Godot Docs · Signal.emit + url: https://docs.godotengine.org/en/stable/classes/class_signal.html#class-signal-method-emit + + - id: fehlermeldung-erklaeren + type: explain + title: Eine Meldung, vier Teile + prompt: | + Diese Meldung erschien beim ersten Start des Testgeländes, in jedem Frame neu. Erkläre in drei bis fünf Sätzen, was jeder Teil sagt und was du tust. + lang: text + code: | + ERROR: The InputMap action "move_down" doesn't exist. Did you mean "ui_down"? + at: get_action_raw_strength (core/input/input.cpp:553) + GDScript backtrace (most recent call first): + [0] _physics_process (res://player.gd:16) + points: + - Die `ERROR`-Zeile sagt, dass das Skript eine Aktion `move_down` abfragt, die in der Eingabe-Zuordnung fehlt. + - "„Did you mean“ schlägt eine vorhandene, ähnlich benannte Aktion vor; `ui_down` ist eine eingebaute Aktion und nicht gemeint." + - "`at:` zeigt die Stelle in Godots C++-Quelltext, der Backtrace `[0]` die eigene Zeile: `player.gd:16`, der Aufruf von `Input.get_vector()`." + - Die Meldung kommt in jedem Physik-Frame, weil `_physics_process` sechzigmal pro Sekunde läuft. + - Abhilfe ist, die Aktionen in den Projekteinstellungen unter Eingabe-Zuordnung anzulegen, nicht den Code zu ändern. + explanation: | + Die erste Zeile ist die Diagnose: Eine Aktion namens `move_down` gibt es nicht. Godot schlägt `ui_down` vor, weil das die ähnlichste eingebaute Aktion ist; sie gehört zur Bedienung von Menüs und ist hier nicht gemeint. Die `at:`-Zeile verweist auf Godots eigenen Quelltext und hilft selten weiter, der Backtrace dagegen nennt `player.gd:16`, die Zeile mit `Input.get_vector()`. Weil `_physics_process` in jedem Physik-Frame läuft, wiederholt sich die Meldung dauernd. Die Lösung liegt nicht im Code, sondern in der Eingabe-Zuordnung: Dort fehlen die Aktionen. + resources: + - title: Godot Docs · Input examples + url: https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html + + - id: testgelaende-anlegen + type: practice + title: Das Testgelände anlegen und prüfen + prompt: | + Leg das Testgelände aus der Lektion an, öffne es in Godot 4.7.2, starte es einmal ohne Aktionen, leg dann die vier Aktionen in der Eingabe-Zuordnung an und prüf das Ergebnis. Notiere die Ausgabe von `ls -A` und die Abschnitte von `project.godot` danach. + lang: text + solution: | + $ ls -A + crate.tscn movement_stats.gd.uid + data pickup.gd + .git pickup.gd.uid + .gitignore pickup.tscn + .godot player.gd + hud.gd player.gd.uid + hud.gd.uid player.tscn + main.tscn project.godot + movement_stats.gd + + $ grep -n "^\[" project.godot + 11:[application] + 17:[display] + 26:[input] + 57:[layer_names] + 63:[rendering] + + Ohne Aktionen: Figur steht, Debugger zählt + "The InputMap action "move_down" doesn't exist." + Mit Aktionen: Figur läuft, Zähler zählt 1, 2, 3. + checklist: + - Godot öffnet das Projekt ohne Fehler beim Laden der Szenen, und F5 zeigt Boden, Figur, Kiste, drei Sammelstücke und den Zähler. + - Vor dem Anlegen der Aktionen hast du die Fehlermeldung gesehen und `player.gd:16` im Backtrace gefunden. + - Die vier Aktionen haben je eine Buchstabentaste und eine Pfeiltaste als physischen Tastencode und eine Achse des linken Sticks. + - "`project.godot` hat einen Abschnitt `[input]` mit `move_left`, `move_right`, `move_up` und `move_down`." + - Mit `Debug Log` siehst du in der Ausgabe Zeilen der Form `Frame speed Wert`. + - Der Stand vor dem ersten Speichern einer Szene liegt als Commit in Git, mit einer `.gitignore`, die `.godot/` ausschließt. + explanation: | + Die Zeilennummern der Abschnitte entstehen, weil Godot `project.godot` beim Speichern der Einstellungen neu schreibt, mit Kommentarkopf und dem neuen Abschnitt `[input]`. Das Testgelände ist ab jetzt deine Werkbank: Spätere Module hängen eigene Nodes mit eigenen kleinen Skripten an, statt die Vorlagen umzuschreiben. Deshalb lohnt der Commit an dieser Stelle; jede spätere Änderung siehst du als eigenen Diff. Die Ausgaben stammen aus einem Lauf mit 4.7.2. + resources: + - title: Godot Docs · Input examples + url: https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html + + - id: projektkarte + type: practice + title: Deine Projektkarte des Testgeländes + prompt: | + Schreib eine Projektkarte des Testgeländes in eigenen Worten, so, dass du sie in drei Wochen ohne Editor verstehst. Geh die sieben Schritte der Lektion durch und halte fest, was du gefunden hast. Die Form wählst du selbst. + lang: prose + checklist: + - Die Karte nennt die Hauptszene und welche Szenen sie wie oft einsetzt. + - Für jede Datei in `data/` und jedes `@export` steht, wo der Wert liegt und wer ihn liest. + - Die Kette vom Betreten eines Sammelstücks bis zur Zahl im Zähler steht vollständig da, mit Signalen und direkten Aufrufen. + - Die Eingabeaktionen sind mit ihren Ereignissen genannt. + - Mindestens eine Beobachtung stammt aus dem Remote-Baum oder Remote-Inspektor. + - Du hast notiert, was dir beim Erkunden unklar blieb. + hints: + - Beginne mit einem Satz, was das Projekt tut, dann gehe Datei für Datei vor. + - Die Kette aus der Lektion ist ein Anfang; schreib sie in deinen Worten. + explanation: | + Die Projektkarte ist das Ergebnis des ganzen Moduls und die Grundlage für alle weiteren: Wer weiß, wo was liegt, ändert gezielt und kann einem Coding-Agenten präzise sagen, wo er ansetzen soll. Offene Fragen sind kein Mangel, sondern der Arbeitsauftrag für die nächsten Module. Halte die Karte aktuell, wenn das Testgelände wächst. + resources: + - title: Godot Docs · Nodes and Scenes + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/nodes_and_scenes.html diff --git a/content/godot/modules/sprites-importieren.yaml b/content/godot/modules/sprites-importieren.yaml new file mode 100644 index 0000000..aa0e3f7 --- /dev/null +++ b/content/godot/modules/sprites-importieren.yaml @@ -0,0 +1,552 @@ +id: sprites-importieren +title: "Eigene Sprites importieren: Import-Dock, Spritesheets und Ursprung" +level: 1 +minutes: 150 +summary: Wie Godot eine PNG-Datei importiert, welche Einstellungen im Import-Dock Pixelart schützen, wie ein Sprite2D ein Spritesheet in Felder teilt und warum der Ursprung an die Füße gehört – geprüft an einem Testbogen, den du selbst anlegst. +place: pc +goals: + - Du erklärst, was in der Quelldatei, in der `.import`-Datei und in `.godot/imported` steht, und welche davon ins Repository gehören. + - Du liest das Import-Dock und erkennst die Einstellungen, die Pixelart unscharf machen oder verfälschen. + - Du unterscheidest den Texturfilter des Projekts von `texture_filter` am Node und liest beide in `project.godot` und `.tscn`. + - Du richtest ein `Sprite2D` mit `hframes`, `vframes`, `frame` und `offset` so ein, dass jedes Feld vollständig erscheint und der Ursprung an den Füßen liegt. + - Du exportierst ein Spritesheet aus Aseprite oder Bildfolgen aus Krita so, dass das Raster erhalten bleibt. +resources: + - title: Godot Docs · Import process + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html + - title: Godot Docs · Importing images + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html + - title: Godot Docs · Sprite2D + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html + - title: Aseprite Docs · Command Line Interface + url: https://www.aseprite.org/docs/cli/ +lesson: | + Du exportierst aus Aseprite ein Blatt mit vier Bildern, ziehst es ins Projekt und setzt es in ein `Sprite2D`. Im Spiel steht dann das ganze Blatt auf einmal da, halb im Boden versunken. Stellst du die Zahl der Felder falsch ein, siehst du halbe Bilder. Und ein Kollege meldet, auf seinem Rechner sei die Grafik verschmiert, obwohl in deinem Projekt alles scharf ist. + + Alle drei Fehler entstehen zwischen deiner Datei und dem Bild im Spiel: beim Import, beim Zerlegen des Blatts und beim Ursprung. Dieses Modul geht diesen Weg einmal vollständig durch. Statt einer Figur nimmst du einen Testbogen, ein technisches Prüfmuster, das du selbst in zwei Minuten anlegst. Wie deine Figuren aussehen und wie groß ihre Felder sind, entscheidest du später selbst. Die 16 × 16 Pixel hier sind ein Platzhalter. Wie die Pixel danach auf den Bildschirm kommen, behandelt [Pixelraster und Skalierung](/godot/pixelraster). + + ## Die Import-Pipeline: Quelle, .import und .godot/imported + + Legst du `test_sheet.png` in den Projektordner, importiert Godot sie sofort. Die PNG bleibt unverändert liegen. Daneben entsteht `test_sheet.png.import` mit den Einstellungen, und im versteckten Ordner `.godot/imported/` liegt das Ergebnis, das das Spiel lädt ([Files generated](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#files-generated)). So sieht die Datei aus, die Godot 4.7.2 für den Testbogen geschrieben hat (gekürzt): + + ```ini title=test_sheet.png.import + [remap] + + importer="texture" + type="CompressedTexture2D" + uid="uid://bnbb5aa4yerph" + path="res://.godot/imported/test_sheet.png-142e95a07cea5c3d31f4022e607774aa.ctex" + + [deps] + + source_file="res://test_assets/test_sheet.png" + + [params] + + compress/mode=0 + mipmaps/generate=false + process/fix_alpha_border=true + process/size_limit=0 + detect_3d/compress_to=1 + ``` + + Die `uid` ist eine feste Kennung der Ressource. Szenen nennen sie neben dem Pfad; nach dem Speichern steht in `player.tscn` beim Testbogen `uid="uid://bnbb5aa4yerph"`. In `.godot/imported/` stehen dazu eine `.ctex` (die fertige Textur) und eine `.md5` (Prüfsumme). Ändert sich die PNG, importiert Godot sie automatisch neu ([Automatic reimport](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#automatic-reimport)). Löschst du `.godot/`, entsteht alles beim nächsten Start neu. + + Für Webentwickler: Das ist ein Bundler. Die Quelle in `src/` bleibt, das Ergebnis landet mit Hash im Namen in einem Ausgabeordner, und die Konfiguration bestimmt, wie umgewandelt wird. Der Unterschied: In Godot steht die Konfiguration pro Datei direkt neben der Quelle, nicht zentral in einer `vite.config.ts`. + + Daraus folgt, was ins Repository gehört: die PNG und die `.import`-Datei ja, `.godot/` nein. Die Doku sagt ausdrücklich, dass die `.import`-Dateien wichtige Metadaten enthalten. Ohne sie importiert jeder Rechner mit Standardwerten, und genau so entsteht das verschmierte Bild beim Kollegen. Mehr zu `.gitignore` und großen Dateien in [Testversionen und Sicherung](/godot/export-sichern). + + > [!MERKE] + > Die Quelle bleibt unverändert, die Einstellungen stehen in `.import`, das Ergebnis in `.godot/imported`. Die ersten beiden versionierst du, das dritte erzeugt Godot. + + ## Das Import-Dock: was für Pixelart zählt + + Wählst du die PNG im Dock *Dateisystem* (FileSystem), zeigt das Dock *Importieren* (Import) ihre Einstellungen ([Changing import parameters](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#changing-import-parameters)). In Godot 4.7.2 mit deutscher Oberfläche sind dort auch die Optionsnamen übersetzt, anders als im Inspektor: + + ![Import-Dock für test_sheet.png: Importiere als Texture2D, Komprimieren Modus Lossless, Mipmaps Erzeugen aus, Prozessierung mit Alpharand beheben an und Größenbeschränkung 0, 3D Erkennen Komprimieren zu VRAM Compressed](/media/godot/sprites-importieren/import-dock.png "Godot 4.7.2, Import-Dock mit den Standardwerten für eine PNG") + + | Im Dock | Original | Standard | Für Pixelart | + | --- | --- | --- | --- | + | Komprimieren → Modus | Compress → Mode | Lossless | Lossless lassen | + | Mipmaps → Erzeugen | Mipmaps → Generate | aus | aus lassen | + | Prozessierung → Alpharand beheben | Fix Alpha Border | an | an lassen | + | Prozessierung → Größenbeschränkung | Size Limit | 0 | 0 lassen | + | 3D Erkennen → Komprimieren zu | Detect 3D → Compress To | VRAM Compressed | wirkt nur, wenn die Textur in 3D benutzt wird | + + Die Standardwerte passen für 2D-Pixelart. Gefährlich wird es, wenn jemand sie ändert. **Lossless** speichert jedes Pixel exakt und ist laut Doku die Empfehlung für Pixelart ([Compress Mode](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html#compress-mode)). **VRAM Compressed** spart Grafikspeicher, verfälscht aber Farben in kleinen Blöcken. **Größenbeschränkung** verkleinert große Texturen beim Import mit kubischer Interpolation ([Size Limit](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html#process-size-limit)). Gemessen mit dem Prüfmuster aus 4 Farben: + + ![Drei Prüfmuster im Spiel: Lossless scharf, VRAM Compressed mit dunklen Flecken und verschobenen Farben, Größenbeschränkung 16 halb so groß und grau verwaschen](/media/godot/sprites-importieren/import-varianten@2x.png "Godot 4.7.2, dieselbe PNG mit drei Import-Einstellungen, Spielbild vierfach vergrößert") + + Lossless liefert die 4 Farben der Quelle, VRAM Compressed nach dem Entpacken 13, Größenbeschränkung 16 ein Bild mit 16 × 16 Pixeln und 32 Farben. **Mipmaps** sind kleinere Kopien für verkleinerte Darstellung; in 2D kosten sie laut Doku rund 33 % mehr Speicher und lohnen sich nur, wenn die Kamera stark herauszoomt ([Mipmaps](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html#mipmaps-generate)). + + Über der Liste steht *Importiere als* (Import As). Für Sprites bleibt es `Texture2D`. Daneben öffnet *Vorgabe* (Preset) ein Menü mit „2D/3D (Autoerkennung)“, „2D“, „3D“ und „Als Default für ‚Texture2D‘ setzen“. Eine Vorgabe „2D Pixel“ wie in Godot 3 gibt es in 4.7 nicht, denn der Texturfilter ist seit Godot 4.0 keine Importeinstellung mehr ([Import options](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html#import-options)). Projektweite Standards pro Dateityp findest du unter *Projekt → Projekteinstellungen → Import-Default-Werte*. + + > [!WARNING] Erst Neuimport, dann weiter + > Änderungen im Import-Dock gelten erst nach einem Klick auf *Neuimport* (Reimport). Wählst du vorher eine andere Datei, sind sie verloren ([Changing import parameters](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#changing-import-parameters)). Wählst du mehrere Dateien zugleich, steht vor jeder Option ein Kästchen; nur die angekreuzten Optionen ändern sich bei allen ([Reimporting multiple assets](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#reimporting-multiple-assets)). + + ## Filter: Projektstandard und texture_filter am Node + + Ob ein Sprite beim Vergrößern scharf bleibt, entscheidet der Filter beim Zeichnen, nicht der Import. Das Testgelände stellt ihn für das ganze Projekt auf Nearest: In `project.godot` steht `textures/canvas_textures/default_texture_filter=0` ([default_texture_filter](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-rendering-textures-canvas-textures-default-texture-filter)). Jeder Node kann das überschreiben: Im Inspektor unter *CanvasItem → Texture → Filter* (`texture_filter`) stehen *Inherit*, *Nearest*, *Linear* und Varianten mit Mipmaps ([texture_filter](https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-texture-filter)). *Inherit* übernimmt den Wert des Elternknotens, am Ende den des Projekts. + + Vorsicht beim Lesen der Textdateien: Die beiden Einstellungen zählen verschieden. + + | Wert | `project.godot` | `texture_filter` in `.tscn` | + | --- | --- | --- | + | 0 | Nearest | Inherit (Zeile fehlt meist ganz) | + | 1 | Linear | Nearest | + | 2 | Linear Mipmap | Linear | + + Godot 4.7.2 schreibt `texture_filter = 1` für Nearest und `texture_filter = 2` für Linear in die Szene, gemessen. Stell den Filter einmal im Projekt ein und lass die Nodes auf *Inherit*. Ein einzelnes Sprite auf Linear fällt im Stretch Mode `viewport` erst auf, wenn es skaliert, gedreht oder zwischen Pixeln steht, im Modus `canvas_items` sofort ([Pixelraster](/godot/pixelraster)). + + ## Sprite2D: Raster, Ausschnitt und Ursprung + + Ein `Sprite2D` zeigt eine Textur. Ein Spritesheet teilt es selbst in Felder: `hframes` ist die Zahl der Spalten, `vframes` die der Zeilen, `frame` das gezeigte Feld, gezählt ab 0 von links oben, zeilenweise ([hframes](https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-hframes)). `frame_coords` ist dasselbe als Spalte und Zeile. Die Feldgröße rechnet Godot aus: Breite der Textur durch `hframes`. Für Blätter mit mehreren Zeilen nimmst du einen zweiten Testbogen `test_sheet_4x2.png`: 64 × 32 Pixel, zwei Zeilen zu je vier Feldern, nummeriert 1 bis 8. + + ![Inspektor eines Sprite2D: Texture mit dem Testbogen aus vier Feldern, Offset mit Centered an und Offset y −8, Animation mit Hframes 4, Vframes 1, Frame 0](/media/godot/sprites-importieren/inspector-sprite2d.png "Godot 4.7.2, Inspektor des Sprite2D mit dem Testbogen") + + Gemessen mit `get_rect()` am Testbogen (64 × 16, vier Felder): + + ```gdscript + sprite.hframes = 4 + print(sprite.get_rect()) # [P: (-8.0, -8.0), S: (16.0, 16.0)] + sprite.hframes = 8 + print(sprite.get_rect()) # [P: (-4.0, -8.0), S: (8.0, 16.0)] + sprite.hframes = 2 + print(sprite.get_rect()) # [P: (-16.0, -8.0), S: (32.0, 16.0)] + ``` + + Stimmt `hframes` nicht mit dem Blatt überein, zeigt jedes Feld ein halbes oder ein doppeltes Bild: + + ![Drei Zeilen mit dem Testbogen: bei hframes 4 die vier Felder mit Nummer 1 bis 4, bei hframes 8 nur halbe Felder, bei hframes 2 je zwei Felder auf einmal](/media/godot/sprites-importieren/hframes-vergleich@2x.png "Godot 4.7.2, Frames 0 bis 3 (unten 0 und 1) bei verschiedenen hframes") + + Für einen Ausschnitt ohne Raster gibt es **Region**: `region_enabled` an, und `region_rect` bestimmt das Rechteck in Pixeln. Dieselbe Idee als Ressource heißt `AtlasTexture`: eine Textur, die nur einen Teil einer anderen zeigt und überall verwendbar ist, wo eine Textur erwartet wird ([AtlasTexture](https://docs.godotengine.org/en/stable/classes/class_atlastexture.html)). SpriteFrames legt solche AtlasTextures an, wenn du Frames aus einem Sheet holst ([Animationen zuordnen](/godot/animationen)). + + ### Der Ursprung gehört an die Füße + + Standardmäßig ist `centered` an: Der Ursprung des Nodes liegt in der Mitte des Felds. Für eine Draufsicht mit Y-Sortierung brauchst du ihn an den Füßen, denn Godot sortiert nach der Position des Nodes. Steht der Ursprung in der Mitte, wird die Figur hinter einer Kiste gezeichnet, obwohl ihre Füße schon davor stehen, denn ihre Mitte liegt noch höher als der Ursprung der Kiste. Im Testgelände liegt der Ursprung des Players deshalb an den Füßen; die Form `Feet` sitzt bei y = −3. + + ![Zwei Felder des Testbogens mit gelbem Kreuz am Ursprung des Nodes: links in der Mitte des Felds, rechts an der Unterkante](/media/godot/sprites-importieren/ursprung@2x.png "Godot 4.7.2, links offset (0, 0), rechts offset (0, −8), gelb der Ursprung des Nodes") + + Zwei Wege führen dahin, beide gemessen mit demselben Ergebnis `[P: (-8.0, -16.0), S: (16.0, 16.0)]`: `centered` an und `offset = Vector2(0, -8)` (halbe Feldhöhe nach oben), oder `centered` aus und `offset = Vector2(-8, -16)`. Der erste Weg passt, solange das Feld gerade Maße hat. Bei ungerader Breite liegt die Mitte zwischen zwei Pixeln, die Doku warnt dann vor verzerrten Sprites ([centered](https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-centered)). Gemessen mit den Einstellungen des Testgeländes (Snap an): Ein 15 × 15 großes Muster zentriert bei (40, 40) erscheint unverzerrt ab Pixel (33, 33), der Ursprung liegt also einen halben Pixel neben der Mitte. `flip_h` spiegelt das Bild im selben Rechteck. Liegt die Fußmarke nicht mittig im Feld, springt die Figur beim Spiegeln zur Seite. + + ## Aus Aseprite und Krita exportieren + + `hframes` und `vframes` funktionieren nur mit einem gleichmäßigen Raster ohne Abstände: jedes Feld gleich groß, keine Lücke zwischen den Feldern, kein Rand. Die Aseprite-Doku beschreibt die Optionen an der Kommandozeile ([CLI](https://www.aseprite.org/docs/cli/)); im Dialog *File → Export Sprite Sheet* stehen sie unter ähnlichen Namen. + + | Option in Aseprite | Wirkung | Für `hframes` | + | --- | --- | --- | + | `--sheet-type` horizontal, rows | Felder in einer Zeile oder in Zeilen | passt | + | `--sheet-pack` | dichte Packung ohne Raster | passt nicht | + | `--trim` | schneidet leere Ränder je Bild ab | zerstört das Raster | + | `--shape-padding`, `--border-padding` | Abstand zwischen Feldern, Rand ums Blatt | passt nicht | + | `--ignore-empty`, `--merge-duplicates` | lässt Bilder weg | verschiebt die Nummern | + | `--scale` | vergrößert beim Export | lass es bei 1 | + + Trimmen ([trim](https://www.aseprite.org/docs/cli/#trim)) macht aus gleich großen Feldern verschieden große. Dann teilt `hframes` an den falschen Stellen, und die Figur springt von Frame zu Frame. Exportiere in der Größe 1:1; vergrößert wird erst im Spiel. + + Krita rendert Animationen über *File → Render Animation* als Bildfolge mit einer PNG je Frame ([Export as Image Sequence](https://docs.krita.org/en/reference_manual/render_animation.html#export-as-image-sequence)). Die Option *Only Unique Frames* lässt dabei gehaltene Bilder weg, dann fehlt das Timing. Einzelbilder ziehst du direkt in ein SpriteFrames, ohne Sheet. Für ein `Sprite2D` mit `hframes` brauchst du ein Blatt. + + Die Quelldateien (`.aseprite`, `.kra`) bewahrst du auf, denn aus ihnen exportierst du neu. Legst du sie in einen Ordner mit einer leeren Datei `.gdignore`, importiert Godot nichts daraus und blendet den Ordner im Dateisystem aus ([Ignoring specific folders](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html#ignoring-specific-folders)). Die Alternative ist ein Ordner neben dem Projekt. Wie du sie in Git sicherst, steht im Modul zu Export und Sicherung. + + > [!PRAXIS] + > Ein Coding-Agent ändert gern Import-Einstellungen nebenbei, etwa „Mipmaps an für bessere Qualität“. Lies deshalb bei jedem Commit den Diff der `.import`-Dateien. Eine geänderte Zeile `compress/mode=2` oder `process/size_limit` fällt dort sofort auf, im Spiel erst, wenn jemand genau hinsieht. + + ## Der Testbogen: ein Prüfmuster statt einer Figur + + Bevor deine eigenen Figuren ins Projekt kommen, prüfst du die Kette mit einem Bild, bei dem jeder Fehler auffällt. Der Testbogen dieses Kurses ist 64 × 16 Pixel groß, vier Felder zu 16 × 16: + + - eine graue Rasterlinie am Rand jedes Felds: Fehlt sie oder ist sie doppelt, stimmt `hframes` nicht, + - die Nummer 1 bis 4: zeigt, welches Feld gerade läuft, + - ein Block, der von Feld zu Feld weiterrückt: macht die Animation sichtbar, + - eine rote Fußmarke von 2 Pixeln unten in der Mitte: muss genau am Ursprung des Nodes stehen. + + So einen Bogen legst du in Aseprite oder Krita in zwei Minuten an: neues Bild 64 × 16, Raster auf 16 × 16, Linien und Ziffern mit dem Stift. Er ist ein Messwerkzeug wie ein Testbild im Fernsehen, keine Grafik für dein Spiel. Leg ihn als `res://test_assets/test_sheet.png` ab. Im Testgelände bekommt der Player ein `Sprite2D` namens `Sprite` mit diesem Bogen; den Platzhalter `Body` schaltest du unsichtbar, statt ihn zu löschen. In [Animationen zuordnen](/godot/animationen) wird daraus ein `AnimatedSprite2D`. + + > [!INTERVIEW] + > „Die Grafik sieht im Spiel anders aus als in Aseprite. Wie findest du die Ursache?“ Gute Antwort, etwa für eine Stelle als Technical Artist: Ich prüfe die Kette von hinten. Zuerst das Import-Dock und den Diff der `.import`-Datei: Lossless, keine Größenbeschränkung, keine Mipmaps. Dann den Filter im Projekt und am Node. Dann `hframes`, `vframes` und `offset` am Sprite. Zum Schluss Stretch Mode und Kamera. Statt der Figur nehme ich ein Prüfmuster, damit ich sehe, ob ein Pixel fehlt. + + ## Häufige Fehler + + - **`.import` nicht versioniert.** Auf einem anderen Rechner importiert Godot mit Standardwerten. `.png` und `.png.import` gehören immer zusammen in den Commit. + - **Import geändert, aber nicht neu importiert.** Ohne Klick auf *Neuimport* bleibt das alte Ergebnis. + - **VRAM Compressed oder Größenbeschränkung bei Pixelart.** Farben verschieben sich, Kanten verschwimmen. Zurück auf Lossless und 0. + - **Filter am Node statt im Projekt.** Ein Sprite mit `texture_filter = 2` ist Linear, auch wenn das Projekt auf Nearest steht. + - **`hframes` falsch.** Halbe oder doppelte Felder. Breite des Blatts durch Feldbreite rechnen. + - **Getrimmt oder mit Abstand exportiert.** Das Raster stimmt nicht mehr, die Figur springt. + - **Ursprung in der Mitte.** Y-Sortierung sortiert nach der Mitte des Bildes; die Figur steht halb hinter Dingen, vor denen sie steht. + + ## Zusammenfassung + + - Quelle, `.import` und `.godot/imported`: Die ersten beiden gehören ins Repository, den Ordner `.godot/` erzeugt Godot selbst. + - Die Standardwerte im Import-Dock passen für Pixelart: Lossless, keine Mipmaps, Größenbeschränkung 0. Änderungen gelten erst nach *Neuimport*. + - Der Filter ist seit Godot 4 eine Zeicheneinstellung: im Projekt `default_texture_filter=0` für Nearest, am Node `texture_filter`, wo 1 Nearest und 2 Linear heißt. + - `hframes` und `vframes` teilen ein Blatt in gleich große Felder; `frame` zählt ab 0 zeilenweise. + - Region und `AtlasTexture` schneiden Rechtecke aus einer größeren Textur. + - Für die Draufsicht liegt der Ursprung an den Füßen: `offset` um die halbe Feldhöhe nach oben. + - Exportiere 1:1, ohne Trim, ohne Abstand. Quelldateien bleiben erhalten, aber außerhalb des Imports. +exercises: + - id: was-versionieren + type: choice + title: Was gehört in den Commit? + prompt: | + Du hast `test_assets/test_sheet.png` angelegt und importiert. Welche Dateien gehören ins Git-Repository? Mehrere Antworten richtig. + options: + - text: "`test_assets/test_sheet.png`" + correct: true + why: Die Quelle ist das, was du gezeichnet hast. Ohne sie gibt es nichts zu importieren. + - text: "`test_assets/test_sheet.png.import`" + correct: true + why: Sie hält die Import-Einstellungen. Fehlt sie, importiert jeder andere Rechner mit Standardwerten. + - text: "`.godot/imported/test_sheet.png-….ctex`" + correct: false + why: Das ist das Ergebnis des Imports. Godot erzeugt es neu, sobald es fehlt; im Repository kostet es nur Platz. + - text: "`.godot/imported/test_sheet.png-….md5`" + correct: false + why: Die Prüfsumme gehört zum Cache in `.godot/` und entsteht ebenfalls beim Import. + - text: Die Quelldatei `test_sheet.aseprite` in einem Ordner mit `.gdignore` + correct: true + why: Aus ihr exportierst du neu, wenn sich etwas ändert. `.gdignore` hält sie aus dem Import heraus. + explanation: | + Ins Repository gehört alles, was sich nicht aus anderen Dateien erzeugen lässt: deine Quellen und die Einstellungen in `.import`. Der Ordner `.godot/` ist ein Cache, Godot baut ihn aus Quelle und Einstellungen neu. Das ist dasselbe Muster wie `src/` und `vite.config.ts` im Repository, `dist/` und `node_modules/` nicht. Wie du große Binärdateien sicherst, klärt das Modul zu Export und Sicherung. + resources: + - title: Godot Docs · Import process · Files generated + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html#files-generated + + - id: vom-export-zum-sprite + type: order + title: Vom Zeichenprogramm ins Spiel + prompt: | + Bring die Schritte in eine sinnvolle Reihenfolge, damit der Testbogen als Sprite mit Ursprung an den Füßen im Spiel steht. + lang: prose + lines: | + In Aseprite als Sheet in einer Zeile exportieren, Maßstab 1, ohne Trim und ohne Abstand + Die PNG nach res://test_assets/ kopieren + Im Import-Dock prüfen: Lossless, Mipmaps aus, Größenbeschränkung 0 + Ein Sprite2D unter dem Player anlegen und die Textur zuweisen + hframes auf 4 stellen + offset auf (0, −8) setzen, damit die Fußmarke am Ursprung liegt + Spiel starten und prüfen, ob Feld 1 scharf und vollständig erscheint + alternatives: + - [1, 2, 3, 4, 6, 5, 7] + explanation: | + Der Export bestimmt das Raster, deshalb kommt er zuerst und ohne Trim. Danach prüfst du den Import, bevor du die Textur benutzt; sonst beurteilst du ein Bild, das später anders aussieht. `hframes` und `offset` sind voneinander unabhängig. Beide verlangen aber, dass die Textur schon zugewiesen ist. Zum Schluss steht der Test im Spiel, nicht im Editor, denn Stretch Mode und Filter wirken erst dort. + resources: + - title: Godot Docs · Importing images + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html + + - id: rechteck-bei-acht + type: output + title: Wie groß ist ein Feld? + prompt: | + `test_sheet.png` ist 64 × 16 Pixel groß, das Sprite hat `centered` an und `offset` (0, 0). Was gibt die letzte Zeile in Godot 4.7.2 aus? + lang: gdscript + code: | + var sprite := Sprite2D.new() + sprite.texture = load("res://test_assets/test_sheet.png") + sprite.hframes = 8 + print(sprite.get_rect()) + expected: | + [P: (-4.0, -8.0), S: (8.0, 16.0)] + hints: + - Die Feldbreite ist die Breite der Textur geteilt durch `hframes`. + - Mit `centered` liegt die Mitte des Felds am Ursprung. + explanation: | + 64 / 8 = 8 Pixel Breite, die Höhe bleibt 16. Zentriert reicht das Rechteck von −4 bis 4 und von −8 bis 8. Ein Feld ist damit halb so breit wie die gezeichneten 16 Pixel: Jedes Frame zeigt eine Hälfte einer Zelle. `get_rect()` ist der schnellste Weg, das ohne Spielstart nachzuprüfen. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Sprite2D.get_rect + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-method-get-rect + + - id: frame-koordinaten + type: output + title: Welche Zelle ist Frame 5? + prompt: | + `test_sheet_4x2.png` hat vier Spalten und zwei Zeilen. Was gibt der Code aus? + lang: gdscript + code: | + var sprite := Sprite2D.new() + sprite.texture = load("res://test_assets/test_sheet_4x2.png") + sprite.hframes = 4 + sprite.vframes = 2 + sprite.frame = 5 + print(sprite.frame_coords) + expected: | + (1, 1) + accept: + - (1,1) + hints: + - "`frame` zählt ab 0 von links oben, eine Zeile nach der anderen." + explanation: | + Frame 0 bis 3 sind die erste Zeile, Frame 4 bis 7 die zweite. Frame 5 ist das zweite Feld der zweiten Zeile, also Spalte 1, Zeile 1, beide ab 0 gezählt. Auf dem Testbogen trägt es die Nummer 6. Umgekehrt ergibt `frame_coords = Vector2i(2, 1)` den Frame 6. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Sprite2D.frame_coords + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-frame-coords + + - id: ursprung-rechteck + type: output + title: Wo liegen die Füße? + prompt: | + Testbogen mit 16 × 16-Feldern, `hframes = 4`. Was gibt die letzte Zeile aus? + lang: gdscript + code: | + sprite.hframes = 4 + sprite.centered = true + sprite.offset = Vector2(0, -8) + print(sprite.get_rect()) + expected: | + [P: (-8.0, -16.0), S: (16.0, 16.0)] + hints: + - Ohne `offset` reicht das Feld von −8 bis 8. Was macht eine Verschiebung um −8 in y? + explanation: | + Zentriert reicht das Feld senkrecht von −8 bis 8. Der `offset` schiebt es um 8 nach oben, auf −16 bis 0. Die Unterkante liegt also am Ursprung, dort, wo die Fußmarke sitzen soll. `centered = false` mit `offset = Vector2(-8, -16)` ergibt dasselbe Rechteck, gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Sprite2D.offset + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-offset + + - id: neu-importieren + type: command + title: Alles neu importieren, ohne Editor + prompt: | + Ein Coding-Agent hat mehrere PNG-Dateien ins Projekt kopiert. Lass Godot alle Ressourcen importieren und danach beenden, ohne ein Fenster zu öffnen. Das Projekt liegt in `~/projekte/testgelaende`, du stehst in deinem Home-Verzeichnis. `godot` ist im Pfad. + answers: + - godot --headless --path ~/projekte/testgelaende --import + - godot --headless --import --path ~/projekte/testgelaende + - godot --path ~/projekte/testgelaende --headless --import + - godot --path ~/projekte/testgelaende --import --headless + - godot --import --headless --path ~/projekte/testgelaende + - godot --import --path ~/projekte/testgelaende --headless + - godot --headless --path ~/projekte/testgelaende/ --import + - godot --headless --import --path ~/projekte/testgelaende/ + - godot --path ~/projekte/testgelaende/ --headless --import + - godot --path ~/projekte/testgelaende/ --import --headless + - godot --import --headless --path ~/projekte/testgelaende/ + - godot --import --path ~/projekte/testgelaende/ --headless + - godot --headless --path projekte/testgelaende --import + - godot --headless --import --path projekte/testgelaende + - godot --path projekte/testgelaende --headless --import + - godot --path projekte/testgelaende --import --headless + - godot --import --headless --path projekte/testgelaende + - godot --import --path projekte/testgelaende --headless + - godot --headless --path projekte/testgelaende/ --import + - godot --headless --import --path projekte/testgelaende/ + - godot --path projekte/testgelaende/ --headless --import + - godot --path projekte/testgelaende/ --import --headless + - godot --import --headless --path projekte/testgelaende/ + - godot --import --path projekte/testgelaende/ --headless + - godot --headless --path ./projekte/testgelaende --import + - godot --headless --import --path ./projekte/testgelaende + - godot --path ./projekte/testgelaende --headless --import + - godot --path ./projekte/testgelaende --import --headless + - godot --import --headless --path ./projekte/testgelaende + - godot --import --path ./projekte/testgelaende --headless + - "godot --headless --path $HOME/projekte/testgelaende --import" + - "godot --headless --import --path $HOME/projekte/testgelaende" + - "godot --path $HOME/projekte/testgelaende --headless --import" + - "godot --path $HOME/projekte/testgelaende --import --headless" + - "godot --import --headless --path $HOME/projekte/testgelaende" + - "godot --import --path $HOME/projekte/testgelaende --headless" + output: | + Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org + … + [ 0% ] reimport | Started Assets (erneut) importieren (12 steps) + … + [ 76% ] reimport | test_sheet.png + [ 84% ] reimport | test_sheet_4x2.png + [ 92% ] reimport | Asset-Import fertigstellen … + [ DONE ] reimport + explanation: | + `--import` startet den Editor, wartet, bis alle Ressourcen importiert sind, und beendet ihn. `--headless` verhindert das Fenster, `--path` nennt den Projektordner. Danach liegen die `.import`-Dateien neben den PNGs und die Ergebnisse in `.godot/imported`. Die Ausgabe ist gekürzt und stammt aus einem Lauf mit Godot 4.7.2 und deutscher Systemsprache. + resources: + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#command-line-reference + + - id: sprite-zeilen + type: gap + title: Feld drei mit Ursprung an den Füßen + prompt: | + Ergänze den Node in `player.tscn`: Der Testbogen hat vier Felder mit 16 × 16 Pixeln in einer Zeile. `centered` bleibt an, der Ursprung soll an der Unterkante liegen, und das Sprite zeigt das Feld mit der Nummer 3. + lang: ini + code: | + [node name="Sprite" type="Sprite2D" parent="."] + texture = ExtResource("3_i3pqv") + offset = Vector2(0, ⟦-8⟧) + ⟦hframes⟧ = 4 + frame = ⟦2⟧ + gaps: + - label: Verschiebung in y + hint: Die halbe Feldhöhe, nach oben ist negativ. + accept: ["-8.0"] + - label: Eigenschaft für die Spalten + hint: Horizontale Frames. + - label: Frame-Index + hint: Gezählt wird ab 0. + explanation: | + `offset` verschiebt das Bild um die halbe Feldhöhe nach oben, damit die Unterkante am Ursprung liegt. `hframes = 4` teilt die 64 Pixel in vier Felder. Das Feld mit der Nummer 3 hat den Index 2, weil `frame` bei 0 beginnt. `centered` steht nicht in der Datei, denn Godot schreibt nur Werte, die vom Standard abweichen. + resources: + - title: Godot Docs · Sprite2D + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html + + - id: groessenbeschraenkung + type: bug + title: Halb so groß und grau + prompt: | + Nach einem Commit des Coding-Agents ist das Prüfmuster im Spiel nur noch halb so groß, und das Schachbrett ist eine graue Fläche. An Szene und Projekteinstellungen hat sich nichts geändert. Hier ist der Ausschnitt aus `pattern.png.import`. Finde die Zeile und korrigiere sie. + lang: ini + code: | + [params] + + compress/mode=0 + mipmaps/generate=false + process/fix_alpha_border=true + process/premult_alpha=false + process/size_limit=16 + detect_3d/compress_to=1 + lines: [7] + fix: + - line: 7 + answers: + - process/size_limit=0 + explanation: | + Die Größenbeschränkung verkleinert die Textur beim Import auf höchstens 16 Pixel an der längeren Seite, mit kubischer Interpolation. Aus dem 32 × 32 großen Muster wird ein 16 × 16 großes mit 32 statt 4 Farben, gemessen mit Godot 4.7.2. Mit 0 ist die Beschränkung aus. Nach der Korrektur musst du im Import-Dock *Neuimport* klicken oder Godot neu starten, damit das Ergebnis in `.godot/imported` ersetzt wird. + resources: + - title: Godot Docs · Importing images · Size Limit + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html#process-size-limit + + - id: halbe-felder + type: bug + title: Die Animation zeigt halbe Zellen + prompt: | + So sehen die ersten vier Frames des Testbogens im Spiel aus. Die Textur ist 64 × 16 Pixel groß und hat vier Felder. Finde die Zeile im Szenenausschnitt und korrigiere sie. + + ![Vier Frames nebeneinander: jedes zeigt nur eine linke oder rechte Hälfte einer Zelle mit halber Ziffer](/media/godot/sprites-importieren/diagnose-halbe-felder@2x.png "Godot 4.7.2, Frames 0 bis 3, Spielbild sechsfach vergrößert") + lang: ini + code: | + [node name="Sprite" type="Sprite2D" parent="."] + texture = ExtResource("3_i3pqv") + offset = Vector2(0, -8) + hframes = 8 + lines: [4] + fix: + - line: 4 + answers: + - hframes = 4 + explanation: | + Mit `hframes = 8` teilt Godot die 64 Pixel in acht Felder zu 8 Pixeln. Frame 0 zeigt die linke Hälfte der ersten Zelle, Frame 1 die rechte, und so weiter. An der Rasterlinie des Testbogens erkennst du das sofort: Jedes Bild hat nur eine senkrechte Kante. `hframes` muss Breite des Blatts durch Feldbreite sein, hier 64 / 16 = 4. + resources: + - title: Godot Docs · Sprite2D.hframes + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-hframes + + - id: ein-sprite-weich + type: bug + title: Nur eine Figur ist weich + prompt: | + Ein Projekt im Stretch Mode `canvas_items` hat `textures/canvas_textures/default_texture_filter=0` in `project.godot`. Alle Sprites sind scharf, nur der Testbogen am Player ist verwaschen. Hier ist sein Node in `player.tscn`. Finde die Zeile und korrigiere sie so, dass das Sprite dem Projekt folgt oder ausdrücklich Nearest nutzt. + lang: ini + code: | + [node name="Sprite" type="Sprite2D" parent="."] + texture_filter = 2 + texture = ExtResource("3_i3pqv") + offset = Vector2(0, -8) + hframes = 4 + lines: [2] + fix: + - line: 2 + answers: + - texture_filter = 1 + - texture_filter = 0 + explanation: | + Am Node bedeutet `texture_filter = 2` Linear, auch wenn im Projekt eine 0 steht, die dort Nearest heißt. Die beiden Einstellungen zählen verschieden: Am Node ist 0 Inherit und 1 Nearest. Mit 0 übernimmt das Sprite den Projektwert, mit 1 erzwingt es Nearest. Godot schreibt Inherit gar nicht in die Datei; die Zeile zu löschen wirkt deshalb wie 0. Im Modus `canvas_items` vergrößert jedes Sprite selbst, deshalb fällt der Filter dort sofort auf. + resources: + - title: Godot Docs · CanvasItem.texture_filter + url: https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-texture-filter + + - id: import-datei-erklaeren + type: explain + title: Was ist diese Datei? + prompt: | + Ein Kollege fragt, warum neben jeder PNG eine solche Datei liegt und ob man sie nicht in `.gitignore` aufnehmen könnte. Erkläre in wenigen Sätzen, was sie ist, was beim Ändern der PNG passiert und was gegen das Ignorieren spricht. + lang: ini + code: | + [remap] + + importer="texture" + type="CompressedTexture2D" + uid="uid://bnbb5aa4yerph" + path="res://.godot/imported/test_sheet.png-142e95a07cea5c3d31f4022e607774aa.ctex" + + [deps] + + source_file="res://test_assets/test_sheet.png" + + [params] + + compress/mode=0 + mipmaps/generate=false + process/size_limit=0 + points: + - Die Datei hält die Import-Einstellungen für genau diese PNG und sagt, wo das Ergebnis liegt. + - Das Ergebnis (`.ctex`) liegt im Cache `.godot/imported` und wird bei jeder Änderung der PNG automatisch neu erzeugt. + - Ohne die Datei importiert jeder andere Rechner mit Standardwerten; abweichende Einstellungen wie ein anderer Kompressionsmodus gingen verloren. + - Die `uid` ist eine feste Kennung der Ressource; Szenen nennen sie in `ext_resource` neben dem Pfad. + explanation: | + Die `.import`-Datei ist die Konfiguration des Imports für eine Datei, vergleichbar mit einer Loader-Regel im Bundler, nur direkt neben der Quelle. Unter `[params]` stehen die Werte aus dem Import-Dock, unter `[remap]` der Pfad zum Ergebnis im Cache. Ändert sich die PNG, erkennt Godot das an der Prüfsumme und importiert neu. Die `uid` ist eine feste Kennung der Ressource: `player.tscn` nennt beim Testbogen `uid="uid://bnbb5aa4yerph"` neben dem Pfad. Die Doku verlangt, `.import`-Dateien zu versionieren; ignoriert wird nur `.godot/`. Sonst sieht dieselbe Grafik auf jedem Rechner anders aus, je nachdem, welche Standards dort gelten. + resources: + - title: Godot Docs · Import process + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html + + - id: testbogen-einsetzen + type: practice + title: Den Testbogen anlegen und einsetzen + prompt: | + Leg in Aseprite oder Krita den Testbogen an: 64 × 16 Pixel, vier Felder zu 16 × 16 mit grauer Rasterlinie am Feldrand, Nummer 1 bis 4, einem Block, der von Feld zu Feld weiterrückt, und einer roten Fußmarke von 2 Pixeln unten in der Mitte. Exportiere ihn nach `res://test_assets/test_sheet.png`. Setz ihn im Testgelände als `Sprite2D` namens `Sprite` unter den Player, schalte `Body` unsichtbar und richte `hframes` und `offset` ein. Prüfe das Ergebnis im laufenden Spiel und im Diff von `player.tscn`. + lang: ini + solution: | + ; player.tscn nach dem Speichern in Godot 4.7.2 (Auszug). + ; uid und unique_id vergibt Godot, sie weichen bei dir ab. + + [ext_resource type="Texture2D" uid="uid://bnbb5aa4yerph" path="res://test_assets/test_sheet.png" id="3_i3pqv"] + + [node name="Body" type="Polygon2D" parent="." unique_id=1710148549] + visible = false + color = Color(0.35, 0.65, 0.95, 1) + polygon = PackedVector2Array(-6, -16, 6, -16, 6, 0, -6, 0) + + [node name="Sprite" type="Sprite2D" parent="." unique_id=1152945] + texture = ExtResource("3_i3pqv") + offset = Vector2(0, -8) + hframes = 4 + checklist: + - Der Testbogen liegt als `res://test_assets/test_sheet.png` im Projekt, die `.import`-Datei daneben zeigt `compress/mode=0` und `process/size_limit=0`. + - Das Import-Dock zeigt Lossless, Mipmaps aus, Größenbeschränkung 0. + - Im Spiel ist Feld 1 vollständig zu sehen, mit allen vier Rasterlinien. + - Die rote Fußmarke steht auf derselben Höhe wie die Unterkante der Form `Feet` (sichtbar mit *Debuggen → Sichtbare Collision-Shapes*). + - Mit `frame` 0 bis 3 im Inspektor erscheinen die Nummern 1 bis 4. + - Im Git-Diff stehen die PNG, ihre `.import`-Datei und die Änderung an `player.tscn`, nichts aus `.godot/`. + explanation: | + Der Testbogen prüft die ganze Kette auf einmal: Import (scharf, richtige Größe), Raster (`hframes`), Ursprung (Fußmarke an der Unterkante der Fußform) und Versionierung. Beim ersten Speichern im Editor ergänzt Godot 4.7.2 `uid` und `unique_id` und streicht Zeilen mit Standardwerten wie `collision_mask = 1`; erschrick nicht über diesen Diff. `Body` bleibt als Node erhalten, weil andere Module des Kurses mit den Namen der Vorlage arbeiten. Die Zahlen sind die Platzhalter des Testbogens. Wenn deine Figuren andere Feldgrößen haben, ändern sich `hframes` und `offset`, die Prüfung bleibt dieselbe. + resources: + - title: Godot Docs · Sprite2D + url: https://docs.godotengine.org/en/stable/classes/class_sprite2d.html + - title: Godot Docs · Overview of debugging tools · Visible collision shapes + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html#visible-collision-shapes + + - id: eigene-exportregeln + type: practice + title: Deine Exportregeln für Sprites + prompt: | + Schreib eine kurze Exportregel für deine eigenen Sprites, so dass du oder ein Coding-Agent sie später ohne Rückfrage befolgen kann. Lege fest, welche Feldgröße du als Platzhalter nimmst, wie das Blatt aufgebaut ist, wo Quelldateien liegen, wie Dateien heißen und woran du nach dem Import prüfst, ob alles stimmt. Die Werte entscheidest du. + lang: prose + checklist: + - Die Regel nennt eine Feldgröße in Pixeln, ausdrücklich als Platzhalter. + - Sie legt Layout (Zeile oder Zeilen), Maßstab beim Export und den Verzicht auf Trim und Abstand fest. + - Sie nennt den Ort der Quelldateien (`.aseprite`, `.kra`), mit `.gdignore` oder außerhalb des Projekts. + - Sie enthält eine Namensregel für Dateien, die ein Coding-Agent ohne Rückfrage anwenden kann. + - Sie nennt mindestens zwei Prüfungen nach dem Import, etwa Werte im Import-Dock und die Lage der Fußmarke. + hints: + - Nimm die Tabelle mit den Aseprite-Optionen aus der Lektion als Gerüst. + - Eine Prüfung ist gut, wenn jemand anderes sie mit Ja oder Nein beantworten kann. + explanation: | + Eine Exportregel macht aus deinen Entscheidungen etwas, das andere ausführen können: ein Coding-Agent, ein Mitstreiter oder du in drei Monaten. Die technischen Teile (kein Trim, kein Abstand, Maßstab 1, Lossless) folgen aus dem, was `hframes` und der Import brauchen. Feldgröße, Aufbau und Namen sind deine Wahl. Schreib die Regel ins Repository, etwa neben die Quelldateien. + resources: + - title: Aseprite Docs · CLI · sheet-type + url: https://www.aseprite.org/docs/cli/#sheet-type + - title: Godot Docs · Project organization + url: https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html diff --git a/content/godot/modules/szenenbaum.yaml b/content/godot/modules/szenenbaum.yaml new file mode 100644 index 0000000..0f9e3e3 --- /dev/null +++ b/content/godot/modules/szenenbaum.yaml @@ -0,0 +1,675 @@ +id: szenenbaum +title: "Szenenbäume lesen: Nodes, Transform und Zeichenreihenfolge" +level: 1 +minutes: 110 +summary: Wer wessen Kind ist, entscheidet in Godot über Position, Zeichenreihenfolge, Pfade im Code und die Reihenfolge, in der Code läuft. Du liest Bäume im Szenen-Dock und als .tscn-Text und sagst vorher, was Godot daraus macht. +place: unterwegs +goals: + - Du liest einen Szenenbaum im Szenen-Dock und erkennst Instanzen, Skripte und Verbindungen an ihren Symbolen. + - Du rechnest `position` und `global_position` über Eltern mit Verschiebung, Skalierung und Drehung aus. + - Du erklärst, in welcher Reihenfolge Godot zeichnet, und setzt Baumreihenfolge, `z_index`, Y-Sortierung und `CanvasLayer` gezielt ein. + - Du schreibst Node-Pfade mit `$`, `get_node()`, `..` und `%` und sagst vorher, was beim Umbenennen oder Verschieben bricht. + - Du rekonstruierst einen Baum samt Instanzen und Verbindungen aus einer `.tscn`-Datei. +resources: + - title: Godot Docs · Nodes and Scenes + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/nodes_and_scenes.html + - title: Godot Docs · CanvasItem + url: https://docs.godotengine.org/en/stable/classes/class_canvasitem.html + - title: Godot Docs · TSCN file format + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html + - title: Godot Docs · Scene Unique Nodes + url: https://docs.godotengine.org/en/stable/tutorials/scripting/scene_unique_nodes.html +lesson: | + Schaltet jemand im Testgelände die Y-Sortierung aus, steht die Figur sichtbar vor der Kiste, mit den Füßen unterhalb ihrer Kante, und trotzdem verdeckt die Kiste ihren Oberkörper. An einem anderen Tag hat ein Agent den Zähler in einen Container verschoben, und seitdem meldet das Spiel beim Start `Node not found: "Counter"`. Beide Fehler stecken nicht in einer Rechnung, sondern in der Struktur: wer wessen Kind ist, in welcher Reihenfolge die Nodes stehen und welcher Pfad zu welchem Node führt. + + Diese Lektion bringt dir bei, Szenenbäume zu lesen, im Szenen-Dock und als Text in der `.tscn`. Grundbegriffe kennst du aus [Godot-Start](/combat/godot-start), die Docks aus [Der Editor als Werkstatt](/godot/editor-anatomie). Alle Ausgaben stammen aus Läufen mit Godot 4.7.2. + + ## Nodes, Bäume und Szenen + + Ein **Node** ist ein Baustein mit einer Aufgabe: `Polygon2D` zeichnet eine Fläche, `CollisionShape2D` gibt einem Körper eine Form, `Camera2D` bestimmt den Bildausschnitt, `Label` zeigt Text. Nodes hängen in einem Baum. Jeder Node hat höchstens einen Elternknoten (parent) und beliebig viele Kinder (children). Der oberste Node einer Szene ist ihre Wurzel (root). Eine **Szene** ist ein gespeicherter Baum, und jede `.tscn` hat genau eine Wurzel ([TSCN file format](https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#the-scene-tree)). Szenen lassen sich in andere einsetzen: `main.tscn` setzt `player.tscn` einmal ein und `pickup.tscn` dreimal ([Nodes and Scenes](https://docs.godotengine.org/en/stable/getting_started/step_by_step/nodes_and_scenes.html)). + + ![Szenen-Dock von main.tscn: Main mit Floor, Objects (darin Player, Crate, Pickup, Pickup2, Pickup3) und HUD mit Counter; rechts Symbole für Verbindung, Instanz, Skript und Sichtbarkeit](/media/godot/szenenbaum/szene-main@2x.png "Godot 4.7.2, Szenen-Dock von main.tscn im Testgelände") + + Die Symbole rechts verraten, was du nicht siehst. Ihre Tooltips in 4.7.2: + + - **Filmklappe** – „Im Editor öffnen“: Der Node ist eine Instanz. Der Tooltip des Namens nennt die Szene, etwa „Instanz: res://player.tscn“. + - **Schriftrolle** – „Skript öffnen: res://player.gd“: Am Node hängt ein Skript. + - **Wellen** – „Node hat eine Verbindung.“: Ein Signal des Nodes ist verbunden. Ein Klick öffnet das Signale-Dock. + - **Auge** – „Sichtbarkeit ein-/ausschalten“. + + `Crate` hat nur Klappe und Auge: eine Instanz ohne Skript und ohne Verbindung. Im Editor ist eine Instanz eine einzige Zeile. Zur Laufzeit hängt dort der ganze Baum der eingesetzten Szene. `print_tree_pretty()` zeigt das ([Node.print_tree_pretty](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-print-tree-pretty)): + + ```gdscript + var main: Node = load("res://main.tscn").instantiate() + root.add_child(main) + main.print_tree_pretty() + ``` + + ```text + ┖╴Main + ┠╴Floor + ┠╴Objects + ┃ ┠╴Player + ┃ ┃ ┠╴Body + ┃ ┃ ┠╴Feet + ┃ ┃ ┖╴Camera2D + ┃ ┠╴Crate + ┃ ┃ ┠╴Body + ┃ ┃ ┖╴Base + ┃ ┠╴Pickup + ┃ ┃ ┠╴Body + ┃ ┃ ┖╴Area + ┃ ┠╴Pickup2 + ┃ ┃ ┠╴Body + ┃ ┃ ┖╴Area + ┃ ┖╴Pickup3 + ┃ ┠╴Body + ┃ ┖╴Area + ┖╴HUD + ┖╴Counter + ``` + + Aus dem Web kennst du das Bild: Der Szenenbaum ähnelt dem DOM, eine Szene einer Komponente, die du an mehreren Stellen einsetzt. Der Vergleich bricht an zwei Stellen. Nodes sind Objekte mit Verhalten, kein Markup. Und die Reihenfolge im Baum bestimmt neben dem Zeichnen auch, wann ihr Code läuft. + + ## Transform: Position relativ zum Elternknoten + + `position` eines `Node2D` gilt relativ zum Elternknoten, `global_position` in der Welt ([Node2D](https://docs.godotengine.org/en/stable/classes/class_node2d.html#class-node2d-property-global-position)). Verschiebung, Skalierung und Drehung des Elternknotens wirken auf alle Kinder ([2D transforms](https://docs.godotengine.org/en/stable/tutorials/2d/2d_transforms.html)). Dieses Skript läuft in `_ready()` eines `Node2D` am Ursprung: + + ```gdscript + func _ready() -> void: + var a := Node2D.new() + a.position = Vector2(100, 50) + var b := Node2D.new() + b.position = Vector2(20, 0) + a.add_child(b) + add_child(a) + print(b.global_position) + a.scale = Vector2(2, 2) + print(b.global_position) + a.scale = Vector2.ONE + a.rotation_degrees = 90 + print(b.global_position) + ``` + + ```text + (120.0, 50.0) + (140.0, 50.0) + (100.0, 70.0) + ``` + + Der Versatz von `b` ist 20 Pixel nach rechts. Mit doppelter Skalierung des Elternknotens werden daraus 40, mit einer Drehung um 90° zeigt er nach unten, denn die y-Achse zeigt in Godot nach unten. `b.position` bleibt dabei die ganze Zeit `(20, 0)`. Setzt du umgekehrt `global_position`, rechnet Godot die passende `position` aus. + + Im Testgelände sind `Body`, `Feet` und `Camera2D` Kinder von `Player`. Ihre Koordinaten gelten relativ zur Figur: Der Körper reicht von y = −16 bis 0, die Fußform sitzt bei `(0, -3)`. Der Ursprung der Figur liegt also an ihren Füßen. Die Kamera folgt der Figur ohne eine Zeile Code, weil sie ihr Kind ist. Was Skalierung mit Pixelart und mit Kollisionsformen anrichtet, behandeln die Module zum [Pixelraster](/godot/pixelraster) und zu [Kollisionsebenen](/godot/kollision-ebenen). + + ## Zeichenreihenfolge: Baum, `z_index`, Y-Sortierung und `CanvasLayer` + + Godot zeichnet 2D-Nodes in Baumreihenfolge, von oben nach unten im Szenen-Dock, und Kinder über ihren Eltern ([CanvasItem](https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#description)). Ein späteres Geschwister liegt also über einem früheren. `Floor` steht im Baum vor `Objects`, deshalb liegt der Boden unter allem. + + **`z_index`** schiebt einen Node in eine andere Ebene. Höhere Werte liegen vorn, der Standard ist 0, erlaubt sind −4096 bis 4096. Solange `z_as_relative` an ist (Standard), addiert sich der Wert zu dem des Elternknotens. Auf die Reihenfolge, in der Code läuft, hat `z_index` keinen Einfluss ([z_index](https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-z-index)). + + **Y-Sortierung** ist die Regel der Draufsicht. Ist an einem Node `y_sort_enabled` an, zeichnet Godot seine Kinder nach ihrer y-Position: Wer weiter unten im Bild steht, liegt vorn ([y_sort_enabled](https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-y-sort-enabled)). Im Testgelände ist sie an `Objects` eingeschaltet. Verglichen wird der Ursprung jedes Kindes, deshalb gehört er an die Füße. Und sie sortiert nur Nodes mit gleichem `z_index`. + + ![Ausschnitt des Spielbilds: Die blaue Figur steht mit den Füßen unterhalb der braunen Kiste und überdeckt sie](/media/godot/szenenbaum/reihenfolge-richtig@2x.png "Godot 4.7.2, Figur bei y = 18, Kiste bei y = 10, Y-Sortierung an") + + ![Derselbe Ausschnitt: Die Kiste liegt über dem Oberkörper der Figur, nur deren Füße schauen unten heraus](/media/godot/szenenbaum/reihenfolge-falsch@2x.png "Godot 4.7.2, gleiche Positionen, Y-Sortierung an Objects aus") + + Ohne Y-Sortierung gilt wieder die Baumreihenfolge: `Crate` steht nach `Player`, also liegt die Kiste immer oben, egal wo die Figur steht. Bei einem `Sprite2D` liegt der Ursprung standardmäßig in der Bildmitte (`centered` ist an). Wie du ihn an die Füße setzt, zeigt das [Modul zum Sprite-Import](/godot/sprites-importieren); Y-Sortierung mit Tiles behandelt das [Modul zu Tile-Leveln](/godot/level-tilemap). + + Ein **`CanvasLayer`** zeichnet seine Kinder in einer eigenen Ebene. Die Welt liegt auf Ebene 0, das HUD des Testgeländes auf Ebene 1, dem Standard von `layer`. Damit liegt es über allem, unabhängig von jedem `z_index` ([CanvasLayer](https://docs.godotengine.org/en/stable/classes/class_canvaslayer.html#description)). Und es hat seine eigene Transformation: Die Kamera verschiebt die Welt, nicht die Ebene. Solange `follow_viewport_enabled` aus ist (Standard), bleibt der Zähler an seiner Stelle auf dem Bildschirm ([Canvas layers](https://docs.godotengine.org/en/stable/tutorials/2d/canvas_layers.html)). + + ![Spielbild mit HUD als CanvasLayer: Der Zähler 0 steht oben links in der Ecke, die Figur steht in der Bildmitte, weil die Kamera ihr folgt](/media/godot/szenenbaum/hud-canvaslayer@2x.png "Godot 4.7.2, Figur bei (60, 30), Zähler in einem CanvasLayer") + + ![Gleiches Spielbild, aber der Zähler hängt direkt unter Main: Die 0 steht mitten in der Welt neben der Kiste](/media/godot/szenenbaum/hud-node2d@2x.png "Godot 4.7.2, Figur bei (60, 30), Zähler ohne CanvasLayer") + + > [!MERKE] + > Ein `CanvasLayer` mit höherem `layer` liegt über allem und bewegt sich nicht mit der Kamera. Darin entscheidet zuerst `z_index`. Bei gleichem `z_index` entscheidet die Y-Sortierung, wo sie an ist, sonst die Baumreihenfolge. + + ## Node-Pfade: `$`, `get_node()`, `..` und `%` + + Ein Skript findet andere Nodes über Pfade, relativ zu dem Node, an dem es hängt ([Node.get_node](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-get-node)). `$Counter` ist die Kurzform von `get_node("Counter")`, `..` geht eine Ebene nach oben, ein Pfad mit `/` am Anfang beginnt bei der Wurzel des ganzen Spiels. Zum Ausprobieren schreibst du vorübergehend in `player.gd`: + + ```gdscript + func _ready() -> void: + print(get_path()) + print(get_parent().name) + print(get_node("../Crate").position) + print(get_node("../../HUD/Counter").text) + ``` + + ```text + /root/Main/Objects/Player + Objects + (40.0, 10.0) + 0 + ``` + + `hud.gd` holt sich den Zähler mit `@onready var counter: Label = $Counter`. `@onready` wertet den Pfad kurz vor `_ready()` aus, wenn alle Kinder im Baum sind ([GDScript reference, @onready](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#onready-annotation)). + + **Was bricht.** Benennst du `Counter` um oder hängst ihn tiefer, ändert Godot das Skript nicht. Beim Start meldet das Spiel, hier im Terminal gestartet (im Editor steht dieselbe Meldung im Debugger unter *Fehler*): + + ```text + ERROR: Node not found: "Counter" (relative to "/root/Main/HUD"). + at: get_node (scene/main/node.cpp:1975) + GDScript backtrace (most recent call first): + [0] @implicit_ready (res://hud.gd:5) + ``` + + `@implicit_ready` ist die Zeile mit `@onready`. Gegen solche Brüche gibt es **Szenen-eindeutige Namen**: Rechtsklick im Szenen-Dock → *Über einzigartigen Namen zugreifen*. Der Node trägt dann ein `%`, und das Skript schreibt `%Counter`. Mit 4.7.2 geprüft: Hängt `Counter` danach in einem zusätzlichen Container, findet `%Counter` ihn weiter, `$Counter` nicht. Das gilt nur innerhalb derselben Szene ([Scene Unique Nodes](https://docs.godotengine.org/en/stable/tutorials/scripting/scene_unique_nodes.html)). + + Signalverbindungen speichert die Szene ebenfalls als Pfade. Verschiebst oder benennst du Nodes im Szenen-Dock um, schreibt der Editor beim Speichern die neuen Pfade. Ändert jemand die `.tscn` von Hand, passiert das nicht. + + > [!WARNING] Stille Brüche + > Mit 4.7.2 nachgestellt: Steht `Player` nach einer Änderung im Text direkt unter `Main`, die Verbindung aber noch mit `from="Objects/Player"`, lädt die Szene ohne Fehler. Nur die Verbindung fehlt, und der Zähler bleibt bei 0. Prüf nach jedem Umbau im Signale-Dock, ob die Verbindungen noch da sind. + + ## Die `.tscn` lesen + + So speichert der Editor `main.tscn` in 4.7.2: + + ```ini title=main.tscn + [gd_scene format=3 uid="uid://ef8tg5r8yt71"] + + [ext_resource type="PackedScene" uid="uid://titrdvxe5ria" path="res://player.tscn" id="1"] + [ext_resource type="PackedScene" uid="uid://pf85is85vm4h" path="res://crate.tscn" id="2"] + [ext_resource type="PackedScene" uid="uid://cjjikuxf00l3x" path="res://pickup.tscn" id="3"] + [ext_resource type="Script" uid="uid://gox8xexcx3h1" path="res://hud.gd" id="4"] + + [node name="Main" type="Node2D" unique_id=1530763771] + + [node name="Floor" type="ColorRect" parent="." unique_id=1265667333] + offset_left = -200.0 + offset_top = -120.0 + offset_right = 200.0 + offset_bottom = 120.0 + color = Color(0.22, 0.3, 0.22, 1) + + [node name="Objects" type="Node2D" parent="." unique_id=659594344] + y_sort_enabled = true + + [node name="Player" parent="Objects" unique_id=1513722367 instance=ExtResource("1")] + + [node name="Crate" parent="Objects" unique_id=481004414 instance=ExtResource("2")] + position = Vector2(40, 10) + + [node name="Pickup" parent="Objects" unique_id=1187784594 instance=ExtResource("3")] + position = Vector2(-50, -20) + + [node name="Pickup2" parent="Objects" unique_id=969255857 instance=ExtResource("3")] + position = Vector2(-70, 30) + + [node name="Pickup3" parent="Objects" unique_id=1350431156 instance=ExtResource("3")] + position = Vector2(70, 40) + + [node name="HUD" type="CanvasLayer" parent="." unique_id=552369509] + script = ExtResource("4") + + [node name="Counter" type="Label" parent="HUD" unique_id=426207881] + offset_left = 4.0 + offset_top = 2.0 + offset_right = 44.0 + offset_bottom = 18.0 + text = "0" + + [connection signal="collected" from="Objects/Player" to="HUD" method="show_total"] + ``` + + Von oben nach unten ([TSCN file format](https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#file-structure)): + + - **`[gd_scene …]`** ist der Kopf mit Format und UID der Szene. + - **`[ext_resource …]`** listet Dateien, die die Szene benutzt, jede mit einer `id`. Weiter unten verweist `ExtResource("3")` auf `pickup.tscn`. + - **`[sub_resource …]`** wären Ressourcen, die in der Szene selbst stecken, etwa eine Kollisionsform. `main.tscn` hat keine, `player.tscn` hat eine für `Feet`. + - **`[node …]`** ist ein Node in Baumreihenfolge. Die Wurzel hat kein `parent`. `parent="."` heißt Kind der Wurzel, `parent="Objects"` Kind von `Objects`; der Name der Wurzel fehlt in diesen Pfaden. `type` nennt die Klasse, `instance=` macht den Node zur Instanz einer Szene. Darunter stehen nur Eigenschaften, die vom Standard abweichen. + - **`[connection …]`** ist eine Signalverbindung: Signal `collected` von `Objects/Player` an die Methode `show_total` von `HUD`. + + `unique_id` schreibt Godot seit 4.6, um Nodes beim Verschieben und Umbenennen zu verfolgen. Von Hand geschriebene Dateien wie die Vorlagen des Testgeländes haben weder `uid` noch `unique_id`. Beim ersten Speichern ergänzt der Editor beides; das ist ein großer, aber erwartbarer Diff ([Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html#core)). + + > [!PRAXIS] + > Lies eine Szene, die ein Agent gebaut hat, einmal als Text von oben nach unten, bevor du sie im Editor anfasst. In zwei Minuten weißt du, welche Szenen sie einsetzt, was von Standardwerten abweicht und welche Verbindungen sie hat. Bitte den Agenten, Struktur im Editor-Format zu ändern oder dir die geänderten `[node]`- und `[connection]`-Zeilen zu nennen, und prüf danach das Signale-Dock. + + > [!DEEP] Aufrufreihenfolge + > Hängt an `Main`, `Objects`, `Player`, `Crate` und `HUD` dasselbe Skript mit `extends Node`, das in `_enter_tree()`, `_ready()` und im ersten `_process()` seinen Namen ausgibt, zeigt 4.7.2: `_enter_tree` läuft von oben nach unten (Main, Objects, Player, Crate, HUD). `_ready` läuft zuerst bei den Kindern: Player, Crate, Objects, HUD, Main. `_process` läuft wieder in Baumreihenfolge: Main, Objects, Player, Crate, HUD. Die Doku beschreibt genau das ([Node._ready](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-ready), [process_priority](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-property-process-priority)). Ein Eltern-Node kann sich in `_ready()` also darauf verlassen, dass seine Kinder bereit sind. Wer früher drankommen muss, setzt `process_priority`. + + > [!INTERVIEW] + > „In einem Draufsicht-Level wird die Figur hinter einem Baum gezeichnet, obwohl sie davor steht. Wie gehst du vor?“ Gute Antwort: Ich prüfe, ob Figur und Baum Kinder desselben Nodes mit Y-Sortierung sind, ob beide denselben `z_index` haben und wo ihr Ursprung liegt. Y-Sortierung vergleicht den Ursprung, also muss er bei beiden an der Standlinie sitzen. Ohne Y-Sortierung entscheidet die Baumreihenfolge, und dann gewinnt, wer später im Baum steht. + + ## Häufige Fehler + + - **Ursprung nicht an den Füßen.** Y-Sortierung vergleicht die Ursprünge. Liegt er in der Bildmitte oder am Kopf, kippt die Reihenfolge, sobald sich zwei Objekte nahe kommen. + - **Y-Sortierung am falschen Node.** Sie sortiert die Kinder des Nodes, an dem sie an ist. Figuren und Hindernisse müssen Geschwister unter diesem Node sein. + - **`z_index` gesetzt, Y-Sortierung wirkt nicht mehr.** Sortiert wird nur unter gleichem `z_index`. Eine Figur mit `z_index = 1` liegt vor allem mit 0. + - **HUD ohne `CanvasLayer`.** Dann wandert der Zähler mit der Welt und kann von Objekten verdeckt werden. + - **Node umbenannt oder verschoben, Skript nicht angepasst.** `Node not found` beim Start. `%`-Namen halten Umbauten innerhalb einer Szene aus. + - **`.tscn` von Hand umgebaut.** Pfade in `[connection]` passen nicht mehr, die Verbindung fällt ohne Meldung weg. + - **`position` mit `global_position` verwechselt.** Zwei Nodes mit gleicher `position` stehen nur dann am selben Ort, wenn sie denselben Elternknoten haben. + + ## Zusammenfassung + + - Ein Node hat eine Aufgabe, höchstens einen Elternknoten und beliebig viele Kinder. Eine Szene ist ein gespeicherter Baum mit genau einer Wurzel. + - Im Editor ist eine Instanz eine Zeile mit Filmklappe; zur Laufzeit hängt dort der ganze Baum der eingesetzten Szene. + - `position` gilt relativ zum Elternknoten. Verschiebung, Skalierung und Drehung der Eltern wirken auf die Kinder. + - Gezeichnet wird in Baumreihenfolge, Kinder über Eltern. `z_index` legt Ebenen fest, Y-Sortierung ordnet Geschwister nach ihrem Ursprung, ein `CanvasLayer` liegt darüber und folgt der Kamera nicht. + - `$Name` und `get_node()` sind relative Pfade, `..` geht nach oben, `%Name` findet einen Node innerhalb der Szene unabhängig vom Pfad. + - In einer `.tscn` stehen Nodes in Baumreihenfolge mit `parent`-Pfad ohne Wurzelnamen, Verbindungen als `[connection]` mit Pfaden. + - `_ready()` läuft bei Kindern zuerst, `_process()` in Baumreihenfolge. +exercises: + - id: baum-aussagen + type: choice + title: Was gilt im Baum? + prompt: | + Welche Aussagen über Nodes und Szenen stimmen in Godot 4.7? + options: + - text: Eine `.tscn` hat genau einen Wurzel-Node. + correct: true + why: Die Doku verlangt genau eine Wurzel je Szene; ohne sie kann Godot die Datei nicht laden. + - text: Im Editor ist `Player` in `main.tscn` eine Zeile, zur Laufzeit hängen darunter `Body`, `Feet` und `Camera2D`. + correct: true + why: Der Editor zeigt die Instanz zugeklappt. Zur Laufzeit ist sie ein gewöhnlicher Teilbaum, wie `print_tree_pretty()` zeigt. + - text: Ein Node kann zwei Elternknoten haben, wenn er in zwei Szenen vorkommt. + correct: false + why: Jeder Node hat höchstens einen Elternknoten. Zwei Instanzen derselben Szene sind zwei getrennte Nodes. + - text: "`position` eines Kindes ist eine Weltkoordinate." + correct: false + why: "`position` ist relativ zum Elternknoten. Die Weltkoordinate heißt `global_position`." + explanation: | + Der Baum hat klare Regeln: eine Wurzel je Szene, ein Elternknoten je Node, Positionen relativ zum Elternknoten. Instanzen sehen im Editor kompakt aus, weil ihr Inhalt in der eingesetzten Szene bearbeitet wird; zur Laufzeit gibt es diesen Unterschied nicht mehr. Wer das weiß, sucht im Remote-Baum nicht vergeblich nach einer „Instanz“, sondern nach ihren Nodes. + resources: + - title: Godot Docs · Nodes and Scenes + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/nodes_and_scenes.html + - title: Godot Docs · TSCN file format · The scene tree + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#the-scene-tree + + - id: zeichenreihenfolge-ordnen + type: order + title: Wer liegt vorn? + prompt: | + Bring die Nodes von `main.tscn` in die Reihenfolge, in der Godot sie zeichnet, zuerst gezeichnet (ganz hinten) oben. `Objects` hat Y-Sortierung an, alle `z_index` sind 0. Positionen: `Pickup` (−50, −20), `Player` (0, 0), `Crate` (40, 10), `Pickup2` (−70, 30), `Pickup3` (70, 40). `Counter` hängt im `CanvasLayer` `HUD`. + lang: text + lines: | + Floor + Pickup + Player + Crate + Pickup2 + Pickup3 + Counter + hints: + - "`Floor` steht im Baum vor `Objects`." + - Unter `Objects` entscheidet die y-Position, kleinere zuerst. + - Der `CanvasLayer` liegt auf Ebene 1, über der Welt. + explanation: | + Zuerst kommt `Floor`, weil er im Baum vor `Objects` steht. Unter `Objects` sortiert Godot nach y: −20, 0, 10, 30, 40. Die x-Werte spielen keine Rolle. Der Zähler im `CanvasLayer` liegt auf Ebene 1 und damit über allem auf Ebene 0, egal welchen `z_index` etwas dort hat. + resources: + - title: Godot Docs · CanvasItem.y_sort_enabled + url: https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-y-sort-enabled + - title: Godot Docs · CanvasLayer + url: https://docs.godotengine.org/en/stable/classes/class_canvaslayer.html#description + + - id: global-position-skaliert + type: output + title: Ein Kind mit großem Vater + prompt: | + Das Skript hängt an einem `Node2D` am Ursprung. Was gibt es aus? + lang: gdscript + code: | + func _ready() -> void: + var a := Node2D.new() + a.position = Vector2(100, 50) + var b := Node2D.new() + b.position = Vector2(20, 0) + a.add_child(b) + add_child(a) + print(b.global_position) + a.scale = Vector2(2, 2) + print(b.global_position) + expected: | + (120.0, 50.0) + (140.0, 50.0) + accept: + - | + (120, 50) + (140, 50) + hints: + - "`b` steht 20 Pixel rechts von `a`." + - Die Skalierung von `a` vergrößert auch den Versatz seiner Kinder. + explanation: | + `b` liegt 20 Pixel rechts von `a`, also bei 100 + 20 = 120. Mit Skalierung 2 wird der Versatz doppelt so groß: 100 + 40 = 140. `b.position` bleibt die ganze Zeit `(20, 0)`. Godot gibt `Vector2` mit Nachkommastelle aus. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Node2D.global_position + url: https://docs.godotengine.org/en/stable/classes/class_node2d.html#class-node2d-property-global-position + + - id: global-position-gedreht + type: output + title: Eine Vierteldrehung + prompt: | + Gleiche Ausgangslage, aber `a` wird gedreht statt skaliert. Was gibt die letzte Zeile aus? + lang: gdscript + code: | + func _ready() -> void: + var a := Node2D.new() + a.position = Vector2(100, 50) + var b := Node2D.new() + b.position = Vector2(20, 0) + a.add_child(b) + add_child(a) + a.rotation_degrees = 90 + print(b.global_position) + expected: | + (100.0, 70.0) + accept: + - (100, 70) + hints: + - In Godot zeigt die y-Achse nach unten. + - Der Versatz (20, 0) dreht sich mit. + explanation: | + Die Drehung von `a` dreht den Versatz seines Kindes mit: Aus 20 Pixeln nach rechts werden 20 Pixel nach unten, weil die y-Achse in Godot nach unten zeigt. `b` landet bei (100, 50 + 20). So wandert etwa eine Hitbox an einem drehenden Pivot um die Figur. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · 2D transforms + url: https://docs.godotengine.org/en/stable/tutorials/2d/2d_transforms.html + + - id: ready-reihenfolge + type: output + title: Wer ist zuerst bereit? + prompt: | + Jeder Node dieser Szene hat dasselbe Skript. Was steht in der Ausgabe? + + ```text + Main (Node2D) + ├── Objects (Node2D) + │ ├── Player (CharacterBody2D) + │ └── Crate (StaticBody2D) + └── HUD (CanvasLayer) + ``` + lang: gdscript + code: | + extends Node + + + func _ready() -> void: + print(name) + expected: | + Player + Crate + Objects + HUD + Main + hints: + - Ein Node ist erst bereit, wenn alle seine Kinder bereit sind. + explanation: | + `_ready()` läuft bei den Kindern zuerst, und Geschwister kommen in Baumreihenfolge dran: Player und Crate, dann ihr Elternknoten Objects, dann HUD, zuletzt die Wurzel Main. Deshalb kann `Main` in `_ready()` auf alle Nodes zugreifen. `_process()` läuft dagegen von oben nach unten: Main, Objects, Player, Crate, HUD. Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Node._ready + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-ready + + - id: pfade-ausgeben + type: output + title: Von der Figur aus gesehen + prompt: | + Du ergänzt vorübergehend `player.gd` im Testgelände. Was gibt `_ready()` aus? Die Kiste steht in `main.tscn` bei `(40, 10)`. + lang: gdscript + code: | + func _ready() -> void: + print(get_path()) + print(get_parent().name) + print(get_node("../Crate").position) + expected: | + /root/Main/Objects/Player + Objects + (40.0, 10.0) + hints: + - "`get_path()` liefert den absoluten Pfad ab der Wurzel des Spiels, `/root`." + - "`..` ist der Elternknoten der Figur." + explanation: | + Der absolute Pfad beginnt bei `/root`, dem Fenster des Spiels, unter dem `Main` hängt. Der Elternknoten der Figur ist `Objects`, und `../Crate` geht von der Figur eine Ebene hoch und dort zu `Crate`. Dieselben Pfade stehen in Fehlermeldungen („relative to "/root/Main/HUD"“). Gemessen mit Godot 4.7.2. + resources: + - title: Godot Docs · Node.get_node + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-get-node + - title: Godot Docs · NodePath + url: https://docs.godotengine.org/en/stable/classes/class_nodepath.html + + - id: pfade-schreiben + type: gap + title: Zwei Wege zum Zähler + prompt: | + Ergänze die Pfade. Das erste Skript hängt an `HUD`, das zweite an `Objects/Player` in `main.tscn`. `Counter` hat keinen eindeutigen Namen. + lang: gdscript + code: | + # hud.gd am Node HUD + @onready var counter: Label = ⟦$Counter⟧ + + # player.gd am Node Objects/Player + @onready var hud_counter: Label = get_node("⟦../../HUD/Counter⟧") + gaps: + - label: Kind des HUD + hint: Die Kurzform mit `$`. + accept: + - get_node("Counter") + - $"Counter" + - label: Von der Figur zum Zähler + hint: Zwei Ebenen hoch zu `Main`, dann hinunter. + accept: + - /root/Main/HUD/Counter + explanation: | + `$Counter` ist die Kurzform für `get_node("Counter")`, relativ zum Node mit dem Skript. Von `Objects/Player` führt `..` zu `Objects`, ein zweites `..` zu `Main`, dann `HUD/Counter`. Der absolute Pfad `/root/Main/HUD/Counter` geht auch, bricht aber, sobald die Szene anders eingebunden ist. Ein `%Counter` ginge erst, wenn du den Zähler als eindeutigen Namen markierst. + resources: + - title: Godot Docs · Node.get_node + url: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-get-node + + - id: tscn-zeilen + type: gap + title: Drei Zeilen aus main.tscn + prompt: | + Ergänze die Zeilen so, dass `Counter` ein Kind von `HUD` ist, `Pickup2` eine Instanz von `pickup.tscn` unter `Objects` und das Signal `collected` der Figur `show_total` am `HUD` aufruft. + lang: ini + code: | + [ext_resource type="PackedScene" path="res://pickup.tscn" id="3"] + + [node name="Pickup2" parent="⟦Objects⟧" instance=ExtResource("⟦3⟧")] + + [node name="Counter" type="Label" parent="⟦HUD⟧"] + + [connection signal="collected" from="⟦Objects/Player⟧" to="HUD" method="show_total"] + gaps: + - label: Elternknoten von Pickup2 + hint: Der Pfad ab der Wurzel, ohne den Namen der Wurzel. + - label: Verweis auf pickup.tscn + hint: Die `id` aus der `[ext_resource]`-Zeile. + - label: Elternknoten von Counter + hint: Der `CanvasLayer`. + - label: Absender des Signals + hint: Der Pfad der Figur ab der Wurzel. + explanation: | + `parent` nennt den Pfad ab der Wurzel, aber ohne deren Namen: Kinder der Wurzel haben `parent="."`, Kinder von `Objects` haben `parent="Objects"`. `instance=ExtResource("3")` verweist über die `id` auf die `[ext_resource]`-Zeile von `pickup.tscn`. In `[connection]` sind `from` und `to` Pfade nach derselben Regel. Genau deshalb bricht eine Verbindung, wenn jemand den Node im Text verschiebt und `from` stehen lässt. + resources: + - title: Godot Docs · TSCN file format · The scene tree + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#the-scene-tree + + - id: zaehler-umbenannt + type: bug + title: Nach dem Umbenennen + prompt: | + Ein Agent hat im Szenen-Dock `HUD/Counter` in `TotalLabel` umbenannt. Seitdem meldet das Spiel beim Start, im Terminal gestartet: + + ```text + ERROR: Node not found: "Counter" (relative to "/root/Main/HUD"). + at: get_node (scene/main/node.cpp:1975) + GDScript backtrace (most recent call first): + [0] @implicit_ready (res://hud.gd:5) + ``` + + Finde die Zeile in `hud.gd` und korrigiere sie. + lang: gdscript + code: | + extends CanvasLayer + + ## Zeigt, wie viele Sammelstücke die Figur hat. + + @onready var counter: Label = $Counter + + + func show_total(total: int) -> void: + counter.text = str(total) + lines: [5] + fix: + - line: 5 + answers: + - "@onready var counter: Label = $TotalLabel" + - "@onready var counter: Label = get_node(\"TotalLabel\")" + - "@onready var counter: Label = $\"TotalLabel\"" + explanation: | + Umbenennen im Szenen-Dock ändert die Szene, aber keine Skripte. `$Counter` sucht ein Kind dieses Namens und findet keins; `@implicit_ready` in der Meldung zeigt auf die Zeile mit `@onready`. Die Korrektur nennt den neuen Namen. Dauerhaft robuster ist ein eindeutiger Name: Zähler als *Über einzigartigen Namen zugreifen* markieren und `%TotalLabel` schreiben. Die Meldung ist mit 4.7.2 nachgestellt. + resources: + - title: Godot Docs · Scene Unique Nodes + url: https://docs.godotengine.org/en/stable/tutorials/scripting/scene_unique_nodes.html + + - id: verbindung-verloren + type: bug + title: Der Zähler bleibt bei 0 + prompt: | + Ein Agent hat `main.tscn` als Text bearbeitet und die Figur direkt unter `Main` gehängt; dort soll sie bleiben. Das Spiel startet ohne Fehlermeldung, die Figur sammelt alle drei Stücke ein, aber der Zähler bleibt bei 0. Finde die Zeile und korrigiere sie. + lang: ini + code: | + [node name="Main" type="Node2D"] + + [node name="Objects" type="Node2D" parent="."] + y_sort_enabled = true + + [node name="Player" parent="." instance=ExtResource("1")] + + [node name="HUD" type="CanvasLayer" parent="."] + script = ExtResource("4") + + [connection signal="collected" from="Objects/Player" to="HUD" method="show_total"] + lines: [11] + fix: + - line: 11 + answers: + - '[connection signal="collected" from="Player" to="HUD" method="show_total"]' + explanation: | + `from="Objects/Player"` zeigt auf einen Node, den es nicht mehr gibt. Mit 4.7.2 nachgestellt: Godot lädt die Szene trotzdem ohne Meldung, nur die Verbindung fehlt. Hättest du die Figur im Szenen-Dock verschoben, hätte der Editor beim Speichern `from="Player"` geschrieben. Prüf nach Umbauten im Text das Signale-Dock: Die Wellen neben `Player` im Szenen-Dock wären verschwunden. Nebenbei liegt die Figur jetzt außerhalb der Y-Sortierung von `Objects`. + resources: + - title: Godot Docs · TSCN file format + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#file-structure + + - id: ursprung-am-kopf + type: bug + title: Davor und trotzdem dahinter + prompt: | + Y-Sortierung an `Objects` ist an. Die Figur steht mit den Füßen sichtbar unterhalb der Kiste, also davor, und wird trotzdem hinter ihr gezeichnet. Welche Zeile in `player.tscn` ist schuld? Korrigiere sie so, dass der Körper wie in der Vorlage über dem Ursprung liegt. + + ![Ausschnitt des Spielbilds: Die braune Kiste verdeckt den Oberkörper der blauen Figur, deren Füße unterhalb der Kiste herausschauen](/media/godot/szenenbaum/reihenfolge-kopf@2x.png "Godot 4.7.2, Figur-Ursprung bei y = 0, Kiste bei y = 10") + lang: ini + code: | + [node name="Player" type="CharacterBody2D"] + collision_layer = 2 + motion_mode = 1 + script = ExtResource("1") + stats = ExtResource("2") + + [node name="Body" type="Polygon2D" parent="."] + color = Color(0.35, 0.65, 0.95, 1) + polygon = PackedVector2Array(-6, 0, 6, 0, 6, 16, -6, 16) + + [node name="Feet" type="CollisionShape2D" parent="."] + position = Vector2(0, -3) + shape = SubResource("feet") + lines: [9] + fix: + - line: 9 + answers: + - polygon = PackedVector2Array(-6, -16, 6, -16, 6, 0, -6, 0) + - polygon = PackedVector2Array(-6, 0, 6, 0, 6, -16, -6, -16) + hints: + - Y-Sortierung vergleicht den Ursprung der Figur, nicht ihr Bild. + - Von welcher y-Koordinate bis zu welcher reicht der Körper hier? + explanation: | + Der Körper reicht hier von y = 0 bis 16, unter dem Ursprung. Der Ursprung sitzt also am Kopf. Steht der Kopf bei y = 0 und die Kiste bei y = 10, zeichnet Godot die Kiste vorn, obwohl die Füße bei y = 16 schon davor stehen. Mit dem Körper von −16 bis 0 liegt der Ursprung an den Füßen, wo Y-Sortierung ihn braucht. Die Fußform bei `(0, -3)` hätte den Fehler verraten: Sie saß über dem Kopf statt an den Füßen. Das Bild ist mit 4.7.2 nachgestellt. + resources: + - title: Godot Docs · CanvasItem.y_sort_enabled + url: https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-y-sort-enabled + + - id: canvaslayer-erklaeren + type: explain + title: Warum der Zähler in der Ecke bleibt + prompt: | + Erkläre in drei bis fünf Sätzen, warum `Counter` oben links bleibt, während die Kamera der Figur folgt, und was sich am Bild ändert, wenn jemand `Counter` aus dem `HUD` nimmt und direkt unter `Main` hängt. + + ![Spielbild: Zähler oben links, die Figur in der Bildmitte](/media/godot/szenenbaum/hud-canvaslayer@2x.png "Godot 4.7.2, Counter im CanvasLayer HUD") + lang: text + code: | + Main (Node2D) + ├── Floor (ColorRect) + ├── Objects (Node2D, y_sort_enabled) + │ └── Player … mit Camera2D als Kind + └── HUD (CanvasLayer, layer = 1) + └── Counter (Label, offset 4, 2) + points: + - Die Kamera verschiebt die Transformation der Welt auf Ebene 0, nicht die eines `CanvasLayer`. + - "`HUD` hat eine eigene Ebene mit eigener Transformation; `Counter` steht in Bildschirmkoordinaten bei (4, 2)." + - Ebene 1 liegt über Ebene 0, unabhängig von jedem `z_index` in der Welt. + - Direkt unter `Main` läge der Zähler in Weltkoordinaten bei (4, 2), würde mit der Kamera aus dem Bild wandern und nach Baumreihenfolge mit den Objekten gezeichnet. + explanation: | + Eine `Camera2D` arbeitet, indem sie die Transformation der Welt-Ebene verschiebt. Ein `CanvasLayer` hat eine eigene Transformation, die die Kamera nicht anfasst, solange `follow_viewport_enabled` aus ist. Deshalb steht `Counter` immer bei (4, 2) auf dem Bildschirm, und weil Ebene 1 über Ebene 0 liegt, verdeckt ihn nichts aus der Welt. Hängt der Zähler direkt unter `Main`, ist (4, 2) eine Weltkoordinate: Er steht neben dem Startpunkt der Figur, läuft mit der Kamera aus dem Bild und wird nach Baumreihenfolge und `z_index` mit den Objekten gezeichnet; so zeigt es das zweite Spielbild der Lektion. Nebenbei fände `hud.gd` ihn mit `$Counter` nicht mehr und meldet beim Start `Node not found: "Counter"`. Im Testgelände nachgestellt mit Godot 4.7.2. + resources: + - title: Godot Docs · Canvas layers + url: https://docs.godotengine.org/en/stable/tutorials/2d/canvas_layers.html + + - id: baum-rekonstruieren + type: practice + title: Einen Baum aus Text bauen + prompt: | + Schreib den Szenenbaum dieser Datei auf, mit Typ jedes Nodes, und notiere die Signalverbindung in einem Satz. Einrückung mit Leerzeichen reicht. + + ```ini + [gd_scene format=3] + + [ext_resource type="Script" path="res://door.gd" id="1"] + + [sub_resource type="RectangleShape2D" id="block"] + size = Vector2(16, 6) + + [sub_resource type="RectangleShape2D" id="sensor"] + size = Vector2(24, 24) + + [node name="Door" type="StaticBody2D"] + script = ExtResource("1") + + [node name="Body" type="Polygon2D" parent="."] + polygon = PackedVector2Array(-8, -16, 8, -16, 8, 0, -8, 0) + + [node name="Block" type="CollisionShape2D" parent="."] + position = Vector2(0, -3) + shape = SubResource("block") + + [node name="Sensor" type="Area2D" parent="."] + + [node name="Range" type="CollisionShape2D" parent="Sensor"] + shape = SubResource("sensor") + + [node name="Hint" type="Label" parent="Sensor"] + offset_top = -32.0 + text = "?" + + [connection signal="body_entered" from="Sensor" to="." method="_on_sensor_body_entered"] + ``` + lang: text + solution: | + Door (StaticBody2D, door.gd) + Body (Polygon2D) + Block (CollisionShape2D, Form "block") + Sensor (Area2D) + Range (CollisionShape2D, Form "sensor") + Hint (Label) + + Wenn ein Körper die Fläche von Sensor betritt, + sendet Sensor body_entered, und Door ruft + _on_sensor_body_entered auf. + checklist: + - "`Door` ist die Wurzel, weil ihre Zeile kein `parent` hat." + - "`Body`, `Block` und `Sensor` sind Kinder der Wurzel (`parent=\".\"`)." + - "`Range` und `Hint` hängen unter `Sensor`." + - Jede Form ist ihrem `CollisionShape2D` über die `id` der `[sub_resource]` zugeordnet. + - Der Satz zur Verbindung nennt Absender, Signal, Empfänger und Methode. + explanation: | + Die Wurzel erkennst du am fehlenden `parent`. `parent="."` heißt Kind der Wurzel, `parent="Sensor"` Kind von `Sensor`. Die Formen stecken als `[sub_resource]` in der Datei, ihre `id` verbindet sie mit dem `shape` der Nodes. In der Verbindung steht `to="."`, also die Wurzel `Door` mit ihrem Skript. Mit 4.7.2 geladen, `print_tree_pretty()` zeigt denselben Baum. + resources: + - title: Godot Docs · TSCN file format + url: https://docs.godotengine.org/en/stable/engine_details/file_formats/tscn.html#file-structure diff --git a/content/godot/modules/werkstatt-messen.yaml b/content/godot/modules/werkstatt-messen.yaml new file mode 100644 index 0000000..819610b --- /dev/null +++ b/content/godot/modules/werkstatt-messen.yaml @@ -0,0 +1,698 @@ +id: werkstatt-messen +title: "Werkstatt: vorhersagen, ändern, messen – und vorbereitete Fehler finden" +level: 3 +minutes: 180 +summary: Am Testgelände misst du mit Log, Anzeige, eigenem Monitor und Zeitlupe, sagst Änderungen vorher und prüfst sie – und öffnest die Fehlerkiste, die einen von acht vorbereiteten Fehlern einbaut, den du über sein Symptom findest, im Diff belegst und protokollierst. +place: pc +goals: + - Du baust am Testgelände einen Messpunkt mit Anzeige im Spiel, eigenem Monitor im Debugger und einem Regler für `Engine.time_scale`. + - Du sagst vor einer Änderung voraus, was Log und Anzeige danach zeigen, und vergleichst mit der Messung. + - Du nimmst die Fehlerkiste in Betrieb, baust Fehler ein, ohne nachzusehen, und setzt das Projekt mit Git zurück. + - Du findest acht vorbereitete Fehler aus Import, Kollision, Skalierung, Parametern, Signalen, Y-Sortierung, Audio-Bus und Physik-Takt über ihr Symptom. + - Du führst ein Korrektur- und Einstellungsprotokoll, das das Abschlussprojekt fortsetzt. +resources: + - title: Godot Docs · Custom performance monitors + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html + - title: Godot Docs · Performance.add_custom_monitor + url: https://docs.godotengine.org/en/stable/classes/class_performance.html#class-performance-method-add-custom-monitor + - title: Godot Docs · Engine.time_scale + url: https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-time-scale + - title: Godot Docs · Command line tutorial + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html +lesson: | + Du hast die Werkzeuge kennengelernt: Debugger, Monitore und Profiler in [Fehler eingrenzen](/godot/fehler-eingrenzen), die Takte in [Bildrate und Simulation](/godot/bildrate-simulation), die Methode des Experiments in [Tuning als Experiment](/combat/tuning). Hier wendest du sie am Testgelände an, zuerst an Werten, die du selbst änderst, dann an Fehlern, die du nicht kennst. Ein Skript, die **Fehlerkiste**, baut einen von acht vorbereiteten Fehlern ein. Du siehst nur das Symptom und gehst vor, wie du es bei einem Projekt tun würdest, das ein Coding-Agent umgebaut hat: beschreiben, eingrenzen, belegen, protokollieren. + + Alle Werte in den Skripten sind Platzhalter, damit die Messungen laufen. Welche Werte dein Spiel bekommt, entscheidest du. Alle Ausgaben stammen aus Läufen mit Godot 4.7.2 an einem Testgelände, das die vorigen Module erweitert haben. + + ## Der Prüfstand im Testgelände + + Die Fehlerkiste und die Messungen brauchen ein paar Dinge, die du aus früheren Modulen hast oder jetzt ergänzt: + + - die Figur mit ihrem `Sprite` aus [Animationen zuordnen](/godot/animationen), + - den Ton beim Einsammeln aus dem Modul zu [Audio](/godot/audio): `PickupSound` mit `pickup_sound.gd` auf einem eigenen Bus, dort mit dem Platzhalternamen `SFX`, + - das Prüfmuster aus [Pixelraster und Skalierung](/godot/pixelraster), 32 × 32 Pixel mit Rahmen, Schachbrett, Streifen und Diagonalen, als `res://test_assets/pruefmuster.png` und als `Sprite2D` namens `Pruefmuster` unter `Objects`, + - ein Git-Repository mit sauberem Stand, wie in [Testversionen exportieren und sichern](/godot/export-sichern), + - den neuen `Messpunkt` mit einer Anzeige. + + ```text + Main (Node2D) + ├── Floor oder Ground je nach Stand des Testgeländes + ├── Objects (Node2D) y_sort_enabled + │ ├── Player mit Sprite (AnimatedSprite2D), Body unsichtbar + │ ├── Crate, Pickups, gegebenenfalls Walls + │ └── Pruefmuster (Sprite2D) pruefmuster.png, Position ist ein Platzhalter + ├── HUD (CanvasLayer) + │ ├── Counter (Label) + │ └── Messanzeige (Label) Schriftgröße 8 unter Theme Overrides → Font Sizes + ├── PickupSound aus dem Audio-Modul, Bus SFX + └── Messpunkt (Node) messpunkt.gd, player = Objects/Player, label = HUD/Messanzeige + ``` + + Das Prüfmuster steht neben der Figur, weil es Fehler im Import und in der Skalierung deutlicher zeigt als ein einzelnes Feld des Testbogens. + + Der Messpunkt ist technisches Gerüst. Er zeigt Tempo und Physik-Tick der Figur, meldet die eingesammelten Stücke als eigenen Monitor und stellt die Zeitlupe ein: + + ```gdscript title=werkstatt/messpunkt.gd + extends Node + + ## Werkstatt-Messpunkt: zeigt Tempo und Physik-Tick der Figur im Spiel an, + ## meldet die eingesammelten Stücke als eigenen Monitor und stellt Engine.time_scale ein. + ## Technisches Gerüst, keine Designentscheidung. Alle Werte sind Platzhalter. + + @export var player: Player + @export var label: Label + ## 1 = normal, 0.25 = Viertel-Tempo zum Zusehen + @export_range(0.05, 1.0, 0.05) var time_scale := 1.0: + set(value): + time_scale = value + Engine.time_scale = value + + + func _ready() -> void: + Performance.add_custom_monitor(&"Testgelände/Gesammelt", _collected) + + + func _exit_tree() -> void: + Performance.remove_custom_monitor(&"Testgelände/Gesammelt") + + + func _process(_delta: float) -> void: + label.text = "Tempo %5.1f px/s Tick %d" % [player.velocity.length(), Engine.get_physics_frames()] + + + func _collected() -> int: + return player.total + ``` + + ![Spielbild des Testgeländes in 320 × 180, doppelt vergrößert: die Figur mit einem Feld des Testbogens vor der Kiste, links darunter das Prüfmuster mit Schachbrett, Streifen und Diagonalen, oben der Zähler 0, unten die Messanzeige Tempo 0.0 px/s Tick 6](/media/godot/werkstatt-messen/pruefstand@2x.png "Godot 4.7.2, Testgelände mit Prüfstand, Spielbild zweifach vergrößert") + + ## Messpunkte: Log, Anzeige, eigener Monitor, Zeitlupe + + Vier Werkzeuge, vier Fragen: + + | Frage | Werkzeug | Takt | + | --- | --- | --- | + | Was passiert in jedem Tick? | `debug_log` am Player, Ausgabe | jeder Physik-Tick | + | Was sehe ich gerade? | Messanzeige im Spiel | jedes Bild | + | Wie entwickelt sich ein Zählwert über Minuten? | eigener Monitor im Debugger | einmal pro Sekunde | + | Wo hakt eine Bewegung? | `time_scale` am Messpunkt | streckt die Spielzeit | + + **Log.** Mit `debug_log` an schreibt `player.gd` in jedem Tick `Tick speed Tempo` in die Ausgabe. Das ist die genaueste Quelle; mit `--fixed-fps 60` aus dem Terminal ist sie bei jedem Lauf gleich. + + **Eigener Monitor.** `Performance.add_custom_monitor()` hängt einen Wert in den Reiter Monitore. Ein Schrägstrich im Namen legt die Gruppe fest: `Testgelände/Gesammelt` erscheint als Gruppe „Testgelände“ mit dem Eintrag „Gesammelt“ ([Custom performance monitors](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html)). Die Funktion muss eine Zahl ab 0 liefern. Der Editor fragt sie einmal pro Sekunde ab, deshalb taugen Monitore für Zählwerte, nicht für das Tempo eines einzelnen Ticks. Den Monitor entfernst du in `_exit_tree()`, sonst meldet ein zweites `add_custom_monitor()` mit derselben ID einen Fehler ([add_custom_monitor](https://docs.godotengine.org/en/stable/classes/class_performance.html#class-performance-method-add-custom-monitor)). + + ![Debugger, Reiter Monitore: Gruppe Testgelände mit Gesammelt 3 angehakt; rechts der Graph Gesammelt steigt in drei Stufen von 0 auf 3, daneben Verwaiste Nodes konstant 0](/media/godot/werkstatt-messen/monitor-gesammelt@2x.png "Godot 4.7.2, eigener Monitor, während die Figur drei Sammelstücke einsammelt") + + **Zeitlupe.** `time_scale` am Messpunkt setzt `Engine.time_scale` über einen Setter; `@export_range` macht daraus einen Schieberegler im Inspektor ([@export_range](https://docs.godotengine.org/en/stable/classes/class_@gdscript.html#class-gdscript-annotation-export-range)). Du kannst es im laufenden Spiel im Remote-Inspektor verändern; wie Remote und Lokal zusammenspielen und warum ein dort gefundener Wert nach dem Beenden weg ist, steht in [Aktionen und Parameter](/godot/aktionen-parameter). Gemessen: Bei 0.25 erreicht die Figur 90 px/s nach 36 statt 9 Ticks, und die Anzeige zeigt dann `Tempo 90.0 px/s`. Das Tempo steigt pro Tick langsamer, weil `delta` kleiner ist, und auch der Weg pro Tick schrumpft; der Endwert von `velocity` bleibt derselbe. Ganze Läufe als Bildfolge nimmst du mit dem Movie Maker auf, beschrieben in [Tuning als Experiment](/combat/tuning). + + ## Vorhersagen, ändern, messen + + Bevor du einen Wert änderst, schreibst du auf, was du erwartest. `player.gd` bewegt das Tempo mit `move_toward()` pro Tick um höchstens `acceleration × delta` auf das Ziel zu ([Vector2.move_toward](https://docs.godotengine.org/en/stable/classes/class_vector2.html#class-vector2-method-move-toward)). Bei den Platzhaltern 90/600/900 wächst das Tempo also pro Tick um 600 / 60 = 10 px/s, nach 9 Ticks sind 90 erreicht; nach dem Loslassen sinkt es um 900 / 60 = 15 pro Tick, die Figur steht nach 6 Ticks, der Bremsweg ist 3,75 px. Eine Probe mit anderen Platzhaltern, `acceleration` 300 und `friction` 450: Vorhersage 18 Ticks bis 90 px/s und 12 Ticks bis zum Stillstand. Gemessen mit Godot 4.7.2: 18 und 12, Bremsweg 8,25 px. + + Stimmt die Messung nicht, ist das ein Befund, kein Fehlschlag. Dann rechnet der Code anders, als du dachtest, oder eine andere Stelle setzt den Wert. Die Reihenfolge, in der Skript, Szene, Instanz und Ressource einen Wert setzen, steht in [Aktionen und Parameter](/godot/aktionen-parameter). Eine Zeile pro Versuch reicht: + + ```text + Datum Godot Datei · Wert alt -> neu Vorhersage Messung Abweichung erklärt? + ``` + + > [!PRAXIS] + > Lass einen Coding-Agenten den Messpunkt bauen, aber prüf ihn mit einer Vorhersage, bevor du ihm traust: Stell einen Wert ein, dessen Ergebnis du ausrechnen kannst, und vergleiche. Eine Messung, die du nicht nachrechnen kannst, ist keine Messung. + + ## Die Fehlerkiste + + Die Fehlerkiste ist ein Skript, das Godot als Hauptschleife ausführt, ohne Fenster und ohne Spiel. Es prüft, ob alles committet ist, wählt zufällig einen von acht Fehlern, baut ihn in eine Datei des Testgeländes ein und schreibt die Nummer nach `user://fehlerkiste.txt`, unter Linux `~/.local/share/godot/app_userdata/Testgelände/fehlerkiste.txt`. Die Fehler betreffen Import, Kollision, Skalierung, Parameter, eine Signalverbindung, die Y-Sortierung, einen Audio-Bus und den Physik-Takt. Jeder ändert genau einen Wert in einer Datei. + + Leg das Skript **außerhalb** des Projekts ab, etwa als `~/werkstatt/fehlerkiste.gd`. So taucht es nicht im Dateisystem-Dock auf, kommt nicht in Exporte, und `git restore` setzt es nicht zurück. Der Code verrät, welche Fehler es gibt; lies ihn erst, wenn du alle acht gefunden hast. + + > [!WARNING] Erst committen, Editor schließen + > Die Kiste bricht ab, solange `git status` Änderungen zeigt, damit `git diff` danach genau den Fehler zeigt und `git restore .` ihn sauber entfernt. Schließ den Editor vorher, damit er beim Speichern keine geöffnete Szene mit seiner Fassung überschreibt. + + ```console + $ git status --short + $ godot --headless --path . -s ~/werkstatt/fehlerkiste.gd + Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org + Ein Fehler ist eingebaut. Die Nummer steht in …/godot/app_userdata/Testgelände/fehlerkiste.txt + ``` + + `-s` führt ein Skript aus; ein absoluter Pfad ist erlaubt ([Running a script](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#running-a-script)). Mit `-- 4` am Ende baust du gezielt Fehler 4 ein, etwa um eine Korrektur zu wiederholen. Gibt es noch Änderungen, kommt `Abbruch: Es gibt Änderungen ohne Commit. Erst committen, dann die Kiste öffnen.`, und nichts wird verändert. Ist der Ordner kein Git-Repository oder fehlt git, lautet die Meldung `Abbruch: git lässt sich nicht starten, oder der Projektordner ist kein Git-Repository.` + + ```gdscript title=~/werkstatt/fehlerkiste.gd + extends SceneTree + + ## Fehlerkiste der Werkstatt: baut genau einen vorbereiteten Fehler ins Testgelände ein + ## und schreibt seine Nummer nach user://fehlerkiste.txt. + ## Editor schließen, alles committen, dann im Projektordner: + ## godot --headless --path . -s ~/werkstatt/fehlerkiste.gd + ## Mit "-- 4" am Ende baust du gezielt Fehler 4 ein. Zurück zum sauberen Stand: git restore . + ## Jeder Fehler ist eine Textänderung in genau einer Datei, wie sie auch ein Agent machen könnte. + ## Technisches Gerüst. Die Werte sind Platzhalter, keine Vorschläge für dein Spiel. + + const PATTERN := "res://test_assets/pruefmuster.png" ## dein Prüfmuster + const COUNT := 8 + + + func _initialize() -> void: + var status := _git_status() + if status == "kein-git": + print("Abbruch: git lässt sich nicht starten, oder der Projektordner ist kein Git-Repository.") + quit(1) + return + if status == "geaendert": + print("Abbruch: Es gibt Änderungen ohne Commit. Erst committen, dann die Kiste öffnen.") + quit(1) + return + var args := OS.get_cmdline_user_args() + var numbers: Array = [int(args[0])] if args.size() > 0 else range(1, COUNT + 1) + numbers.shuffle() + for number: int in numbers: + if _apply(number): + var file := FileAccess.open("user://fehlerkiste.txt", FileAccess.WRITE) + file.store_line(str(number)) + print("Ein Fehler ist eingebaut. Die Nummer steht in ", ProjectSettings.globalize_path("user://fehlerkiste.txt")) + quit() + return + print("Abbruch: Kein passender Fehler ließ sich einbauen.") + quit(1) + + + func _apply(number: int) -> bool: + match number: + 1: return _set_import(PATTERN, "compress/mode", 2) + 2: return _set_property("res://crate.tscn", "[node name=\"Crate\"", "collision_layer", "4", true) + 3: return _set_property("res://player.tscn", "[node name=\"Camera2D\"", "zoom", "Vector2(1.5, 1.5)", true) + 4: return _set_property("res://data/player_stats.tres", "[resource]", "friction", "30.0", true) + 5: return _remove_line("res://main.tscn", "", "[connection signal=\"collected\" from=\"Objects/Player\" to=\"HUD\" method=\"show_total\"]") + 6: return _remove_line("res://main.tscn", "[node name=\"Objects\"", "y_sort_enabled = true") + 7: return _set_property("res://default_bus_layout.tres", "[resource]", "bus/1/volume_db", "-80.0", false) + 8: + ProjectSettings.set_setting("physics/common/physics_ticks_per_second", 30) + return ProjectSettings.save() == OK + return false + + + ## Liefert "sauber", "geaendert" oder "kein-git". + func _git_status() -> String: + var output := [] + var code := OS.execute("git", ["-C", ProjectSettings.globalize_path("res://"), "status", "--porcelain"], output) + if code != 0: + return "kein-git" + return "sauber" if output[0].strip_edges().is_empty() else "geaendert" + + + func _set_import(path: String, key: String, value: Variant) -> bool: + var config := ConfigFile.new() + if config.load(path + ".import") != OK: + return false + config.set_value("params", key, value) + return config.save(path + ".import") == OK + + + ## Setzt "key = value" im Abschnitt, der mit header beginnt. Steht key noch nicht da, + ## hängt die Funktion die Zeile ans Ende des Abschnitts, wenn insert true ist, + ## also hinter "script = …", damit auch Werte aus Skripten ankommen. + func _set_property(path: String, header: String, key: String, value: String, insert: bool) -> bool: + var lines := _read(path) + var start := _find_header(lines, header) + if start == -1: + return false + var i := start + 1 + while i < lines.size() and lines[i] != "" and not lines[i].begins_with("["): + if lines[i].begins_with(key + " = "): + lines[i] = key + " = " + value + return _write(path, lines) + i += 1 + if not insert: + return false + lines.insert(i, key + " = " + value) + return _write(path, lines) + + + ## Entfernt eine Zeile, im ganzen Text (header leer) oder nur im Abschnitt unter header. + func _remove_line(path: String, header: String, line: String) -> bool: + var lines := _read(path) + var start := 0 if header.is_empty() else _find_header(lines, header) + if start == -1: + return false + for i in range(start, lines.size()): + if not header.is_empty() and i > start and (lines[i].begins_with("[") or lines[i] == ""): + break + if lines[i] == line: + lines.remove_at(i) + return _write(path, lines) + return false + + + func _find_header(lines: PackedStringArray, header: String) -> int: + for i in lines.size(): + if lines[i].begins_with(header): + return i + return -1 + + + func _read(path: String) -> PackedStringArray: + return FileAccess.get_file_as_string(path).split("\n") + + + func _write(path: String, lines: PackedStringArray) -> bool: + var file := FileAccess.open(path, FileAccess.WRITE) + if file == null: + return false + file.store_string("\n".join(lines)) + return true + ``` + + Die Kiste ändert Dateien als Text: Sie setzt oder löscht genau eine Zeile, wie es ein Agent mit einem Texteditor tun würde. Nur die Import-Einstellung schreibt sie über `ConfigFile` und den Physik-Takt über `ProjectSettings.save()` ([ConfigFile](https://docs.godotengine.org/en/stable/classes/class_configfile.html)). Deshalb zeigt `git diff` danach genau die geänderte Zeile, auch in Szenen, die du von Hand aus dem Kurs kopiert und nie im Editor gespeichert hast. Gemessen mit Godot 4.7.2 an einem frischen Klon: Jeder der acht Fehler änderte genau eine Datei, beim Physik-Takt kam der Abschnitt `[physics]` mit zwei Leerzeilen dazu, und nach `git restore .` zeigte `git status` nichts mehr. + + > [!DEEP] Die Reihenfolge in einer `.tres` + > Godot setzt die Werte einer Ressource in der Reihenfolge, in der sie in der Datei stehen. Ein Wert aus einem Skript wie `friction` muss deshalb hinter `script = ExtResource(…)` stehen. Im Test mit Godot 4.7.2 stand `friction = 30.0` zuerst vor dieser Zeile: Die Datei lud ohne Meldung, und `friction` war trotzdem 900, weil der Wert gesetzt wurde, bevor das Skript da war. Eine erste Fassung der Kiste hatte genau diesen Fehler, gefunden durch eine Messung, die nicht zur Vorhersage passte. + + ## Eine Runde mit der Kiste + + 1. Kiste öffnen, Editor starten, spielen. Den Inhalt von `fehlerkiste.txt` nicht ansehen. + 2. **Symptom** notieren: was du siehst, hörst oder misst, ohne Vermutung. „Die Figur läuft durch die Kiste“, nicht „die Maske stimmt nicht“. + 3. **Vermutung** notieren und mit einem Werkzeug prüfen: Inspektor, Import-Dock, Audio-Panel, Projekteinstellungen, Log, Monitor, *Sichtbare Collision-Shapes*. + 4. **Ursache** finden und erst dann mit `git diff` bestätigen. Der Diff ist der Beleg, nicht der Suchweg. + 5. Nummer in `fehlerkiste.txt` nachsehen und die passende Übung „Fehlerkiste, Fall …“ unten öffnen. + 6. Zurücksetzen mit `git restore .` ([git-restore](https://git-scm.com/docs/git-restore)), Editor öffnen; geänderte Import-Einstellungen importiert er dabei neu. + 7. Protokollieren, dann die nächste Runde. + + Wo du je nach Symptom zuerst nachsiehst: + + | Symptom | Erste Stelle | + | --- | --- | + | Bild sieht anders aus als die Datei | Import-Dock, `.import`-Datei, Filter, Kamera | + | Pixel ungleich groß, alles zu groß oder zu klein | Stretch-Einstellungen, `Camera2D` → Zoom | + | Figur läuft durch etwas oder sammelt nicht | *Sichtbare Collision-Shapes*, Layer und Mask im Inspektor | + | Bewegung fühlt sich anders an | Log mit `debug_log`, Werte in der `.tres` | + | Etwas passiert, aber die Anzeige reagiert nicht | Signale-Dock, `[connection]` in der `.tscn` | + | Falsche Figur vorn | Y-Sortierung am Elternknoten | + | Ton fehlt | Audio-Panel: Lautstärke, Stumm, Bus des Players | + | Bewegung gröber, alles reagiert träger | Projekteinstellungen → Physik, Tick im Messpunkt | + + Die Tabelle nennt Orte, keine Lösungen. Mehrere Symptome können dieselbe Ursache haben, und dasselbe Symptom kann aus verschiedenen Ursachen kommen. + + > [!INTERVIEW] + > „Ein Prototyp verhält sich nach einem Umbau anders, und niemand weiß, warum. Wie gehst du vor?“ Gute Antwort: Ich beschreibe das Symptom ohne Vermutung und stelle es reproduzierbar nach. Dann prüfe ich gezielt die Stellen, an denen sich dieses Symptom einstellen lässt, und erst wenn ich eine Ursache habe, bestätige ich sie im Diff gegen den letzten guten Commit. Jede Korrektur bekommt einen eigenen Commit und einen Eintrag mit Symptom, Ursache und Beleg, damit das Team den Fehler beim nächsten Mal wiedererkennt. + + ## Das Korrektur- und Einstellungsprotokoll + + Das Abschlussprojekt verlangt zwei Dateien, die hier beginnen. `KORREKTUREN.md` bekommt pro Fehler einen Eintrag: + + ```text + Fall ‹Nummer aus fehlerkiste.txt, nach der Diagnose eingetragen› + Symptom ‹was du gesehen, gehört oder gemessen hast› + Vermutung ‹vor der Prüfung› + Prüfung ‹Werkzeug und was es gezeigt hat› + Ursache ‹Datei, Zeile, Wert› + Korrektur ‹git restore oder deine Änderung› + Beleg ‹Diff-Zeile, Log-Ausschnitt oder Screenshot› + Dauer ‹Minuten bis zur Ursache› + ``` + + `EINSTELLUNGEN.md` sammelt jede Einstellung, die vom Standard abweicht, mit Ort und einem Satz Begründung. Was nicht in `project.godot`, einer `.import`-Datei oder `default_bus_layout.tres` steht, ist der Standard; diese drei Dateien sind deshalb die Quelle, nicht deine Erinnerung. Platzhalter aus dem Kurs, die du übernimmst, kennzeichnest du dort als solche. + + ## Häufige Fehler + + - **Vermutung als Symptom notiert.** „Maske falsch“ ist schon eine Diagnose. Schreib auf, was du siehst. + - **Zuerst `git diff`.** Dann übst du Lesen von Diffs, nicht Eingrenzen. Der Diff kommt als Beleg am Ende. + - **Kiste bei offenem Editor geöffnet.** Speicherst du danach im Editor, kann er seine Fassung einer geöffneten Szene zurückschreiben, und der Fehler ist weg. + - **Monitor für schnelle Werte.** Einmal pro Sekunde abgefragt, zeigt er das Tempo nur zufällig an einer Stelle. Dafür gibt es Log und Anzeige. + - **Monitor nicht entfernt.** Startet der Messpunkt ein zweites Mal, meldet `add_custom_monitor()` einen Fehler. + - **Zeitlupe mit falscher Erwartung.** `time_scale` streckt `delta`: `velocity` steigt langsamer, erreicht aber denselben Endwert; Tick-Zähler laufen weiter. + - **Nach `git restore` nicht neu importiert.** Eine zurückgesetzte `.import`-Datei wirkt erst, wenn Godot neu importiert, also nach dem Öffnen des Editors oder `--import`. + + ## Zusammenfassung + + - Der Prüfstand ergänzt das Testgelände um Prüfmuster, Ton auf eigenem Bus und einen Messpunkt; Werte darin sind Platzhalter. + - Log für jeden Tick, Anzeige für jedes Bild, eigener Monitor für Zählwerte einmal pro Sekunde, `time_scale` zum Zusehen. + - Vor jeder Änderung steht eine Vorhersage; eine Abweichung ist ein Befund. + - Die Fehlerkiste baut einen von acht Fehlern ein, nur bei sauberem Stand und mit geschlossenem Editor; die Nummer liegt in `user://fehlerkiste.txt`. + - Eine Runde: Symptom, Vermutung, Prüfung, Ursache, dann Diff als Beleg, `git restore .`, Protokoll. + - `KORREKTUREN.md` und `EINSTELLUNGEN.md` setzen das Abschlussprojekt fort; die Quelle für Einstellungen sind die Dateien, nicht die Erinnerung. +exercises: + - id: kistenrunde-ordnen + type: order + title: Eine Runde mit der Fehlerkiste + prompt: | + Bring die Schritte einer Runde in die Reihenfolge aus der Lektion. + lang: prose + lines: | + Mit git status prüfen, dass alles committet ist, und den Editor schließen. + Die Kiste mit godot --headless -s öffnen. + Spielen und das Symptom ohne Vermutung notieren. + Eine Vermutung aufschreiben und mit einem Werkzeug im Editor prüfen. + Die gefundene Ursache mit git diff bestätigen und die Nummer in fehlerkiste.txt nachsehen. + Mit git restore zurücksetzen und den Eintrag in KORREKTUREN.md schreiben. + explanation: | + Ohne sauberen Stand bricht die Kiste ab, und ein offener Editor würde den Fehler überschreiben. Das Symptom kommt vor jeder Vermutung, sonst siehst du nur, was du erwartest. Der Diff steht am Ende: Er bestätigt die Ursache, die du mit den Werkzeugen gefunden hast. Zurücksetzen und Protokoll schließen die Runde ab, damit die nächste wieder auf sauberem Stand beginnt. + resources: + - title: Git · git-restore + url: https://git-scm.com/docs/git-restore + + - id: zeitlupe-anzeige + type: output + title: Was die Anzeige in der Zeitlupe zeigt + prompt: | + Am Messpunkt steht `time_scale` auf 0.25, die Werte sind die Platzhalter 90/600/900. Die Figur startet aus dem Stand und `move_right` bleibt gedrückt. Erste Zeile: Nach wie vielen Ticks erreicht die Figur 90 px/s? Zweite Zeile: Welche Zahl zeigt die Messanzeige danach hinter „Tempo“? + lang: gdscript + code: | + # player.gd + velocity = velocity.move_toward(input * stats.max_speed, rate * delta) + + # messpunkt.gd + label.text = "Tempo %5.1f px/s Tick %d" % [player.velocity.length(), Engine.get_physics_frames()] + expected: | + 36 + 90.0 + accept: + - | + 36 Ticks + 90.0 + - | + 36 + 90 + - | + 36 Ticks + 90 + - | + 36 + Tempo 90.0 px/s + - | + 36 + Tempo 90.0 px/s + explanation: | + `time_scale` 0.25 viertelt `delta`, das Tempo wächst pro Tick um 600 × 1/240 = 2,5 statt 10 px/s; 90 / 2,5 = 36 Ticks. Der Endwert von `velocity` ist trotzdem 90, denn `move_toward` steuert auf `max_speed` zu, unabhängig von `delta`. Langsamer werden der Anstieg pro Tick und der Weg pro Tick. Gemessen mit Godot 4.7.2: 9 Ticks bei 1.0, 36 bei 0.25, und die Anzeige zeigte `Tempo 90.0 px/s`. + resources: + - title: Godot Docs · Engine.time_scale + url: https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-time-scale + + - id: monitor-registrieren + type: gap + title: Ein eigener Monitor, sauber auf- und abgebaut + prompt: | + Ergänze die Lücken, damit die eingesammelten Stücke im Reiter Monitore unter „Testgelände“ erscheinen und der Monitor beim Verlassen des Baums wieder verschwindet. + lang: gdscript + code: | + func _ready() -> void: + Performance.⟦add_custom_monitor⟧(&"Testgelände/Gesammelt", _collected) + + + func _exit_tree() -> void: + Performance.⟦remove_custom_monitor⟧(&"Testgelände/Gesammelt") + + + func _collected() -> int: + return player.total + gaps: + - label: Monitor anlegen + hint: Die Methode von `Performance`, die einen Namen und ein Callable bekommt. + - label: Monitor entfernen + hint: Das Gegenstück zum Anlegen, mit derselben ID. + explanation: | + `add_custom_monitor()` nimmt eine ID und ein Callable, das eine Zahl ab 0 liefert. Der Teil vor dem Schrägstrich wird zur Gruppe im Reiter Monitore. Ohne `remove_custom_monitor()` bliebe die ID registriert, und ein zweiter Start des Messpunkts, etwa nach einem Szenenwechsel, würde mit derselben ID einen Fehler melden. Die Doku nennt diesen Fehler in der Beschreibung von `add_custom_monitor()`. + resources: + - title: Godot Docs · Performance.add_custom_monitor + url: https://docs.godotengine.org/en/stable/classes/class_performance.html#class-performance-method-add-custom-monitor + + - id: fall-1 + type: bug + title: Fehlerkiste, Fall 1 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Das Prüfmuster sieht im Spiel anders aus als die Datei. Der dunkelgraue Grund ist in Blöcken fast schwarz, im Feld mit den Diagonalen entstehen hellere Kästchen, Weiß wirkt stellenweise grau (rechts im Bild, links der saubere Stand). Finde die Zeile in `test_assets/pruefmuster.png.import` und korrigiere sie. + + ![Zwei Spielausschnitte des Prüfmusters, sechsfach vergrößert: links gleichmäßig grauer Grund mit Schachbrett, Streifen und Diagonalen; rechts ist der Grund in Blöcken schwarz, um die Diagonalen liegen hellere Kästchen](/media/godot/werkstatt-messen/fall-import@2x.png "Godot 4.7.2, links Lossless, rechts nach Fall 1") + lang: ini + code: | + [params] + + compress/mode=2 + compress/high_quality=false + compress/lossy_quality=0.7 + mipmaps/generate=false + process/fix_alpha_border=true + process/size_limit=0 + lines: [3] + fix: + - line: 3 + answers: + - compress/mode=0 + explanation: | + `compress/mode=2` ist im Import-Dock *Komprimieren → Modus: VRAM Compressed*. Godot speichert die Textur dann in einem Blockformat, das jeden Block aus 4 × 4 Pixeln mit wenigen Farben beschreibt; im Test lud das Spiel das deckende Prüfmuster im Format DXT1, einen Testbogen mit Transparenz im Format DXT5. Pixelart verliert dabei Flächen und Kanten. `0` ist `Lossless`, der Standard für 2D. Nach `git restore .` importiert Godot die Datei beim nächsten Öffnen neu; gemessen mit Godot 4.7.2 war die Textur danach wieder unkomprimiert. Mehr zu den Import-Einstellungen in [Sprites importieren](/godot/sprites-importieren). + resources: + - title: Godot Docs · Importing images · Compress mode + url: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html#compress-mode + + - id: fall-2 + type: bug + title: Fehlerkiste, Fall 2 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Die Figur läuft durch die Kiste hindurch. Mit `move_right` von (0, 8) aus stand sie nach 60 Ticks bei x = 84 statt bei 27. Sammelstücke sammelt sie weiter ein. Finde die Zeile in `crate.tscn` und korrigiere sie. + lang: ini + code: | + [gd_scene format=3 uid="uid://pf85is85vm4h"] + + [sub_resource type="RectangleShape2D" id="base"] + size = Vector2(16, 8) + + [node name="Crate" type="StaticBody2D" unique_id=755276202] + collision_mask = 0 + collision_layer = 4 + + [node name="Body" type="Polygon2D" parent="." unique_id=1999749856] + color = Color(0.6, 0.45, 0.3, 1) + polygon = PackedVector2Array(-8, -16, 8, -16, 8, 0, -8, 0) + + [node name="Base" type="CollisionShape2D" parent="." unique_id=319616897] + position = Vector2(0, -4) + shape = SubResource("base") + lines: [8] + fix: + - line: 8 + answers: + - collision_layer = 1 + explanation: | + Die Kiste liegt mit Wert 4 auf Ebene 3, „pickups“. Die Figur sucht mit Maske 1 nur Ebene 1, „world“, findet die Kiste also nicht und läuft hindurch. Die Pickups sind nicht betroffen, weil sie die Figur auf Ebene 2 suchen. Im Testgelände steht in `crate.tscn` gar keine Zeile `collision_layer`, weil Ebene 1 der Standard ist; die korrigierte Zeile verschwindet beim nächsten Speichern im Editor wieder. Mehr zu Ebenen und Masken in [Kollisionen und Ebenen](/godot/kollision-ebenen). + resources: + - title: Godot Docs · Collision layers and masks + url: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks + + - id: fall-3 + type: bug + title: Fehlerkiste, Fall 3 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Alles im Spiel wirkt größer, und das Prüfmuster ist ungleichmäßig: Streifen sind verschieden breit, im Schachbrett sind manche Reihen doppelt so hoch wie andere (rechts im Bild, links der saubere Stand). Finde die Zeile in `player.tscn` und korrigiere sie. + + ![Prüfmuster zweimal: links klein mit gleichmäßigem Schachbrett und gleich breiten Streifen, rechts anderthalbfach so groß mit ungleich breiten Streifen und unregelmäßigem Schachbrett](/media/godot/werkstatt-messen/fall-zoom@2x.png "Godot 4.7.2, links sauberer Stand, rechts nach Fall 3, beide vierfach vergrößert") + lang: ini + code: | + [node name="Feet" type="CollisionShape2D" parent="." unique_id=1982390300] + position = Vector2(0, -3) + shape = SubResource("feet") + + [node name="Camera2D" type="Camera2D" parent="." unique_id=380230576] + zoom = Vector2(1.5, 1.5) + + [node name="Sprite" type="AnimatedSprite2D" parent="." unique_id=942859769] + sprite_frames = SubResource("SpriteFrames_7ahtn") + lines: [6] + fix: + - line: 6 + answers: + - zoom = Vector2(1, 1) + - zoom = Vector2(1.0, 1.0) + explanation: | + Ein Zoom von 1,5 verteilt jedes Pixel des Musters auf anderthalb Pixel des 320 × 180-Bilds. Weil es keine halben Pixel gibt, werden manche Pixel zu einem, andere zu zwei. Im Testgelände hat die Kamera keinen eigenen Zoom; der korrigierte Wert ist der Standard und verschwindet beim Speichern aus der Datei. Ob dein Spiel einen ganzzahligen Zoom bekommt, entscheidest du; warum nicht ganzzahlige Faktoren Pixelart verzerren, steht in [Pixelraster und Skalierung](/godot/pixelraster). + resources: + - title: Godot Docs · Camera2D.zoom + url: https://docs.godotengine.org/en/stable/classes/class_camera2d.html#class-camera2d-property-zoom + + - id: fall-4 + type: bug + title: Fehlerkiste, Fall 4 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Nach dem Loslassen gleitet die Figur lange weiter. Mit `debug_log` zeigt das Log nach dem Loslassen in Tick 60: + + ```text + 59 speed 90.0 + 60 speed 89.5 + 61 speed 89.0 + 62 speed 88.5 + ``` + + Finde die Zeile in `data/player_stats.tres` und korrigiere sie auf den Platzhalter aus `movement_stats.gd`. + lang: ini + code: | + [gd_resource type="Resource" script_class="MovementStats" format=3 uid="uid://c328erhiur8b8"] + + [ext_resource type="Script" uid="uid://bwctjxhdkev23" path="res://movement_stats.gd" id="1"] + + [resource] + script = ExtResource("1") + friction = 30.0 + lines: [7] + fix: + - line: 7 + answers: + - friction = 900.0 + - friction = 900 + explanation: | + Das Tempo sinkt pro Tick um 0,5 px/s, also ist `friction × delta` = 0,5 und `friction` = 30. Mit dem Platzhalter 900 wären es 15 pro Tick. Gemessen mit Godot 4.7.2: Mit 30 stand die Figur nach 180 Ticks, mit 900 nach 6. Die Datei enthält sonst keine Werte, weil alle dem Standard im Skript entsprechen; die korrigierte Zeile verschwindet beim Speichern im Editor wieder. Rechne aus dem Log zurück, bevor du in Dateien suchst: Die Zahl pro Tick verrät den Wert. + resources: + - title: Godot Docs · Vector2.move_toward + url: https://docs.godotengine.org/en/stable/classes/class_vector2.html#class-vector2-method-move-toward + + - id: fall-5 + type: explain + title: Fehlerkiste, Fall 5 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Die Sammelstücke verschwinden, der Ton spielt, aber der Zähler im HUD bleibt bei 0. Die Ausgabe ist leer. So endet `main.tscn` jetzt. Erkläre, was fehlt, warum es keine Fehlermeldung gibt und wie du es im Editor siehst und behebst. + lang: ini + code: | + [node name="Messpunkt" type="Node" parent="." unique_id=427264249 node_paths=PackedStringArray("player", "label")] + script = ExtResource("8_kek77") + player = NodePath("../Objects/Player") + label = NodePath("../HUD/Messanzeige") + + [connection signal="collected" from="Objects/Player" to="PickupSound" method="_on_player_collected"] + points: + - Die Verbindung von `collected` an `HUD.show_total` fehlt; nur die zu `PickupSound` ist übrig, deshalb spielt der Ton. + - Ein Signal ohne Empfänger ist kein Fehler; `emit()` läuft durch, und `show_total()` wird nie aufgerufen. + - Im Signale-Dock des Players fehlt unter `collected` der Eintrag für `HUD`. + - Behoben wird es durch die Verbindung im Signale-Dock oder `git restore .`; in der Datei steht danach wieder eine `[connection]`-Zeile. + explanation: | + `player.gd` sendet `collected`, aber in `main.tscn` ist nur noch `PickupSound` verbunden. Ein Signal ohne Verbindung ist erlaubt, Godot meldet nichts, und `show_total()` läuft nie. Deshalb ist die Ausgabe leer, während Pickups verschwinden und der Ton spielt. Im Signale-Dock siehst du unter `collected` nur einen Empfänger. Die Zeile `[connection signal="collected" from="Objects/Player" to="HUD" method="show_total"]` kommt mit der neuen Verbindung oder mit `git restore .` zurück. Der Fall zeigt, warum ein Symptom ohne Fehlermeldung oft auf eine fehlende Verbindung oder einen falschen Wert hindeutet, nicht auf kaputten Code. + resources: + - title: Godot Docs · Using signals + url: https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html + + - id: fall-6 + type: choice + title: Fehlerkiste, Fall 6 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Steht die Figur unterhalb der Kiste, also vor ihr, verdeckt die Kiste trotzdem ihr Sprite (rechts im Bild, links der saubere Stand). Im Inspektor steht *Z Index* bei Figur und Kiste auf 0. Welche Einstellung erklärt das? + + ![Zwei Spielausschnitte, sechsfach vergrößert: links steht die Figur mit einem Feld des Testbogens vor der braunen Kiste und verdeckt sie teilweise, rechts an derselben Stelle verdeckt die Kiste die Figur bis auf einen Streifen unten](/media/godot/werkstatt-messen/fall-ysort@2x.png "Godot 4.7.2, links sauberer Stand, rechts nach Fall 6, Figur bei (40, 14), Kiste bei (40, 10)") + options: + - text: "`Objects` hat `y_sort_enabled` aus; ohne Y-Sortierung zeichnet Godot die Kinder in Baumreihenfolge, und `Crate` steht nach `Player`." + correct: true + why: In `main.tscn` fehlt die Zeile `y_sort_enabled = true` am Node `Objects`. Ohne sie gilt die Baumreihenfolge, unabhängig von der Position. + - text: Der Ursprung der Figur liegt in der Mitte statt an den Füßen. + correct: false + why: Ein falscher Ursprung verschiebt den Punkt, an dem sortiert wird, aber nur, wenn überhaupt sortiert wird. Hier steht die Figur 4 Pixel tiefer als die Kiste und wird trotzdem verdeckt. + - text: Die Kiste hat einen höheren `z_index` als die Figur. + correct: false + why: Das gäbe dasselbe Bild, laut Inspektor steht *Z Index* aber bei beiden auf 0. Prüfen solltest du es trotzdem, unter Ordering. + explanation: | + Mit `y_sort_enabled` am gemeinsamen Elternknoten zeichnet Godot die Kinder nach ihrer y-Position: Wer weiter unten steht, wird später gezeichnet und liegt vorn. Ohne gilt die Reihenfolge im Szenen-Dock, und `Crate` kommt nach `Player`. Prüfen lässt sich das im Inspektor von `Objects` unter Ordering und im Diff von `main.tscn`, wo die Zeile `y_sort_enabled = true` fehlt. Wie das mit Tiles zusammenspielt, steht in [Level aus Tiles](/godot/level-tilemap). + resources: + - title: Godot Docs · CanvasItem.y_sort_enabled + url: https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem-property-y-sort-enabled + + - id: fall-7 + type: bug + title: Fehlerkiste, Fall 7 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Beim Einsammeln ist der Ton nicht zu hören. Das Pickup verschwindet, der Zähler zählt. Im Audio-Panel steht der Regler des Busses `SFX` ganz unten (rechts im Bild). Finde die Zeile in `default_bus_layout.tres` und korrigiere sie. + + ![Audio-Panel mit den Bussen Master und SFX; beim Master steht der Lautstärkeregler auf 0 dB, bei SFX ganz unten unter −72 dB](/media/godot/werkstatt-messen/fall-bus@2x.png "Godot 4.7.2, Audio-Panel nach Fall 7; SFX ist der Platzhaltername aus dem Audio-Modul") + lang: ini + code: | + [gd_resource type="AudioBusLayout" format=3] + + [resource] + bus/1/name = &"SFX" + bus/1/solo = false + bus/1/mute = false + bus/1/bypass_fx = false + bus/1/volume_db = -80.0 + bus/1/send = &"Master" + lines: [8] + fix: + - line: 8 + answers: + - bus/1/volume_db = 0.0 + - bus/1/volume_db = 0 + explanation: | + −80 dB sind ein Zehntausendstel der Amplitude, praktisch stumm. `PickupSound` spielt, sein Bus lässt aber nichts durch. Gemessen mit Godot 4.7.2: `AudioServer.get_bus_volume_db(1)` lieferte `-80.0`, `is_bus_mute(1)` blieb `false`; der Knopf M (Stummschalten) ist also nicht die Ursache. 0 dB ist der Stand des Testgeländes; welche Lautstärke dein Bus bekommt, entscheidest du beim Abmischen. + resources: + - title: Godot Docs · Audio buses + url: https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html + + - id: fall-8 + type: output + title: Fehlerkiste, Fall 8 + prompt: | + Öffne diese Übung erst nach deiner Diagnose. Symptom: Die Figur bewegt sich in gröberen Schritten, und sie reagiert träger. Du startest das Testgelände mit `--fixed-fps 60`, also mit genau einer Sekunde Spielzeit pro 60 Bilder, und liest den Tick in der Messanzeige vor und nach 60 Bildern. Um wie viel ist er gestiegen? + lang: ini + code: | + [physics] + + common/physics_ticks_per_second=30 + expected: | + 30 + accept: + - 30 Ticks + explanation: | + Die Fehlerkiste hat in `project.godot` die Physik auf 30 Ticks pro Sekunde gesetzt; im Testgelände fehlt der Abschnitt, also gilt der Standard 60. Mit 30 Ticks legt die Figur bei 90 px/s pro Tick 3 statt 1,5 Pixel zurück und reagiert auf Eingaben nur halb so oft. Gemessen mit Godot 4.7.2: 30 Ticks in 60 Bildern statt 60, und die Figur stand 3 statt 6 Ticks nach dem Loslassen. Unter *Projekteinstellungen → Physik → Allgemein* siehst du den Wert als „Physik-Ticks pro Sekunde“. + resources: + - title: Godot Docs · Engine.physics_ticks_per_second + url: https://docs.godotengine.org/en/stable/classes/class_engine.html#class-engine-property-physics-ticks-per-second + + - id: pruefstand-aufbauen + type: practice + title: Prüfstand aufbauen und die Kiste öffnen + prompt: | + Ergänze das Testgelände um den Prüfstand aus der Lektion: Prüfmuster als `Sprite2D`, Messanzeige und Messpunkt; `PickupSound` und den Bus `SFX` hast du aus dem Audio-Modul. Prüfe den Messpunkt mit einer Vorhersage, committe, leg die Fehlerkiste unter `~/werkstatt/fehlerkiste.gd` ab und öffne sie einmal. + lang: text + solution: | + Szenen-Dock von main.tscn: + Objects/Pruefmuster Sprite2D, Texture test_assets/pruefmuster.png + HUD/Messanzeige Label, Theme Overrides → Font Sizes 8 + PickupSound aus dem Audio-Modul, Bus SFX + Messpunkt Node, messpunkt.gd, + Player = Objects/Player, Label = HUD/Messanzeige + + Vorhersage mit time_scale 0.25 am Messpunkt: + 36 Ticks bis 90 px/s, Anzeige danach "Tempo 90.0 px/s" + + $ git add -A && git commit -m "Prüfstand für die Werkstatt" + $ git status --short + $ godot --headless --path . -s ~/werkstatt/fehlerkiste.gd + Ein Fehler ist eingebaut. Die Nummer steht in …/godot/app_userdata/Testgelände/fehlerkiste.txt + $ git status --short + M + checklist: + - Das Prüfmuster steht im Spiel, die Messanzeige zeigt Tempo und Tick. + - Im Reiter Monitore gibt es die Gruppe „Testgelände“ mit „Gesammelt“, und der Wert steigt beim Einsammeln. + - Deine Vorhersage für einen geänderten Wert steht im Protokoll, bevor du gemessen hast, und die Messung daneben. + - "`git status` war vor dem Öffnen der Kiste leer und zeigt danach genau eine geänderte Datei." + - Du hast die Kiste bei geschlossenem Editor geöffnet und `fehlerkiste.txt` noch nicht angesehen. + explanation: | + Der Prüfstand macht die Fehler der Kiste sichtbar: Ohne Prüfmuster fällt ein Importfehler kaum auf, ohne Ton auf eigenem Bus gibt es keinen Busfehler, ohne Messanzeige siehst du den Physik-Takt nicht. Die Vorhersage prüft den Messpunkt selbst, bevor du ihm Messungen anvertraust. Eine geänderte Datei nach dem Öffnen ist das Zeichen, dass die Kiste gearbeitet hat; welche, liest du erst nach deiner Diagnose. Fehlt eine Voraussetzung, etwa das Prüfmuster, wählt die Kiste einen anderen Fehler. + resources: + - title: Godot Docs · Custom performance monitors + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html + - title: Godot Docs · Command line tutorial · Running a script + url: https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#running-a-script + + - id: korrekturprotokoll + type: practice + title: Acht Fälle im Korrekturprotokoll + prompt: | + Arbeite die Fehlerkiste durch, bis du alle acht Fälle einmal gefunden hast, und führe dabei `KORREKTUREN.md` nach der Vorlage aus der Lektion. Ergänze `EINSTELLUNGEN.md` um jede Einstellung des Prüfstands, die vom Standard abweicht. + lang: prose + checklist: + - "`KORREKTUREN.md` hat acht Einträge, jeder mit Symptom, Vermutung, Prüfung, Ursache, Korrektur, Beleg und Dauer." + - Jedes Symptom ist eine Beobachtung, keine Diagnose. + - Bei jedem Eintrag steht die Vermutung vor der Prüfung, und bei mindestens einem Fall ist eine Vermutung als falsch markiert, falls es eine gab. + - Jeder Beleg ist eine Diff-Zeile, ein Log-Ausschnitt oder ein Screenshot. + - "`EINSTELLUNGEN.md` nennt für jede Abweichung im Prüfstand Datei oder Dock, und übernommene Kurswerte sind als Platzhalter markiert." + - Nach jedem Fall war `git status` nach `git restore .` leer. + hints: + - Wiederholt sich eine Nummer, öffne die Kiste noch einmal; mit `-- 4` am Ende baust du einen bestimmten Fall gezielt ein, wenn nur noch einer fehlt. + - Notiere die Dauer bis zur Ursache; beim zweiten Durchgang derselben Kategorie siehst du, was du gelernt hast. + explanation: | + Das Protokoll ist der Teil, den ein Team von dir sieht. Es trennt Beobachtung und Deutung und macht jeden Fund mit einem Beleg nachprüfbar. Falsche Vermutungen gehören hinein, denn sie zeigen, welche Stellen du schon ausgeschlossen hast. `EINSTELLUNGEN.md` beantwortet die Frage, die in fremden Projekten am meisten Zeit kostet: Was weicht hier vom Standard ab, und warum. Beide Dateien sind Teil des Abschlussprojekts. + resources: + - title: Godot Docs · Overview of debugging tools + url: https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html diff --git a/content/godot/projects.yaml b/content/godot/projects.yaml new file mode 100644 index 0000000..7052476 --- /dev/null +++ b/content/godot/projects.yaml @@ -0,0 +1,212 @@ +projects: + - id: projektkarte-demo + title: Ein fremdes Projekt kartieren + level: 1 + hours: 5 + summary: Du nimmst ein Godot-Projekt, das du nicht geschrieben hast – die offizielle JRPG-Demo – und zeichnest eine Karte, mit der du dich darin zurechtfindest. Danach änderst du drei Werte und sagst vorher, was passiert. Lesen und Notieren gehen auch unterwegs. + brief: | + Ein Projekt, das ein Coding-Agent baut, ist für dich zuerst ein fremdes Projekt. Die Fertigkeit, die du hier übst, ist dieselbe: Du findest heraus, wo was liegt, ohne jede Zeile zu lesen. Als Übungsobjekt dient die Demo [Role Playing Game](https://github.com/godotengine/godot-demo-projects/tree/master/2d/role_playing_game) aus den offiziellen Godot-Demos. Sie ist für Godot 4.7 gepflegt, hat Bewegung auf einem Raster, Dialoge und einen Kampfbildschirm, also genug Szenen, Signale und Ressourcen, um sich darin zu verlaufen. + + Lade das Repository als ZIP oder klone es (`git clone https://github.com/godotengine/godot-demo-projects.git`) und öffne nur den Ordner `2d/role_playing_game` im Projektmanager. Ändere nichts, bevor die Karte steht. + + Grundlage sind [Der Editor als Werkstatt](/godot/editor-anatomie), [Szenenbäume lesen](/godot/szenenbaum), [Instanzen und Ressourcen](/godot/instanzen-ressourcen) und [Ein fremdes Projekt erkunden](/godot/projekt-erkunden). + skills: [editor, szenenbaum, signale, ressourcen, remote-baum] + steps: + - id: einstellungen-lesen + title: project.godot lesen + detail: Notiere Hauptszene, Autoloads, Eingabeaktionen, Renderer und Fenstereinstellungen. Lies dafür die Datei selbst, nicht nur die Projekteinstellungen; was dort nicht steht, ist der Standard. + - id: hauptszene-baum + title: Den Baum der Hauptszene aufzeichnen + detail: Zeichne den Baum der Hauptszene bis zur zweiten Ebene ab und markiere, welche Nodes Instanzen anderer Szenen sind und welche ein Skript tragen. + - id: signale-verfolgen + title: Drei Signale verfolgen + detail: Wähle drei Verbindungen im Signale-Dock oder im `[connection]`-Teil der `.tscn` und schreib zu jeder auf, wer sendet, wer empfängt und was danach im Spiel passiert. + - id: ressourcen-finden + title: Geteilte Ressourcen finden + detail: Suche Ressourcen, die mehrere Szenen benutzen (Texturen, `.tres`-Dateien), und notiere, was sich überall ändern würde, wenn du sie bearbeitest. + - id: laufzeit-baum + title: Den Baum zur Laufzeit vergleichen + detail: Starte das Spiel, wechsle im Szene-Dock auf „Remote“ und vergleiche mit deiner Zeichnung. Notiere Nodes, die erst zur Laufzeit entstehen. + - id: drei-werte + title: Drei Werte ändern und vorhersagen + detail: Such drei exportierte Werte oder Ressourcenwerte aus. Schreib vor jeder Änderung auf, was du erwartest, ändere nur diesen einen Wert, beobachte und notiere das Ergebnis. Danach den alten Wert wiederherstellen. + - id: karte-schreiben + title: Die Karte schreiben + detail: Eine Seite `KARTE.md`, mit der eine zweite Person in fünf Minuten die Stelle findet, an der der Dialog startet. Prüfe die Karte, indem du nach einer Pause eine Frage nur mit ihr beantwortest. + stretch: + - Dieselbe Karte für die Demo `2d/isometric` anlegen und vergleichen, welche Fragen dort schwerer zu beantworten sind. + - Einem Coding-Agenten die Karte geben und prüfen, ob er damit eine kleine Änderung ohne Rückfrage an der richtigen Stelle macht. + portfolio: | + Eine Projektkarte zeigt, dass du dich in fremdem Code zurechtfindest, und das braucht jedes Team, das an einem gemeinsamen Projekt arbeitet. Zeig die Zeichnung des Baums und die drei Vorhersagen mit Ergebnis, auch die falschen. + + - id: asset-pipeline + title: Die eigene Asset-Pipeline vom Zeichenprogramm ins Spiel + level: 2 + hours: 8 + summary: Du legst fest und dokumentierst, wie deine Bilder und Töne vom Zeichenprogramm oder von REAPER ins Testgelände kommen – Export, Ordner, Import-Voreinstellungen, Ursprung, Animation, Bus – und prüfst die Kette mit einem Prüfmuster und einer Änderung am Original. + brief: | + Wer Assets von Hand und nach Gefühl importiert, findet nach drei Wochen unscharfe Sprites, verrutschte Füße und Töne auf dem falschen Bus. Eine Pipeline ist eine kurze, aufgeschriebene Abfolge, die jedes Asset gleich durchläuft. Welche Auflösung, welche Rastergröße und welches Zeichenprogramm du verwendest, entscheidest du; dieses Projekt sorgt dafür, dass deine Entscheidungen festgehalten und überprüfbar sind. + + Arbeite im [Testgelände](/godot/projekt-erkunden). Grundlage sind [Pixelraster und Skalierung](/godot/pixelraster), [Eigene Sprites importieren](/godot/sprites-importieren), [Animationen zuordnen](/godot/animationen), [Audio zuordnen](/godot/audio) und [Testversionen exportieren und sichern](/godot/export-sichern). + skills: [import, pixelart, spritesheet, animation, audio, git] + steps: + - id: ordner-festlegen + title: Ordner und Namen festlegen + detail: Lege fest, wo Originale (`.aseprite`, `.kra`, `.rpp`) und wo exportierte Dateien liegen, und wie Dateien heißen. Originale gehören nicht in einen Ordner, den Godot importiert. + - id: pruefmuster + title: Mit dem Prüfmuster beginnen + detail: Exportiere einen Testbogen mit Rasterlinien und Frame-Nummern und importiere ihn. Im Spiel müssen alle Linien gleich breit und scharf sein, und jeder Frame zeigt genau seine Nummer. + - id: import-voreinstellung + title: Import-Voreinstellung festhalten + detail: Lege die Import-Einstellungen für Sprites als Standard fest und schreib in `ASSETS.md`, welche Werte du gewählt hast und warum. + - id: eigenes-sprite + title: Ein eigenes Sprite mit Ursprung + detail: Ein eigenes Sprite ersetzt den Platzhalter der Figur. Der Ursprung sitzt dort, wo du ihn für die Y-Sortierung brauchst; prüfe es, indem die Figur hinter und vor der Kiste vorbeiläuft. + - id: eigene-animation + title: Eine eigene Animation + detail: Eine Animation aus deinem Sheet läuft im Spiel. Notiere FPS und Frameanzahl und prüfe, ob die Füße bei der Platzhalter-Geschwindigkeit rutschen. + - id: eigener-ton + title: Ein eigener Ton auf einem eigenen Bus + detail: Ein eigener kurzer Ton spielt beim Einsammeln auf einem Bus, den du angelegt hast, und wird nicht abgeschnitten. + - id: original-aendern + title: Die Kette einmal durchlaufen + detail: Ändere ein Original, exportiere neu und prüfe, dass Godot neu importiert und das Spiel die Änderung zeigt, ohne dass du im Editor etwas einstellen musst. + - id: pipeline-commit + title: Pipeline sichern + detail: Originale, Exporte, `.import`-Dateien und `ASSETS.md` liegen in Git; ein Commit pro Schritt mit sprechender Nachricht. + stretch: + - Ein kleines Skript oder einen Exportbefehl deines Zeichenprogramms, das alle Sheets mit denselben Einstellungen exportiert, von einer KI schreiben lassen und prüfen. + - Die Pipeline einer zweiten Person geben und beobachten, wo sie hängen bleibt. + portfolio: | + Eine dokumentierte Pipeline mit Prüfmuster zeigt, dass du Technik und Gestaltung zusammenbringst – die Kernaufgabe von Technical Artists. Zeig `ASSETS.md`, das Prüfmuster im Spiel und einen Commit, in dem eine Änderung am Original ohne Handarbeit im Spiel ankommt. + + - id: level-aus-tiles + title: Ein kleiner Level aus eigenen Tiles + level: 2 + hours: 10 + summary: Du baust im Testgelände einen kleinen Level aus eigenen Tiles mit Boden, Wänden und hohen Objekten, mit Kollision und Y-Sortierung, misst Laufwege und exportierst eine Testversion, die jemand anderes spielt. + brief: | + Ein Level ist die Stelle, an der Assets, Kollision, Zeichenreihenfolge und Bewegung zusammenkommen. Fehler zeigen sich hier zuerst: eine Wand, durch die man läuft, ein Baum, der über der Figur hängt, Nähte zwischen Tiles. Wie der Level aussieht und wie er sich spielt, entscheidest du. Der Kurs gibt dir keine Vorlage und keine Größe vor. + + Grundlage sind [Kollisionen und Trefferzonen](/godot/kollision-ebenen), [2D-Level aus Tiles](/godot/level-tilemap), [Eingabeaktionen und Parameter](/godot/aktionen-parameter) und [Testversionen exportieren und sichern](/godot/export-sichern). + skills: [tileset, tilemaplayer, kollision, y-sortierung, export] + steps: + - id: tileset-anlegen + title: TileSet aus eigenen Tiles + detail: Ein Atlas aus deinem Tile-Sheet mit Tilegröße, Abstand und Rand so, wie du sie exportiert hast. Notiere die Werte. + - id: ebenen + title: Ebenen trennen + detail: Mindestens Boden und Wände als eigene `TileMapLayer`. Schreib auf, warum ein Tile auf welcher Ebene liegt. + - id: kollision-tiles + title: Kollision auf Tiles + detail: Wände haben Kollisionsformen auf einem Physik-Layer, den die Maske der Figur enthält. Prüfe mit *Debuggen → Sichtbare Collision-Shapes*, dass Form und Bild zusammenpassen. + - id: hohe-objekte + title: Hohe Objekte mit Y-Sortierung + detail: Mindestens ein hohes Objekt, hinter und vor dem die Figur vorbeilaufen kann, ohne falsch überdeckt zu werden. + - id: naehte-pruefen + title: Auf Nähte prüfen + detail: Lauf mit der Kamera über den Level und prüfe bei ganzzahliger Skalierung auf Linien zwischen Tiles. Notiere, was du findest und was du dagegen getan hast. + - id: laufwege-messen + title: Laufwege messen + detail: Miss mit dem Log der Figur, wie lange der längste Weg durch den Level bei den Platzhalterwerten dauert. Sag vorher, wie sich die Zeit bei einer anderen Höchstgeschwindigkeit ändert, und prüfe es. + - id: test-export + title: Testversion exportieren + detail: Exportiere eine Testversion für Linux oder Windows und lass eine andere Person spielen. Notiere, wo sie hängen bleibt, ohne einzugreifen. + - id: level-commit + title: Stand sichern + detail: Level, TileSet und Originale der Tiles in Git; die Testversion mit einem Tag markiert. + stretch: + - Terrains für Boden und Wände einrichten und vergleichen, wie schnell sich der Level danach ändern lässt. + - Sammelstücke als Szenen-Tiles setzen. + portfolio: | + Ein kleiner, sauber gebauter Level mit Beobachtungsnotizen einer Testperson zeigt, dass du prüfst, was du baust. Zeig ein kurzes Video mit sichtbaren Kollisionsformen und die Notizen der Testperson. + + - id: abschluss-testgelaende + title: "Abschlussprojekt: Ein KI-Prototyp in deinen Händen" + capstone: true + level: 3 + hours: 30 + summary: Ein kleines Studio hat seinen Prototyp mit einem Coding-Agenten gebaut, und niemand im Team traut sich an den Editor. Du übernimmst das Testgelände als Stellvertreter, setzt eigene Assets ein, findest vorbereitete Fehler, stimmst Werte messbar ab und lieferst eine spielbare Testversion mit Einstellungs- und Korrekturprotokoll. + brief: | + ## Auftrag + + Ein Studio mit vier Leuten in Köln arbeitet an einem Pixelart-Spiel in der Draufsicht. Die Programmierung hat das Team weitgehend einem Coding-Agenten überlassen. Der Prototyp läuft, aber: + + - Die Zeichnerin bekommt ihre Sprites nicht sauber ins Spiel: Mal sind sie unscharf, mal sitzen die Füße neben dem Schatten. + - Auf dem Steam Deck sehen die Pixel anders aus als am Monitor, und niemand weiß, warum. + - Werte wie Tempo und Bremsweg stehen an drei Stellen, und keiner weiß, welche gilt. + - Fehler werden behoben, aber niemand schreibt auf, was die Ursache war, also kommen sie wieder. + + Gesucht ist jemand, der den Editor beherrscht: Projekte liest, Assets sauber einbindet, Fehler eingrenzt und alles so dokumentiert, dass das Team ohne Rückfrage weiterarbeiten kann. Als Stellvertreter für den Prototyp dient dein [Testgelände](/godot/projekt-erkunden) mit der [Fehlerkiste](/godot/werkstatt-messen). + + ## Was du lieferst + + 1. **Eine spielbare Szene** in Godot 4.7 auf Grundlage des Testgeländes: Figur, Boden und Wände aus Tiles, mindestens ein hohes Objekt mit Y-Sortierung, Sammelstücke mit Ton und Animation – alles mit deinen eigenen Assets. + 2. **`EINSTELLUNGEN.md`:** jede Einstellung, die vom Standard abweicht – Projekt, Import, Physik, Audio – mit Ort (Datei oder Dock) und einem Satz Begründung. + 3. **`KORREKTUREN.md`:** jeder gefundene Fehler mit Symptom, Vermutung, Prüfung, Ursache, Korrektur und Beleg (Log, Screenshot oder Commit). + 4. **`MESSUNGEN.md`:** mindestens vier Parameteränderungen mit Vorhersage vor der Änderung, Messung danach und Entscheidung. + 5. **Testversionen** für Linux und Windows, mit Tag im Repository. + 6. **Eine Git-Historie**, in der sich jede Korrektur einzeln nachvollziehen lässt. + + ## Technischer Rahmen + + Godot 4.7, GDScript, Stretch Mode `viewport`, Renderer Compatibility. Auflösung, Rastergröße, Aussehen, Klang und alle Werte entscheidest du; Platzhalter aus dem Kurs, die du übernimmst, kennzeichnest du in `EINSTELLUNGEN.md` als solche. Code und technisches Gerüst darf ein Coding-Agent liefern, auch nach deiner Vorgabe; du prüfst es im Editor und im Log. Das Projekt muss auf einem zweiten Rechner aus dem Repository heraus ohne Nacharbeit starten. + + Grundlage sind alle Module dieses Bereichs. + skills: [godot, editor, import, tilemap, kollision, audio, debugger, export, git, dokumentation] + acceptance: + - id: start-ohne-nacharbeit + text: Ein frischer Klon des Repositorys öffnet sich in Godot 4.7 ohne Fehler in der Ausgabe und startet die Hauptszene. + - id: eigene-assets + text: Figur, Tiles, Sammelstück und Ton stammen von dir; kein Platzhalter-Polygon und kein Platzhalter-Boden mehr im Spiel. + - id: pixel-sauber + text: Bei ganzzahliger Skalierung sind alle Pixel gleich groß und scharf, auch bei 1280 × 800; ein Screenshot je Auflösung liegt bei. + - id: kollision-sichtbar-richtig + text: Mit *Debuggen → Sichtbare Collision-Shapes* passen alle Formen zu den Bildern, die Figur läuft durch keine Wand und sammelt jedes Sammelstück ein. + - id: fehler-dokumentiert + text: Jeder Fehler aus der Fehlerkiste und jeder eigene Fund steht in `KORREKTUREN.md` mit Ursache und Beleg und hat einen eigenen Commit. + - id: messungen-belegt + text: "`MESSUNGEN.md` enthält mindestens vier Änderungen, bei denen die Vorhersage vor der Messung notiert ist; Abweichungen sind erklärt." + - id: einstellungen-vollstaendig + text: "`EINSTELLUNGEN.md` nennt jede Abweichung vom Standard in `project.godot`, den `.import`-Dateien und im Audio-Layout." + - id: testversionen + text: Linux- und Windows-Export starten auf einem Rechner ohne Godot; der Tag im Repository zeigt auf den exportierten Stand. + steps: + - id: ausgangslage + title: Ausgangslage festhalten + detail: Godot-Version, Stand des Testgeländes und ein erster Commit. Starte das Spiel und schreib in drei Sätzen auf, was du siehst. + - id: projektkarte + title: Projektkarte anlegen + detail: Wo liegen Hauptszene, Figur, Werte, Verbindungen? Die Karte gehört an den Anfang von `EINSTELLUNGEN.md`. + - id: fehlerkiste + title: Vorbereitete Fehler finden + detail: Bau die Fehler aus der Fehlerkiste ein, ohne nachzusehen, welche es sind. Finde jeden über sein Symptom und trag ihn in `KORREKTUREN.md` ein, bevor du ihn behebst. + - id: pipeline + title: Asset-Pipeline festlegen + detail: Ordner, Exporteinstellungen und Import-Voreinstellungen für deine Bilder und Töne, geprüft mit einem Testbogen. + - id: figur-einbinden + title: Eigene Figur einbinden + detail: Sprite mit Ursprung an der richtigen Stelle, Animationen nach Blickrichtung, Kollisionsform passend zum Bild. + - id: level-bauen + title: Boden und Wände aus Tiles + detail: TileSet, Ebenen, Kollision, Y-Sortierung; der Platzhalter-Boden verschwindet. + - id: ton-einbinden + title: Ton und Bus + detail: Eigene Töne auf eigenen Bussen, nichts wird beim Einsammeln abgeschnitten. + - id: werte-abstimmen + title: Werte messbar abstimmen + detail: Vier Änderungen mit Vorhersage, Messung und Entscheidung in `MESSUNGEN.md`. Werte stehen danach an genau einer Stelle. + - id: aufloesungen-pruefen + title: Auf drei Auflösungen prüfen + detail: Fenstergröße für 1920 × 1080, 2560 × 1440 und 1280 × 800 nachstellen, Screenshots ablegen und Ränder erklären. + - id: exportieren + title: Testversionen exportieren + detail: Linux und Windows, mit `--verbose` gestartet, Log ohne Fehler, Tag im Repository. + - id: frischer-klon + title: Frischer Klon als Abnahme + detail: Repository in einen neuen Ordner klonen, in Godot öffnen und alle Abnahmekriterien selbst abhaken. + stretch: + - Eine eigene Debug-Anzeige, die Tempo, Blickrichtung und aktuelle Animation zeigt und sich mit einer Taste ausblenden lässt. + - Einer zweiten Person nur `EINSTELLUNGEN.md` und `KORREKTUREN.md` geben und prüfen, ob sie einen der Fehler wiederfindet, wenn du ihn erneut einbaust. + portfolio: | + Zeig dieses Projekt als Probeaufgabe für Technical Art oder Technical Design: ein Video von zwei Minuten mit sichtbaren Kollisionsformen, dazu `KORREKTUREN.md` mit zwei ausgewählten Fehlern von Symptom bis Beleg. Im Gespräch wird gefragt, wie du einen Fehler gefunden hast, den du nicht kanntest, und wie du sicherstellst, dass er nicht wiederkommt. Antworte mit dem Weg, nicht mit der Lösung: Symptom, Vermutung, Prüfung, Ursache. diff --git a/public/media/godot/aktionen-parameter/eingabe-zuordnung.png b/public/media/godot/aktionen-parameter/eingabe-zuordnung.png new file mode 100644 index 0000000..9528245 Binary files /dev/null and b/public/media/godot/aktionen-parameter/eingabe-zuordnung.png differ diff --git a/public/media/godot/aktionen-parameter/remote-player.png b/public/media/godot/aktionen-parameter/remote-player.png new file mode 100644 index 0000000..cae124f Binary files /dev/null and b/public/media/godot/aktionen-parameter/remote-player.png differ diff --git a/public/media/godot/aktionen-parameter/spawner-inspektor.png b/public/media/godot/aktionen-parameter/spawner-inspektor.png new file mode 100644 index 0000000..23dacbb Binary files /dev/null and b/public/media/godot/aktionen-parameter/spawner-inspektor.png differ diff --git a/public/media/godot/aktionen-parameter/stats-inspektor.png b/public/media/godot/aktionen-parameter/stats-inspektor.png new file mode 100644 index 0000000..698a938 Binary files /dev/null and b/public/media/godot/aktionen-parameter/stats-inspektor.png differ diff --git a/public/media/godot/animationen/flip-h@2x.png b/public/media/godot/animationen/flip-h@2x.png new file mode 100644 index 0000000..e48f5ed Binary files /dev/null and b/public/media/godot/animationen/flip-h@2x.png differ diff --git a/public/media/godot/animationen/frames-auswaehlen.png b/public/media/godot/animationen/frames-auswaehlen.png new file mode 100644 index 0000000..b8ef301 Binary files /dev/null and b/public/media/godot/animationen/frames-auswaehlen.png differ diff --git a/public/media/godot/animationen/spriteframes-panel.png b/public/media/godot/animationen/spriteframes-panel.png new file mode 100644 index 0000000..83d8dc0 Binary files /dev/null and b/public/media/godot/animationen/spriteframes-panel.png differ diff --git a/public/media/godot/audio/audio-busse.png b/public/media/godot/audio/audio-busse.png new file mode 100644 index 0000000..cf135e8 Binary files /dev/null and b/public/media/godot/audio/audio-busse.png differ diff --git a/public/media/godot/audio/import-wav.png b/public/media/godot/audio/import-wav.png new file mode 100644 index 0000000..2a86eb3 Binary files /dev/null and b/public/media/godot/audio/import-wav.png differ diff --git a/public/media/godot/bildrate-simulation/monitore-zeit@2x.png b/public/media/godot/bildrate-simulation/monitore-zeit@2x.png new file mode 100644 index 0000000..1cd62fa Binary files /dev/null and b/public/media/godot/bildrate-simulation/monitore-zeit@2x.png differ diff --git a/public/media/godot/bildrate-simulation/physik-allgemein@2x.png b/public/media/godot/bildrate-simulation/physik-allgemein@2x.png new file mode 100644 index 0000000..cd94f85 Binary files /dev/null and b/public/media/godot/bildrate-simulation/physik-allgemein@2x.png differ diff --git a/public/media/godot/bildrate-simulation/takt-streifen@2x.png b/public/media/godot/bildrate-simulation/takt-streifen@2x.png new file mode 100644 index 0000000..3c3739b Binary files /dev/null and b/public/media/godot/bildrate-simulation/takt-streifen@2x.png differ diff --git a/public/media/godot/editor-anatomie/dateisystem@2x.png b/public/media/godot/editor-anatomie/dateisystem@2x.png new file mode 100644 index 0000000..16ab208 Binary files /dev/null and b/public/media/godot/editor-anatomie/dateisystem@2x.png differ diff --git a/public/media/godot/editor-anatomie/editor-ueberblick@2x.png b/public/media/godot/editor-anatomie/editor-ueberblick@2x.png new file mode 100644 index 0000000..ca603e8 Binary files /dev/null and b/public/media/godot/editor-anatomie/editor-ueberblick@2x.png differ diff --git a/public/media/godot/editor-anatomie/falsche-hauptszene@2x.png b/public/media/godot/editor-anatomie/falsche-hauptszene@2x.png new file mode 100644 index 0000000..5c7c96e Binary files /dev/null and b/public/media/godot/editor-anatomie/falsche-hauptszene@2x.png differ diff --git a/public/media/godot/editor-anatomie/inspektor-player@2x.png b/public/media/godot/editor-anatomie/inspektor-player@2x.png new file mode 100644 index 0000000..bde25f2 Binary files /dev/null and b/public/media/godot/editor-anatomie/inspektor-player@2x.png differ diff --git a/public/media/godot/editor-anatomie/pickup-inspektor@2x.png b/public/media/godot/editor-anatomie/pickup-inspektor@2x.png new file mode 100644 index 0000000..fd915f9 Binary files /dev/null and b/public/media/godot/editor-anatomie/pickup-inspektor@2x.png differ diff --git a/public/media/godot/editor-anatomie/stats-regler@2x.png b/public/media/godot/editor-anatomie/stats-regler@2x.png new file mode 100644 index 0000000..bd0671b Binary files /dev/null and b/public/media/godot/editor-anatomie/stats-regler@2x.png differ diff --git a/public/media/godot/export-sichern/export-preset-linux@2x.png b/public/media/godot/export-sichern/export-preset-linux@2x.png new file mode 100644 index 0000000..6afc272 Binary files /dev/null and b/public/media/godot/export-sichern/export-preset-linux@2x.png differ diff --git a/public/media/godot/export-sichern/export-vorlage-fehlt@2x.png b/public/media/godot/export-sichern/export-vorlage-fehlt@2x.png new file mode 100644 index 0000000..745397a Binary files /dev/null and b/public/media/godot/export-sichern/export-vorlage-fehlt@2x.png differ diff --git a/public/media/godot/export-sichern/exportvorlagen@2x.png b/public/media/godot/export-sichern/exportvorlagen@2x.png new file mode 100644 index 0000000..5f001f6 Binary files /dev/null and b/public/media/godot/export-sichern/exportvorlagen@2x.png differ diff --git a/public/media/godot/export-sichern/vcs-metadaten@2x.png b/public/media/godot/export-sichern/vcs-metadaten@2x.png new file mode 100644 index 0000000..82dc213 Binary files /dev/null and b/public/media/godot/export-sichern/vcs-metadaten@2x.png differ diff --git a/public/media/godot/fehler-eingrenzen/fehler-tab@2x.png b/public/media/godot/fehler-eingrenzen/fehler-tab@2x.png new file mode 100644 index 0000000..8575ffa Binary files /dev/null and b/public/media/godot/fehler-eingrenzen/fehler-tab@2x.png differ diff --git a/public/media/godot/fehler-eingrenzen/monitor-verwaist@2x.png b/public/media/godot/fehler-eingrenzen/monitor-verwaist@2x.png new file mode 100644 index 0000000..9aa9b8b Binary files /dev/null and b/public/media/godot/fehler-eingrenzen/monitor-verwaist@2x.png differ diff --git a/public/media/godot/fehler-eingrenzen/profiler@2x.png b/public/media/godot/fehler-eingrenzen/profiler@2x.png new file mode 100644 index 0000000..78c461b Binary files /dev/null and b/public/media/godot/fehler-eingrenzen/profiler@2x.png differ diff --git a/public/media/godot/fehler-eingrenzen/stacktrace@2x.png b/public/media/godot/fehler-eingrenzen/stacktrace@2x.png new file mode 100644 index 0000000..1f7c7ae Binary files /dev/null and b/public/media/godot/fehler-eingrenzen/stacktrace@2x.png differ diff --git a/public/media/godot/instanzen-ressourcen/bearbeitbare-kinder@2x.png b/public/media/godot/instanzen-ressourcen/bearbeitbare-kinder@2x.png new file mode 100644 index 0000000..a065b3e Binary files /dev/null and b/public/media/godot/instanzen-ressourcen/bearbeitbare-kinder@2x.png differ diff --git a/public/media/godot/instanzen-ressourcen/geteilte-form@2x.png b/public/media/godot/instanzen-ressourcen/geteilte-form@2x.png new file mode 100644 index 0000000..10ee268 Binary files /dev/null and b/public/media/godot/instanzen-ressourcen/geteilte-form@2x.png differ diff --git a/public/media/godot/instanzen-ressourcen/instanz-override@2x.png b/public/media/godot/instanzen-ressourcen/instanz-override@2x.png new file mode 100644 index 0000000..719648c Binary files /dev/null and b/public/media/godot/instanzen-ressourcen/instanz-override@2x.png differ diff --git a/public/media/godot/instanzen-ressourcen/kontextmenue-instanz@2x.png b/public/media/godot/instanzen-ressourcen/kontextmenue-instanz@2x.png new file mode 100644 index 0000000..c53a9f3 Binary files /dev/null and b/public/media/godot/instanzen-ressourcen/kontextmenue-instanz@2x.png differ diff --git a/public/media/godot/instanzen-ressourcen/nur-lesbar@2x.png b/public/media/godot/instanzen-ressourcen/nur-lesbar@2x.png new file mode 100644 index 0000000..056ebea Binary files /dev/null and b/public/media/godot/instanzen-ressourcen/nur-lesbar@2x.png differ diff --git a/public/media/godot/kollision-ebenen/fussform@2x.png b/public/media/godot/kollision-ebenen/fussform@2x.png new file mode 100644 index 0000000..3712bfb Binary files /dev/null and b/public/media/godot/kollision-ebenen/fussform@2x.png differ diff --git a/public/media/godot/kollision-ebenen/ganzkoerper@2x.png b/public/media/godot/kollision-ebenen/ganzkoerper@2x.png new file mode 100644 index 0000000..1d3f527 Binary files /dev/null and b/public/media/godot/kollision-ebenen/ganzkoerper@2x.png differ diff --git a/public/media/godot/kollision-ebenen/pickup-inspektor.png b/public/media/godot/kollision-ebenen/pickup-inspektor.png new file mode 100644 index 0000000..febfca9 Binary files /dev/null and b/public/media/godot/kollision-ebenen/pickup-inspektor.png differ diff --git a/public/media/godot/kollision-ebenen/pickup-maske-1.png b/public/media/godot/kollision-ebenen/pickup-maske-1.png new file mode 100644 index 0000000..ba7fc25 Binary files /dev/null and b/public/media/godot/kollision-ebenen/pickup-maske-1.png differ diff --git a/public/media/godot/kollision-ebenen/szene-warnung.png b/public/media/godot/kollision-ebenen/szene-warnung.png new file mode 100644 index 0000000..87e7676 Binary files /dev/null and b/public/media/godot/kollision-ebenen/szene-warnung.png differ diff --git a/public/media/godot/level-tilemap/naht@2x.png b/public/media/godot/level-tilemap/naht@2x.png new file mode 100644 index 0000000..6db9eac Binary files /dev/null and b/public/media/godot/level-tilemap/naht@2x.png differ diff --git a/public/media/godot/level-tilemap/pruefmuster@2x.png b/public/media/godot/level-tilemap/pruefmuster@2x.png new file mode 100644 index 0000000..3053507 Binary files /dev/null and b/public/media/godot/level-tilemap/pruefmuster@2x.png differ diff --git a/public/media/godot/level-tilemap/szene-tiles.png b/public/media/godot/level-tilemap/szene-tiles.png new file mode 100644 index 0000000..07cd546 Binary files /dev/null and b/public/media/godot/level-tilemap/szene-tiles.png differ diff --git a/public/media/godot/level-tilemap/tile-eigenschaften.png b/public/media/godot/level-tilemap/tile-eigenschaften.png new file mode 100644 index 0000000..bc7fef2 Binary files /dev/null and b/public/media/godot/level-tilemap/tile-eigenschaften.png differ diff --git a/public/media/godot/level-tilemap/tilemap-leiste.png b/public/media/godot/level-tilemap/tilemap-leiste.png new file mode 100644 index 0000000..970f012 Binary files /dev/null and b/public/media/godot/level-tilemap/tilemap-leiste.png differ diff --git a/public/media/godot/level-tilemap/tileset-atlas.png b/public/media/godot/level-tilemap/tileset-atlas.png new file mode 100644 index 0000000..c03db8d Binary files /dev/null and b/public/media/godot/level-tilemap/tileset-atlas.png differ diff --git a/public/media/godot/level-tilemap/walls-ysort.png b/public/media/godot/level-tilemap/walls-ysort.png new file mode 100644 index 0000000..a5f625d Binary files /dev/null and b/public/media/godot/level-tilemap/walls-ysort.png differ diff --git a/public/media/godot/level-tilemap/ysort-falsch@2x.png b/public/media/godot/level-tilemap/ysort-falsch@2x.png new file mode 100644 index 0000000..1c00b09 Binary files /dev/null and b/public/media/godot/level-tilemap/ysort-falsch@2x.png differ diff --git a/public/media/godot/level-tilemap/ysort-richtig@2x.png b/public/media/godot/level-tilemap/ysort-richtig@2x.png new file mode 100644 index 0000000..95994b5 Binary files /dev/null and b/public/media/godot/level-tilemap/ysort-richtig@2x.png differ diff --git a/public/media/godot/pixelraster/diagnose-mixel@2x.png b/public/media/godot/pixelraster/diagnose-mixel@2x.png new file mode 100644 index 0000000..f673f38 Binary files /dev/null and b/public/media/godot/pixelraster/diagnose-mixel@2x.png differ diff --git a/public/media/godot/pixelraster/diagnose-streifen@2x.png b/public/media/godot/pixelraster/diagnose-streifen@2x.png new file mode 100644 index 0000000..ac179fd Binary files /dev/null and b/public/media/godot/pixelraster/diagnose-streifen@2x.png differ diff --git a/public/media/godot/pixelraster/diagnose-weich@2x.png b/public/media/godot/pixelraster/diagnose-weich@2x.png new file mode 100644 index 0000000..db5931c Binary files /dev/null and b/public/media/godot/pixelraster/diagnose-weich@2x.png differ diff --git a/public/media/godot/pixelraster/diagnose-zoom@2x.png b/public/media/godot/pixelraster/diagnose-zoom@2x.png new file mode 100644 index 0000000..73243b9 Binary files /dev/null and b/public/media/godot/pixelraster/diagnose-zoom@2x.png differ diff --git a/public/media/godot/pixelraster/drehung@2x.png b/public/media/godot/pixelraster/drehung@2x.png new file mode 100644 index 0000000..ec30163 Binary files /dev/null and b/public/media/godot/pixelraster/drehung@2x.png differ diff --git a/public/media/godot/pixelraster/faktor-4-und-3-5@2x.png b/public/media/godot/pixelraster/faktor-4-und-3-5@2x.png new file mode 100644 index 0000000..7f9504a Binary files /dev/null and b/public/media/godot/pixelraster/faktor-4-und-3-5@2x.png differ diff --git a/public/media/godot/pixelraster/filter-linear-nearest@2x.png b/public/media/godot/pixelraster/filter-linear-nearest@2x.png new file mode 100644 index 0000000..6d15e1a Binary files /dev/null and b/public/media/godot/pixelraster/filter-linear-nearest@2x.png differ diff --git a/public/media/godot/pixelraster/kamera-zoom@2x.png b/public/media/godot/pixelraster/kamera-zoom@2x.png new file mode 100644 index 0000000..7c340a1 Binary files /dev/null and b/public/media/godot/pixelraster/kamera-zoom@2x.png differ diff --git a/public/media/godot/pixelraster/mixel@2x.png b/public/media/godot/pixelraster/mixel@2x.png new file mode 100644 index 0000000..30cb76b Binary files /dev/null and b/public/media/godot/pixelraster/mixel@2x.png differ diff --git a/public/media/godot/pixelraster/pruefmuster@2x.png b/public/media/godot/pixelraster/pruefmuster@2x.png new file mode 100644 index 0000000..8803287 Binary files /dev/null and b/public/media/godot/pixelraster/pruefmuster@2x.png differ diff --git a/public/media/godot/pixelraster/raender-1366x768@2x.png b/public/media/godot/pixelraster/raender-1366x768@2x.png new file mode 100644 index 0000000..85e4731 Binary files /dev/null and b/public/media/godot/pixelraster/raender-1366x768@2x.png differ diff --git a/public/media/godot/projekt-erkunden/debugger-zaehler@2x.png b/public/media/godot/projekt-erkunden/debugger-zaehler@2x.png new file mode 100644 index 0000000..e4b9619 Binary files /dev/null and b/public/media/godot/projekt-erkunden/debugger-zaehler@2x.png differ diff --git a/public/media/godot/projekt-erkunden/eingabe-zuordnung@2x.png b/public/media/godot/projekt-erkunden/eingabe-zuordnung@2x.png new file mode 100644 index 0000000..9cbb1c6 Binary files /dev/null and b/public/media/godot/projekt-erkunden/eingabe-zuordnung@2x.png differ diff --git a/public/media/godot/projekt-erkunden/ereignis-dialog@2x.png b/public/media/godot/projekt-erkunden/ereignis-dialog@2x.png new file mode 100644 index 0000000..e72d75d Binary files /dev/null and b/public/media/godot/projekt-erkunden/ereignis-dialog@2x.png differ diff --git a/public/media/godot/projekt-erkunden/remote-baum@2x.png b/public/media/godot/projekt-erkunden/remote-baum@2x.png new file mode 100644 index 0000000..2af82e4 Binary files /dev/null and b/public/media/godot/projekt-erkunden/remote-baum@2x.png differ diff --git a/public/media/godot/projekt-erkunden/remote-inspektor@2x.png b/public/media/godot/projekt-erkunden/remote-inspektor@2x.png new file mode 100644 index 0000000..c6d4f39 Binary files /dev/null and b/public/media/godot/projekt-erkunden/remote-inspektor@2x.png differ diff --git a/public/media/godot/projekt-erkunden/signale-dock@2x.png b/public/media/godot/projekt-erkunden/signale-dock@2x.png new file mode 100644 index 0000000..cd0d729 Binary files /dev/null and b/public/media/godot/projekt-erkunden/signale-dock@2x.png differ diff --git a/public/media/godot/sprites-importieren/diagnose-halbe-felder@2x.png b/public/media/godot/sprites-importieren/diagnose-halbe-felder@2x.png new file mode 100644 index 0000000..7a24d3a Binary files /dev/null and b/public/media/godot/sprites-importieren/diagnose-halbe-felder@2x.png differ diff --git a/public/media/godot/sprites-importieren/hframes-vergleich@2x.png b/public/media/godot/sprites-importieren/hframes-vergleich@2x.png new file mode 100644 index 0000000..a077b17 Binary files /dev/null and b/public/media/godot/sprites-importieren/hframes-vergleich@2x.png differ diff --git a/public/media/godot/sprites-importieren/import-dock.png b/public/media/godot/sprites-importieren/import-dock.png new file mode 100644 index 0000000..f064222 Binary files /dev/null and b/public/media/godot/sprites-importieren/import-dock.png differ diff --git a/public/media/godot/sprites-importieren/import-varianten@2x.png b/public/media/godot/sprites-importieren/import-varianten@2x.png new file mode 100644 index 0000000..c3c2d5d Binary files /dev/null and b/public/media/godot/sprites-importieren/import-varianten@2x.png differ diff --git a/public/media/godot/sprites-importieren/inspector-sprite2d.png b/public/media/godot/sprites-importieren/inspector-sprite2d.png new file mode 100644 index 0000000..8b9818f Binary files /dev/null and b/public/media/godot/sprites-importieren/inspector-sprite2d.png differ diff --git a/public/media/godot/sprites-importieren/ursprung@2x.png b/public/media/godot/sprites-importieren/ursprung@2x.png new file mode 100644 index 0000000..5b50cd3 Binary files /dev/null and b/public/media/godot/sprites-importieren/ursprung@2x.png differ diff --git a/public/media/godot/szenenbaum/hud-canvaslayer@2x.png b/public/media/godot/szenenbaum/hud-canvaslayer@2x.png new file mode 100644 index 0000000..39cfc27 Binary files /dev/null and b/public/media/godot/szenenbaum/hud-canvaslayer@2x.png differ diff --git a/public/media/godot/szenenbaum/hud-node2d@2x.png b/public/media/godot/szenenbaum/hud-node2d@2x.png new file mode 100644 index 0000000..66528da Binary files /dev/null and b/public/media/godot/szenenbaum/hud-node2d@2x.png differ diff --git a/public/media/godot/szenenbaum/reihenfolge-falsch@2x.png b/public/media/godot/szenenbaum/reihenfolge-falsch@2x.png new file mode 100644 index 0000000..6bd3c51 Binary files /dev/null and b/public/media/godot/szenenbaum/reihenfolge-falsch@2x.png differ diff --git a/public/media/godot/szenenbaum/reihenfolge-kopf@2x.png b/public/media/godot/szenenbaum/reihenfolge-kopf@2x.png new file mode 100644 index 0000000..f923a65 Binary files /dev/null and b/public/media/godot/szenenbaum/reihenfolge-kopf@2x.png differ diff --git a/public/media/godot/szenenbaum/reihenfolge-richtig@2x.png b/public/media/godot/szenenbaum/reihenfolge-richtig@2x.png new file mode 100644 index 0000000..da4788a Binary files /dev/null and b/public/media/godot/szenenbaum/reihenfolge-richtig@2x.png differ diff --git a/public/media/godot/szenenbaum/szene-main@2x.png b/public/media/godot/szenenbaum/szene-main@2x.png new file mode 100644 index 0000000..19475e5 Binary files /dev/null and b/public/media/godot/szenenbaum/szene-main@2x.png differ diff --git a/public/media/godot/werkstatt-messen/fall-bus@2x.png b/public/media/godot/werkstatt-messen/fall-bus@2x.png new file mode 100644 index 0000000..0e50d99 Binary files /dev/null and b/public/media/godot/werkstatt-messen/fall-bus@2x.png differ diff --git a/public/media/godot/werkstatt-messen/fall-import@2x.png b/public/media/godot/werkstatt-messen/fall-import@2x.png new file mode 100644 index 0000000..56f0c3b Binary files /dev/null and b/public/media/godot/werkstatt-messen/fall-import@2x.png differ diff --git a/public/media/godot/werkstatt-messen/fall-ysort@2x.png b/public/media/godot/werkstatt-messen/fall-ysort@2x.png new file mode 100644 index 0000000..a127fbe Binary files /dev/null and b/public/media/godot/werkstatt-messen/fall-ysort@2x.png differ diff --git a/public/media/godot/werkstatt-messen/fall-zoom@2x.png b/public/media/godot/werkstatt-messen/fall-zoom@2x.png new file mode 100644 index 0000000..7f68b7f Binary files /dev/null and b/public/media/godot/werkstatt-messen/fall-zoom@2x.png differ diff --git a/public/media/godot/werkstatt-messen/monitor-gesammelt@2x.png b/public/media/godot/werkstatt-messen/monitor-gesammelt@2x.png new file mode 100644 index 0000000..c619adc Binary files /dev/null and b/public/media/godot/werkstatt-messen/monitor-gesammelt@2x.png differ diff --git a/public/media/godot/werkstatt-messen/pruefstand@2x.png b/public/media/godot/werkstatt-messen/pruefstand@2x.png new file mode 100644 index 0000000..c39823b Binary files /dev/null and b/public/media/godot/werkstatt-messen/pruefstand@2x.png differ diff --git a/src/styles/content.css b/src/styles/content.css index 526bee1..2a169e3 100644 --- a/src/styles/content.css +++ b/src/styles/content.css @@ -16,7 +16,11 @@ .prose a.external::after, .external-link::after { content: '↗'; font-family: var(--mono); font-size: .8em; margin-left: 2px; } .prose blockquote { margin: 0; padding: 4px 0 4px 20px; border-left: 8px solid var(--ink); } .prose hr { border: 0; border-top: var(--border); } -.prose img { border: var(--border); } +.prose img { border: var(--border); display: block; max-width: 100%; height: auto; background: var(--white); } +/* Screenshots from public/media/: never wider than the column, a tap opens the full image for zooming. */ +.prose .figure { margin-left: 0; margin-right: 0; } +.figure-link { display: block; width: fit-content; max-width: 100%; box-shadow: var(--shadow-sm); } +.figure figcaption { margin-top: 10px; font-size: 15px; line-height: 1.5; color: var(--muted); } .compact.prose { font-size: 16px; } .compact.prose > * + * { margin-top: .75em; } diff --git a/tooling/content.test.ts b/tooling/content.test.ts index d3dadd8..5df4f05 100644 --- a/tooling/content.test.ts +++ b/tooling/content.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { join } from 'node:path'; -import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { formatIssues, lessonBlocks, loadContent } from './content.ts'; import { codeBlock, createMarkdown } from './markdown.ts'; @@ -127,6 +127,32 @@ describe('content', () => { const module = loadContent(undefined, join(import.meta.dirname, 'fixtures')).areas.beispiel.modules['alle-typen']; expect(module.toc.every((entry) => module.lesson.includes(`id="${entry.id}"`))).toBe(true); }); + it('renders images from public/media as sized figures and reports missing files and alt texts', () => { + const root = mkdtempSync(join(tmpdir(), 'learning-media-')); + try { + // Smallest valid PNG header: signature, IHDR length and type, then width 640 and height 360. + const png = Buffer.alloc(33); + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52]).copy(png); + png.writeUInt32BE(640, 16); + png.writeUInt32BE(360, 20); + const media = join(root, 'public'); + mkdirSync(join(media, 'media', 'kurs'), { recursive: true }); + writeFileSync(join(media, 'media', 'kurs', 'szene.png'), png); + writeFileSync(join(media, 'media', 'kurs', 'pixel@2x.png'), png); + const md = createMarkdown({ media }); + const figure = md.block('![Szenen-Dock mit drei Nodes](/media/kurs/szene.png "Der Baum der Testszene")'); + expect(figure).toBe('
Szenen-Dock mit drei Nodes (in voller Größe, neuer Tab)
Der Baum der Testszene
\n'); + expect(md.block('![Spielbild, doppelt aufgelöst](/media/kurs/pixel@2x.png)')).toContain('width="320" height="180"'); + expect(md.takeImageProblems()).toEqual([]); + md.block('Text mit ![](/media/kurs/szene.png) und ![Fehlt](/media/kurs/fehlt.png) und ![Außen](https://example.com/a.png)'); + expect(md.takeImageProblems()).toEqual([ + 'Bild /media/kurs/szene.png braucht einen Alt-Text, der beschreibt, was zu sehen ist', + 'Bild /media/kurs/fehlt.png fehlt: erlaubt sind PNG-Dateien unter public/media/', + 'Bild https://example.com/a.png fehlt: erlaubt sind PNG-Dateien unter public/media/', + ]); + expect(md.takeImageProblems()).toEqual([]); + } finally { rmSync(root, { recursive: true, force: true }); } + }); it('highlights GDScript without making it runnable', () => { const html = codeBlock('@export var windup := 12 # Frames\nfunc _ready() -> void:\n\t$Hitbox.monitoring = false\n\tplay(&"attack")', 'gdscript run'); expect(html).toContain('GDScript'); diff --git a/tooling/content.ts b/tooling/content.ts index ad64228..5739958 100644 --- a/tooling/content.ts +++ b/tooling/content.ts @@ -415,6 +415,7 @@ function loadModule(areaId: string, moduleId: string, report: Reporter, raw: Raw if (choices > exercises.length / 3) report.warn(where, `${choices} von ${exercises.length} Übungen sind Multiple Choice – mehr aktive Formate`); const noResources = exercises.filter((e) => !e.resources.length).length; if (noResources > exercises.length / 2) report.warn(where, `${noResources} Übungen ohne Doku-Link (resources)`); + for (const problem of md.takeImageProblems()) report.error(where, problem); return { id: moduleId, title: f.str('title'), summary: f.str('summary'), level: level(data.level, where, report), minutes: f.num('minutes', 60, 5, 600), goals: f.texts('goals').map((goal) => inline(goal)), lesson, toc, resources: resources(data.resources, where, report), exercises, lab, vm, bear: false, place, @@ -611,6 +612,7 @@ function loadArea(dir: string, raw: RawExercise[], sink: { errors: Issue[]; warn const page = (name: string) => existsSync(join(dir, name)) ? md.document(readFileSync(join(dir, name), 'utf8')).html : null; const cheatsheet = page('cheatsheet.md'); const career = page('career.md'); + for (const problem of md.takeImageProblems()) report.error('Bereich', problem); const moduleSummaries: ModuleSummary[] = tracks.flatMap((track) => track.modules).map((mid) => { const m = modules[mid]; diff --git a/tooling/markdown.ts b/tooling/markdown.ts index 901c2e2..530380f 100644 --- a/tooling/markdown.ts +++ b/tooling/markdown.ts @@ -1,9 +1,26 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { Marked, type Tokens } from 'marked'; import { escape, highlight, languageOf } from '../src/engine/highlight.ts'; import type { TocEntry } from '../src/content/types.ts'; // Build-time Markdown. Content is authored in this repository and therefore trusted. +const PUBLIC = join(import.meta.dirname, '..', 'public'); + +/** + * Width and height of a PNG under public/media/, in CSS pixels: a name ending in `@2x.png` holds twice as many + * pixels as it shows. Null if the file is missing or no PNG. + */ +function pngSize(root: string, src: string): { width: number; height: number } | null { + const file = join(root, src); + if (!/^\/media\/[\w\-/.@]+\.png$/.test(src) || src.includes('..') || !existsSync(file)) return null; + const head = readFileSync(file).subarray(0, 24); + if (head.toString('latin1', 1, 4) !== 'PNG') return null; + const density = Number(src.match(/@(\d)x\.png$/)?.[1] ?? 1); + return { width: Math.round(head.readUInt32BE(16) / density), height: Math.round(head.readUInt32BE(20) / density) }; +} + const CALLOUTS: Record = { NOTE: 'Hinweis', TIP: 'Tipp', WARNING: 'Achtung', INTERVIEW: 'Im Interview', PRAXIS: 'Aus der Praxis', MERKE: 'Merksatz', DEEP: 'Tiefer graben', }; @@ -39,13 +56,28 @@ export function codeBlock(text: string, info = ''): string { return `
${escape(label)}
${body}
`; } -export function createMarkdown() { +export function createMarkdown({ media = PUBLIC }: { media?: string } = {}) { let toc: TocEntry[] = []; const used = new Set(); + let imageProblems: string[] = []; + /** `![Alt](/media/…png "Caption")`: the size comes from the file, so the page does not jump while it loads. A tap opens the full image. */ + function image({ href, title, text }: Tokens.Image): string { + const size = pngSize(media, href); + if (!size) imageProblems.push(`Bild ${href} fehlt: erlaubt sind PNG-Dateien unter public/media/`); + if (!text.trim()) imageProblems.push(`Bild ${href} braucht einen Alt-Text, der beschreibt, was zu sehen ist`); + const img = `${escape(text)}`; + return `${img} (in voller Größe, neuer Tab)${title ? `
${escape(title)}
` : ''}`; + } const marked = new Marked({ gfm: true, renderer: { code({ text, lang }: Tokens.Code) { return codeBlock(text, lang ?? ''); }, + image, + // An image on its own line is a figure; its title becomes the caption. + paragraph(this: any, { tokens }: Tokens.Paragraph) { + if (tokens.length === 1 && tokens[0].type === 'image') return `
${image(tokens[0] as Tokens.Image)}
\n`; + return `

${this.parser.parseInline(tokens)}

\n`; + }, heading(this: any, { tokens, depth }: Tokens.Heading) { const html = this.parser.parseInline(tokens); const level = Math.min(depth + 0, 6); @@ -104,5 +136,11 @@ export function createMarkdown() { inline(source: string): string { return marked.parseInline(source, { async: false }) as string; }, + /** Missing files and alt texts of the images rendered since the last call. */ + takeImageProblems(): string[] { + const problems = imageProblems; + imageProblems = []; + return problems; + }, }; } diff --git a/vercel.json b/vercel.json index 6d5a848..5761c32 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,7 @@ "buildCommand": "pnpm build:production", "outputDirectory": "dist", "git": { "deploymentEnabled": { "**": false, "main": true } }, - "rewrites": [{ "source": "/((?!assets/|fonts/|pyodide/|vm/|icons/|sw\\.js|manifest\\.webmanifest).*)", "destination": "/index.html" }], + "rewrites": [{ "source": "/((?!assets/|fonts/|media/|pyodide/|vm/|icons/|sw\\.js|manifest\\.webmanifest).*)", "destination": "/index.html" }], "headers": [ { "source": "/(.*)",