[#_lesson_8_when_it_goes_wrong] == Lektion 8: Wenn etwas schiefgeht image::learn/08-when-it-goes-wrong.png[Die fertige Lektion, 480] Du hast ein Spiel geschrieben. Ab jetzt wirst du mehr Zeit damit verbringen, Programme zu reparieren, als sie zu schreiben, und das ist kein Zeichen, dass mit dir etwas nicht stimmt. Das ist die Arbeit. Der Unterschied zwischen jemandem, dem Programmieren Spaß macht, und jemandem, der es aufgibt, ist fast nie Talent; es ist die Frage, ob er gelernt hat zu lesen, was der Computer ihm sagt. In dieser Lektion gibt es also nichts Neues zu bauen. Es geht um die fünf Meldungen, die du für den Rest deines Lebens am häufigsten sehen wirst, was jede davon wirklich bedeutet, und um vier Wege, den Fehler dahinter aufzuspüren. Lass `dodge.singe` aus Lektion sieben offen, denn du wirst es mehrmals absichtlich kaputt machen. [#_two_kinds_of_stop] === Zwei Arten anzuhalten Singe hält aus zwei verschiedenen Gründen an, und es sagt das auf zwei verschiedene Arten. Wenn das Skript gar nicht erst startet, bekommst du diese Form: ---- Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock' ---- Lua liest deine ganze Datei, bevor es auch nur ein Wort davon ausführt, und das hier heißt, dass es aus dem Gelesenen nicht schlau wurde. Überhaupt nichts ist gelaufen. Wenn das Skript gut gestartet ist, aber später etwas schiefging, bekommst du stattdessen diese Form: ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped') stack traceback: dodge/dodge.singe:98: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- Das Spiel lief. Singe hat dein `onOverlayUpdate` aufgerufen, wie es das sechzigmal pro Sekunde tut, und diesmal kam der Aufruf nicht zurück. Beide Formen enthalten dieselben drei Dinge, und das sind genau die drei, die du willst: eine Datei, eine Zeile und eine Beschwerde. Lies sie in dieser Reihenfolge, jedes Mal. Die Zeilennummern in dieser Lektion stammen aus dem fertigen Listing von Lektion sieben. Liegen deine um eine oder zwei Zeilen daneben, dann tut es deine Datei auch, und nichts am Lesen eines Fehlers hängt davon ab, dass die Nummer stimmt. [#_the_ones_that_stop_it_before_it_starts] === Die, die es aufhalten, bevor es anfängt Das sind die leichten, und es sind auch die, die sich am schlimmsten anfühlen, weil der Bildschirm schwarz bleibt und es nichts zu sehen gibt. Geh in `updatePlaying` und lösch das Wort `then` aus der Zeile `if spawnTimer <= 0 then`. Speichere: ---- Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock' ---- Lua ist ans Ende der `if`-Zeile gekommen, wollte das Wort `then` und hat stattdessen den Anfang der nächsten Zeile gefunden -- das ist `spawnBlock()`, und daher kommt der Teil `near 'spawnBlock'`. „Near“ heißt „hier habe ich es gemerkt“, und das ist meistens das Erste *nach* dem, was du weggelassen hast. Jetzt ein fieserer. Setz `then` wieder ein und lösch stattdessen das `end` in der letzten Zeile von `spawnBlock`. Speichere: ---- Error running script: dodge/dodge.singe:148: 'end' expected (to close 'function' at line 58) near ---- Zeile 148 ist das Ende der Datei, gleich hinter ihrer letzten Zeile. An Zeile 148 ist nichts falsch. Lua hat bis ganz zum Ende gelesen -- `` nennt es das Ende einer Datei -- und dabei immer noch auf ein `end` gewartet, das nie kam, und bis dahin hatte es jede Funktion nach `spawnBlock` verschluckt, als gehöre sie dazu. Die nützliche Hälfte dieser Meldung ist `(to close 'function' at line 58)`, und dort fängt `spawnBlock` an. Wenn eine Meldung aufs Ende der Datei zeigt, glaub dem, was in den Klammern steht, nicht der Zeilennummer. Das ist auch der Grund, warum die Einrückung so wichtig ist: Ein fehlendes `end` ist in einer Reihe von Zeilen fast unsichtbar und sofort offensichtlich, sobald alles darunter eine Stufe zu weit eingerückt ist. Zwei weitere, die du wiedererkennen solltest, denn beiden begegnest du noch diese Woche: ---- Error running script: dodge/dodge.singe:41: unfinished string near '"GAME OVER)' ---- Ein Anführungszeichen, das du geöffnet und nie geschlossen hast; das kennst du aus Lektion eins. ---- Error running script: dodge/dodge.singe:135: 'then' expected near '=' ---- Das ist `if state = "playing" then`. Ein `=` setzt eine Variable, zwei `==` stellen eine Frage, und Lua lässt dich in einem `if` keine setzen. Es sieht nach einer seltsamen Beschwerde aus, und sie bedeutet immer dasselbe. [#_attempt_to_index_a_nil_value] === Attempt to Index a Nil Value Diese Meldung wirst du mit großem Abstand am häufigsten sehen. Geh in `spawnBlock` und schreib `block` in der ersten Zeile, die es benutzt, falsch, sodass dort `blocl.x = math.random(0, screenWidth - BLOCK_SIZE)` steht. Speichere, drück die Leertaste: ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:60: attempt to index a nil value (global 'blocl') stack traceback: dodge/dodge.singe:60: in function 'spawnBlock' dodge/dodge.singe:92: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- *Indizieren* ist das, was der Punkt tut. `blocl.x` heißt „geh in das Ding namens `blocl` und such darin das `x`“. Das geht nur bei einer Tabelle. *Nil* nennt Lua einen Namen, hinter dem nichts steht. Du hast es bisher selten gesehen, weil du sorgfältig allem einen Wert gegeben hast, aber jeder Name, den du nie benutzt hast, ist schon `nil`, und wer einen Namen falsch tippt, erfindet einen nagelneuen. Die ganze Meldung sagt also: *Du hast einen Punkt auf etwas angewendet, das es nicht gibt*. Der Teil in Klammern ist das Geschenk. `global 'blocl'` ist genau der Name des Dings, das leer war. Such in deiner Datei danach. Kommt er genau einmal vor, ist es ein Tippfehler, und du hast ihn in zehn Sekunden gefunden. Das Wort vor dem Namen sagt dir, wo Lua danach gesucht hat: * `global` -- ein Name, vor dem nirgends ein `local` steht. * `local` -- ein Name, den du in der Funktion, in der du gerade bist, mit `local` deklariert hast. * `field` -- ein Name nach einem Punkt, wie das `y` in `block.y`. * `upvalue` -- ein `local` von weiter außen, das diese Funktion sehen kann, und das sind alle Variablen von `dodge.singe`, denn sie werden oben in der Datei deklariert und in Funktionen benutzt. `field` ist die andere häufige Ursache, und die ist überhaupt kein Tippfehler. Wenn du `block.hit.when` geschrieben und `block.hit` nie gesetzt hast, bekommst du `attempt to index a nil value (field 'hit')`. Der Punkt, der scheitert, ist der zweite. Lua sagt dir, welcher, indem es ihn beim Namen nennt. [#_attempt_to_call_a_nil_value] === Attempt to Call a Nil Value Mach `blocl` wieder rückgängig. Jetzt schreib eine Engine-Funktion falsch: Ändere in `drawGame` das `overlayPrint` in `overlayPirnt`. Speichere: ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:36: attempt to call a nil value (global 'overlayPirnt') stack traceback: dodge/dodge.singe:36: in function 'drawGame' dodge/dodge.singe:140: in function 'onOverlayUpdate' ---- Dasselbe `nil`, anderes Vergehen. *Aufrufen* ist das, was die Klammern tun, und das geht nur bei einer Funktion. Es gibt keine Funktion namens `overlayPirnt`, also ist `overlayPirnt` gleich `nil`, also haben die Klammern nichts, was sie aufrufen könnten. Das passiert aus vier Gründen, nach Häufigkeit geordnet: . Du hast eine deiner eigenen Funktionen falsch geschrieben. . Du hast eine von Singe falsch geschrieben. Nichts in der Engine prüft deine Rechtschreibung, wenn die Datei geladen wird, also ist ein falsch geschriebener Engine-Name einfach ein weiterer Name, der zufällig leer ist, und er kostet dich nichts, bis die Zeile läuft. . Die Funktion gibt es in Singe überhaupt nicht -- es war eine vernünftige Vermutung, und vernünftige Vermutungen liegen etwa die Hälfte der Zeit daneben. Die Funktionsliste in der Referenz ist der einzige Weg, sicher zu sein. . Du hast sie `local` gemacht und von irgendwo oberhalb der Stelle aufgerufen, an der sie deklariert ist. Genau darum sind die Funktionen in `dodge.singe` global. Es gibt eine viel schlimmere Version dieses Fehlers, und du bist ihr in Lektion eins begegnet, ohne es zu wissen: Schreib einen *Callback* falsch, und du bekommst überhaupt keine Meldung. `onOverlayUpdated` ist kein Fehler. Es ist eine völlig ordentliche Funktion, von der Singe nie gehört hat und die es nie aufrufen wird, also bleibt der Bildschirm schwarz, und nichts beschwert sich. Wenn ein ganzer Teil des Spiels überhaupt nichts tut, verdächtige den Namen. [#_attempt_to_perform_arithmetic_on_a_nil_value] === Attempt to Perform Arithmetic on a Nil Value Setz `overlayPrint` wieder ein. Schreib in `updatePlaying` in der Fall-Zeile `blockSpeed` falsch, sodass sie `block.y = block.y + blockSped` lautet: ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped') stack traceback: dodge/dodge.singe:98: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- Rechnen heißt `+`, `-`, `*` und `/`. Du kannst nicht nichts zu einer Zahl addieren. Wieder nennen die Klammern das leere Ding, und wieder ist die häufigste Ursache ein Tippfehler -- aber dieser hier hat noch eine zweite Ursache, die du kennen solltest, weil sie schwerer zu sehen ist. Eine Variable, der du nie eine Zahl gegeben hast, ist `nil`, nicht null. Wenn du dem Spiel einen Highscore hinzufügst und oben `local hiScore` ohne `= 0` schreibst, steht darin `nil`, bis etwas sie setzt, und das erste `hiScore + score` hält das Spiel an. Lua lässt Zahlen nicht für dich bei null anfangen. Das machst du. Zwei nahe Verwandte, mit derselben Ursache und demselben Heilmittel: ---- dodge/dodge.singe:37: attempt to concatenate a nil value (upvalue 'hiScore') ---- `..`, das einen String an etwas hängt, das nicht da ist. Den siehst du jedes Mal, wenn du eine Punktzahl ausgibst, die du zu setzen vergessen hast -- hier `hiScore`, angehängt an die Punktezeile in `drawGame`, die jetzt Zeile 37 ist, weil `local hiScore` darüber steht. ---- dodge/dodge.singe:105: attempt to compare nil with number ---- Das ist `if score > hiScore then`, unter `state = "over"` gesetzt, um beim Ende eines Spiels die beste Punktzahl festzuhalten, ohne dass etwas in `hiScore` steht. Beachte, dass diese Meldung nichts in Klammern nennt. Wenn Lua dir nicht sagen kann, welche Seite leer war, zeigt dir der übernächste Abschnitt, wie du es herausfindest. [#_an_argument_of_the_wrong_kind] === Ein Argument der falschen Art Jede Meldung bisher kam von Lua. Diese kommt von Singe, und sie sieht völlig anders aus, also lohnt es sich, sie absichtlich kennenzulernen. Angenommen, du willst sehen, was in `blocks` steckt, und versuchst, es auszugeben. Füg `drawGame` diese Zeile hinzu, unter der Punktzahl: [source,lua] ---- overlayPrint(1, 3, blocks) ---- ---- 37:overlayPrint: Argument 3 must be a string. ---- Kein Dateiname, kein Traceback, kein `attempt to` irgendwas. Eine Beschwerde der Engine fängt mit der Zeilennummer an, dann kommt der Name der Funktion, die sich geweigert hat, dann, was sie wollte. Hier hat `overlayPrint` eine Tabelle bekommen, wo es Text braucht, und es hat angehalten, statt etwas Sinnloses zu zeichnen. Die Schwestermeldung zählt, statt zu prüfen: ---- 34:overlayBox: Expected 4 argument(s), got 3. ---- Das ist ein `overlayBox`, dem eine Ecke fehlt. Das seltsam aussehende `argument(s)` stammt von der Engine, nicht aus einem Tippfehler von dir. Wenn du so eine bekommst, ist der Eintrag zu dieser Funktion in der Referenz die Antwort, und das kostet dich eine Minute. Jeder Eintrag listet die Argumente der Reihe nach auf, mit dem, was jedes sein muss. Singe prüft sie, weil die Alternative ein Spiel ist, das zehn Minuten läuft und dann Müll zeichnet. [#_finding_out_what_is_really_in_there] === Herausfinden, was wirklich drinsteckt Die Hälfte der Fehlersuche ist überhaupt nicht das Lesen von Fehlermeldungen. Es ist der Fall, in dem nichts abstürzt und nichts funktioniert: Blöcke, die nie erscheinen, eine Spielfigur, die sich nicht bewegen will, eine Punktzahl, die bei null hängt. Dafür musst du in das laufende Spiel hineinsehen, und dafür gibt es zwei Wege. Der erste ist `debugPrint`, das eine Zeile in das Terminal schreibt, aus dem du Singe gestartet hast. Nimm die Zeile, aus der du nicht schlau wirst, und setz eins darüber: [source,lua] ---- debugPrint("blocks " .. #blocks .. ", timer " .. spawnTimer) ---- Jetzt siehst du dir an, was wirklich da ist, statt dessen, wovon du sicher bist, dass es da ist, und in dieser Lücke wohnen die Fehler. Gib `playerX` auf dieselbe Weise aus, und du siehst auch die Wahrheit darüber: Es wird als `160.0` ausgegeben, nicht als `160`, weil Teilen mit `/` in Lua immer eine Zahl mit Nachkommateil ergibt, auch wenn der Nachkommateil null ist. Zwei Dinge werden dich beißen. Das erste ist, dass `debugPrint` auf Text oder einer Zahl besteht; gib ihm etwas anderes, und es hält das Spiel an, so wie `overlayPrint` es getan hat. `debugPrint(blocks)` zeigt dir keine Liste, und `debugPrint("left " .. goingLeft)` ganz oben in `updatePlaying` zeigt dir auch kein `true` oder `false`: ---- dodge/dodge.singe:78: attempt to concatenate a boolean value (upvalue 'goingLeft') ---- `tostring` ist die Lösung. Es nimmt überhaupt alles und gibt dir Text dafür, also ist `tostring(goingLeft)` entweder `"true"` oder `"false"`, und die Zeile funktioniert. Das zweite ist, dass `onOverlayUpdate` sechzigmal pro Sekunde läuft, also gibt ein `debugPrint` darin sechzig Zeilen pro Sekunde aus, und du kannst keine davon lesen. Die Referenz sagt das auch in ihrem Eintrag: Der Aufruf ist billig, aber nichts bremst ihn. Bevor du irgendetwas anderes machst, setz einen Schalter vor jede Debug-Zeile, die du hinzufügst, damit du den Lärm abstellen kannst, ohne sie alle wieder aufspüren zu müssen: [source,lua] ---- local DEBUG = true function debugLog(text) if DEBUG then debugPrint(text) end end ---- Jedes `debugLog` im Spiel gehorcht diesem einen `true`. Ändere es in `false`, und das Spiel wird still; ändere es zurück, und alles ist wieder da. Dann gib etwas aus, wenn etwas *passiert*, statt in jedem Frame: [source,lua] ---- debugLog("hit at y " .. math.floor(block.y) .. ", lives now " .. lives) ---- Oder gib es nach einem Timer aus, der genau so heruntergezählt wird, wie Blöcke erzeugt werden: [source,lua] ---- reportTimer = reportTimer - 1 if reportTimer <= 0 then debugReport() reportTimer = REPORT_FRAMES end ---- Im Ordner `learn` liegt das Ganze als `08-when-it-goes-wrong.singe`: Es ist das Spiel aus Lektion sieben mit einem Bericht etwa alle zwei Sekunden, einer Zeile, wenn du getroffen wirst, und einer Zeile, wenn das Spiel endet. Der zweite Weg hineinzusehen ist, es auf den Bildschirm zu bringen, und das ist besser für alles, was sich in jedem Frame ändert, denn sechzig Zeilen pro Sekunde in einem Terminal sind nutzlos, aber eine Zahl, die in der Ecke tickt, lässt sich leicht beobachten: [source,lua] ---- if DEBUG then overlayPrint(1, 3, "BLOCKS " .. #blocks .. " PLAYER " .. playerX) end ---- [#_cutting_the_problem_in_half] === Das Problem halbieren Wenn du nicht siehst, welche von zwanzig Zeilen falsch ist, hör auf zu suchen und fang an wegzunehmen. Setz zwei Bindestriche an den Anfang einer Zeile, und Lua ignoriert den Rest davon. Das ist ein *Kommentar*, und meistens hinterlässt man sich damit Notizen, aber die bessere Verwendung ist diese: [source,lua] ---- -- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) ---- Kommentier die Hälfte von `drawGame` aus und starte es. Ist das Problem noch da, steckt es in der Hälfte, die du behalten hast. Ist es weg, steckt es in der Hälfte, die du entfernt hast. Setz diese Hälfte wieder ein und halbiere *sie*. Zwanzig Zeilen brauchen fünf solche Runden, und jede Runde ist einmal speichern und einmal hinsehen. Das funktioniert nicht nur mit Zeilen. Kommentier den Rumpf von `updatePlaying` aus, und die Blöcke hören auf, sich zu bewegen, und das sagt dir, ob das, was du jagst, im Bewegen steckt oder im Zeichnen. Kommentier den Zweig mit `collideRects` aus und sieh nach, ob das Spiel aufhört abzustürzen. Du versuchst dabei nicht, irgendetwas zu reparieren. Du versuchst herauszufinden, wo es nicht ist. [#_going_round_faster] === Schnellere Runden All das hängt davon ab, dass du etwas in ein paar Sekunden ausprobieren kannst, und dafür ist `-R` da. Du benutzt es seit Lektion eins; hier steht, was es wirklich tut, und das solltest du jetzt wissen, wo du dich darauf stützen wirst. Singe beobachtet jede Skriptdatei, die dein Spiel geladen hat. Wenn du eine speicherst, fängt das Spiel wieder von vorne an: Alles, was das Skript gemacht hat, wird weggeworfen, und deine Datei läuft neu, in der Zeit, die das Lesen der Datei braucht, und nicht in der Zeit, die das Starten der Engine braucht. `F5` macht dasselbe auf Wunsch, ohne dass du etwas speicherst. Eine Taste, die du während des Neuladens gedrückt hältst, wird ignoriert, bis du sie loslässt, also läuft die Spielfigur nach einem Neuladen nicht von allein davon. Die beiden Arten anzuhalten verhalten sich hier verschieden, und es ist nützlich zu wissen, welche du vor dir hast, ohne die Meldung zu lesen. Ein Skript, das sich nicht kompilieren lässt, wird *gemeldet und überstanden*: Das Fenster bleibt offen und leer, die Datei wird weiter beobachtet, und wenn du die Korrektur speicherst, kommt das Spiel zurück. Ein Absturz in einem Callback -- alle `attempt to`-Meldungen und die Beschwerden der Engine über Argumente -- reißt Singe mit, und du startest es von Hand neu. [#_reading_a_traceback] === Einen Traceback lesen Die Zeilen unter `stack traceback:` sind die Liste der Funktionen, die gerade liefen, als alles anhielt, die innerste zuerst. Hier ist noch einmal der von `blocl`: ---- dodge/dodge.singe:60: in function 'spawnBlock' dodge/dodge.singe:92: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- Lies ihn von unten nach oben, und er ist ein Satz. Singe hat `onOverlayUpdate` aufgerufen. In Zeile 136 hat `onOverlayUpdate` `updatePlaying` aufgerufen. In Zeile 92 hat `updatePlaying` `spawnBlock` aufgerufen. In Zeile 60 hat `spawnBlock` aufgegeben. Die oberste Zeile ist die Stelle, an der es kaputtging. Die Zeilen darunter zeigen, wie es dorthin kam, und die brauchst du, wenn die oberste Zeile unschuldig aussieht. Eine Funktion, die eine Zahl zu `block.y` addiert, ist nicht falsch; eine Funktion, die einen Block ohne `y` in die Liste gelegt hat, schon. Nur die Spur verbindet die beiden, und die Spur wird dir jedes einzelne Mal ausgegeben. [#_the_habit] === Die Gewohnheit Alles oben ist Technik. Das hier ist die Gewohnheit, und sie ist wichtiger: *Ändere eine Sache. Starte es. Sieh dir an, was passiert ist.* Wenn etwas nicht funktionieren will, ist die Versuchung groß, vier Dinge auf einmal zu ändern, weil eines davon es ja sein muss. Mach das, und es funktioniert auf eine neue Art nicht, und jetzt weißt du nicht, welches der vier es war oder ob sich zwei davon gegenseitig aufheben. Du hast das Problem größer gemacht und besser versteckt. Eine Änderung. Starten. Hinsehen. Es fühlt sich langsamer an. Es ist das Schnellste, was es gibt, und jeder erfahrene Programmierer, dem du je begegnest, macht es so, aus genau dem Grund, den du gleich herausfinden wirst. [#_try_it_8] === Probier es aus . *Lern sie alle kennen.* Mach jeden der fünf Fehler aus dieser Lektion absichtlich in `dodge.singe`, einen nach dem anderen, und lies die Meldung, bevor du ihn behebst. Du wirst nie wieder Angst vor ihnen haben. . *Beobachte den Erzeuger.* Setz ein `debugPrint` in `spawnBlock`, das das `x` des neuen Blocks ausgibt. Spiel dreißig Sekunden lang, dann sieh dir die Zahlen an. Sind sie über den Bildschirm verteilt, oder bevorzugen sie eine Seite? . *Schalt es ab.* Ändere im Skript der Lektion `DEBUG` in `false` und überzeug dich davon, dass nichts ausgegeben wird und das Spiel unverändert ist. . *Ein Fehler ohne Fehlermeldung.* Ändere in `updatePlaying` `if spawnTimer <= 0 then` in `if spawnTimer == 0 then`, was so aussieht, als bedeute es dasselbe. Starte es und fang ein Spiel an. Es gibt keine Fehlermeldung, nichts stürzt ab, und kein einziger Block fällt je, du kannst also nicht einmal verlieren. Setz ein `debugLog` in `spawnBlock`, füg `spawnTimer` der ersten Zeile hinzu, die `debugReport` ausgibt, und spiel noch einmal. Finde aus dem, was sie sagen, und aus dem, was eines von beiden nie sagt, heraus, warum `==` die falsche Frage war. . *Mach den Namen eines Callbacks kaputt.* Benenne `onInputPressed` in `onInputPress` um und starte es. Keine Fehlermeldung wird ausgegeben, nichts stürzt ab, die Spielfigur bewegt sich nicht, und die Leertaste startet kein Spiel. Dann erklär dir selbst, warum das der gefährlichste Fehler in dieser Lektion ist. [#_what_you_learned_8] === Was du gelernt hast * Fehlermeldungen haben eine Datei, eine Zeile und eine Beschwerde. Lies sie in dieser Reihenfolge. * `Error running script` heißt, dass nichts gelaufen ist; die Form mit einem Traceback heißt, dass es lief und angehalten hat. * `nil` ist ein Name, hinter dem nichts steht, und es steckt hinter den meisten Fehlermeldungen, die du je sehen wirst. * Das Wort in Klammern nennt das leere Ding und sagt, ob es global, lokal, ein Feld oder ein Upvalue war. * Ein fehlendes `end` wird am Ende der Datei gemeldet, und die Klammern sagen dir, wo der Fehler wirklich steckt. * Beschwerden der Engine sehen anders aus: eine Zeilennummer, ein Funktionsname und was sie stattdessen wollte. * `debugPrint` zeigt dir, was wirklich in einer Variablen steckt. `tostring` macht alles ausgebbar, und ein Schalter wie `DEBUG` stellt den Lärm ab. * Kommentier mit `--` eine Hälfte aus, um herauszufinden, in welcher Hälfte das Problem steckt. * Ein falsch geschriebener Callback erzeugt überhaupt keine Fehlermeldung, und das macht ihn schlimmer als einen Fehler, der eine erzeugt. * Ändere eine Sache, starte es, sieh hin. [#_next_time_8] === Beim nächsten Mal Damit endet Teil eins. Du kannst ein Spiel schreiben, und jetzt kannst du auch eins reparieren, und das heißt, dass alles ab hier Anbau ist und kein Fundament mehr. Teil zwei fängt damit an, diese farbigen Kästen durch echte Bilder zu ersetzen, und von da an sieht das Spiel, das du baust, allmählich wie ein Spiel aus, das auch jemand anders spielen wollen würde.