[#_lesson_15_the_other_way_to_write_it] == Lektion 15: Die andere Schreibweise image::learn/15-threaded.png[Die fertige Lektion, 480] Jede Lektion bisher war auf dieselbe Art geschrieben. Du schreibst Funktionen, deren Namen mit `on` anfangen, Singe ruft sie auf, wann es ihm passt, und du sagst nie, in welcher Reihenfolge irgendetwas passiert. Das ist das *ereignisgesteuerte* Modell, und so funktionieren die meisten Spiel-Engines. Singe hat ein zweites Modell. Darin schreibst du dein Spiel als Liste von Schritten, von der ersten Zeile bis zur letzten, und du darfst warten. In dieser Lektion schreibst du dieselbe kurze Videosequenz auf beide Arten und siehst den Unterschied mit eigenen Augen. [#_the_sequence] === Der Ablauf Das macht das Spiel, auf Deutsch: . Starte den Film am Anfang und spiel ihn bis Frame 120. . Halte dort an und bitte den Spieler, die Leertaste zu drücken. . Wenn er sie drückt, spiel weiter bis Frame 300. . Halte dort an, sag „The end.“, warte vier Sekunden und beende das Spiel. Vier Schritte. Lies sie noch einmal und achte darauf, dass in jedem von ihnen das Wort „dann“ steckt, ausgesprochen oder nicht. [#_the_way_you_know] === Die Art, die du kennst Leg im Ordner `movie` aus Lektion vierzehn eine Datei namens `events.singe` an und tipp das hier ein: [source,lua] ---- dofile("Singe/Framework.singe") local step = 1 local endedAt = 0 function onInputPressed(what) if what == SWITCH_BUTTON1 and step == 2 then step = 3 discPlay() end end function onOverlayUpdate() local frame = discGetFrame() overlayClear() overlayPrint(2, 2, "Frame " .. frame) if step == 1 then if frame >= 120 then discPause() step = 2 end elseif step == 2 then overlayPrint(2, 4, "Press the space bar to go on.") elseif step == 3 then if frame >= 300 then discPause() endedAt = singeGetTicks() step = 4 end elseif step == 4 then overlayPrint(2, 4, "The end.") if singeGetTicks() - endedAt > 4000 then singeQuit() end end return OVERLAY_UPDATED end discSearch(0) discPlay() ---- Starte es. Den Ordner zu nennen würde `movie.singe` starten, also nenn stattdessen das Skript, so wie Lektion eins es dir gezeigt hat: ---- Singe -R -v Singe/menuBackground.mkv movie/events.singe ---- Es funktioniert. Es macht genau die vier Schritte. `singeGetTicks` ist neu -- es gibt die Zahl der Millisekunden seit dem Start der Engine zurück, und so misst man gewöhnlich eine Zeitspanne -- und `singeQuit` beendet das Spiel, genau das, was die Engine für dich tut, wenn du Esc drückst. Lies jetzt das Skript und versuch, die vier Schritte darin zu finden. Sie sind da, aber von innen nach außen gekehrt. Die Reihenfolge der Geschichte steht in den *Werten* von `step`, nicht in der Reihenfolge der Zeilen, und `step` wird in einer Funktion gesetzt und in einer anderen gelesen. Um die Frage „Was passiert, nachdem der Spieler die Leertaste gedrückt hat?“ zu beantworten, musst du das Ganze auf einmal im Kopf behalten. Und das ist die einfache Version. Es gibt darin zwei Entscheidungen. [#_the_other_way] === Die andere Art Fang eine neue Datei an, `steps.singe`, und tipp das hier ein: [source,lua] ---- local message = "" local pressed = false local function drawFrame() overlayClear() overlayPrint(2, 2, "Frame " .. discGetFrame()) overlayPrint(2, 4, message) end local function playTo(frame) while discGetFrame() < frame do drawFrame() singeYield() end end local function waitForButton() pressed = false while not pressed do drawFrame() singeYield() end end local function waitSeconds(seconds) local stopTime = singeGetTicks() + seconds * 1000 while singeGetTicks() < stopTime do drawFrame() singeYield() end end function onInputPressed(what) if what == SWITCH_BUTTON1 then pressed = true end end function singeMain() discSearch(0) discPlay() playTo(120) discPause() message = "Press the space bar to go on." waitForButton() message = "" discPlay() playTo(300) discPause() message = "The end." waitSeconds(4) end dofile("Singe/Framework.singe") ---- Starte es genauso, diesmal mit `movie/steps.singe` am Ende. Es macht dieselben vier Dinge. Lies jetzt `singeMain`. Die vier Schritte sind die vier Absätze, der Reihe nach, von oben nach unten, und nirgends gibt es eine Variable `step`, denn die Stelle in der Geschichte *ist* die Stelle in der Funktion. Darum geht es in dieser ganzen Lektion. [#_what_just_happened_11] === Was gerade passiert ist [source,lua] ---- function singeMain() ---- `singeMain` ist ein Name, nach dem Singe sucht, wie `onOverlayUpdate`. Wenn dein Skript sie definiert, lässt das Framework dein Spiel von ihr aus laufen: Sie wird einmal aufgerufen, am Anfang, und dein Spiel dauert so lange, wie sie dauert. Wenn sie zurückkehrt, wird das Spiel beendet. [source,lua] ---- singeYield() ---- Das ist der Aufruf, der das möglich macht, und er bedeutet „lass einen Frame passieren“. Singe macht nicht zwei Dinge gleichzeitig -- hier läuft nichts parallel, egal, was der Name *Thread-Modell* (englisch *threaded*) nahelegt. Was passiert, ist Folgendes: `singeYield` hält deine Funktion genau dort an, wo sie gerade steht, gibt die Kontrolle an die Engine zurück und merkt sich die Stelle. Die Engine zeichnet den Frame, liest die Eingaben, lässt das Video weiterlaufen und startet deine Funktion dann wieder ab der Zeile nach dem `singeYield`, mit jeder Variablen genau so, wie du sie verlassen hast. Eine `while`-Schleife mit einem `singeYield` darin ist also eine Art zu warten. Es ist die einzige Art zu warten, und du musst eines in jede Schleife setzen, die auf etwas wartet. Das ist ein Handel, den du mit der Engine abschließt: Du darfst dasitzen und warten, solange du ihr sechzigmal pro Sekunde den Frame zurückgibst. [source,lua] ---- local function playTo(frame) while discGetFrame() < frame do drawFrame() singeYield() end end ---- Singe hat keine Funktion „warte, bis das Video Frame 120 erreicht“. Es braucht keine, denn du kannst sie in vier Zeilen schreiben, und das hast du gerade getan. Mach weiter, solange die Disc den Frame, den du willst, noch nicht erreicht hat; gib in jeder Runde einen Frame zurück. `waitForButton` hat dieselbe Form mit einer anderen Frage, und `waitSeconds` hat noch einmal dieselbe Form, mit einer Uhr. Drei kleine Funktionen, einmal geschrieben, und ihretwegen liest sich `singeMain` wie die deutschen Sätze oben in der Lektion. Genau dafür sind deine eigenen Funktionen da, hat Lektion fünf gesagt. [source,lua] ---- drawFrame() singeYield() ---- Jede Warteschleife zeichnet, bevor sie den Frame abgibt, und das ist der Teil, der die Leute überrascht. Im Thread-Modell gibt es kein eigenes `onOverlayUpdate`, also wird nichts gezeichnet, was du nicht selbst zeichnest. Was du vor einem `singeYield` zeichnest, erscheint in diesem Frame. Hör auf zu zeichnen, und das Overlay hört auf, sich zu ändern. Darum gibt es `message`. `singeMain` setzt es, und `drawFrame` gibt es aus, und die drei Warteschleifen rufen alle `drawFrame` auf. Das ist ein kleiner Preis dafür, dass die Geschichte wieder in der richtigen Reihenfolge steht. [source,lua] ---- dofile("Singe/Framework.singe") ---- *Ganz unten*. Das ist wichtiger als alles andere in dieser Lektion, also lohnt es sich zu wissen, warum. `dofile` führt ein anderes Skript innerhalb deines Skripts aus, und eines der Dinge, die das Framework dabei tut, ist, nach einer Funktion namens `singeMain` zu suchen. Wenn es eine findet, baut es dein Spiel um sie herum und schreibt ein eigenes `onOverlayUpdate`, dessen ganze Aufgabe es ist, dein `singeMain` einmal pro Frame wieder anzustoßen. Findet es keine, tut es nichts dergleichen, und dein Spiel ist ein gewöhnliches ereignisgesteuertes Spiel. Also muss das Framework geladen werden, *nachdem* es `singeMain` gibt. Lädst du es oben, so wie in jeder Lektion seit der siebten, dann gibt es in dem Moment, in dem es nachsieht, noch kein `singeMain`, und du bekommst ein Spiel, in dem kein Spiel steckt. [#_the_rules] === Die Regeln Es sind vier, und zusammen decken sie jeden Fehler ab, den du hier machen kannst. *Das Framework kommt zuletzt.* Aus dem Grund oben. *Schreib kein eigenes `onOverlayUpdate`.* Das Framework hat schon eines geschrieben, und es kann nur eines geben: Welches der beiden zuletzt geschrieben wird, gewinnt, und das andere wird kommentarlos weggeworfen. Steht deins über dem `dofile`, fliegt deins raus, und was du zeichnest, erscheint nie. Steht es darunter, fliegt das des Frameworks raus, und dein `singeMain` läuft nie wieder. Zeichne stattdessen aus `singeMain` heraus. *Jede wartende Schleife braucht ein `singeYield`.* Eine Schleife ohne gibt den Frame nie zurück. Das Fenster zeichnet sich nicht mehr neu, reagiert nicht mehr, und das Spiel ist weg; um herauszukommen, schließt du das Terminal. Es gibt keine Fehlermeldung, denn aus Sicht der Engine ist deine Funktion noch nicht fertig. *Wenn `singeMain` zurückkehrt, endet das Spiel.* Das ist ordentlich, wenn du es so meinst, und eine Überraschung, wenn nicht. Ein Spiel, das ewig weiterlaufen soll, endet mit einer Schleife, die nie verlassen wird: [source,lua] ---- while true do drawFrame() singeYield() end ---- [#_input_still_arrives] === Eingaben kommen trotzdem an Sieh dir noch einmal an, wo `pressed` gesetzt wird: [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then pressed = true end end ---- Das ist ein Callback, in einem Skript, das als Liste von Schritten geschrieben ist. Beide Modelle laufen gleichzeitig, und das ist so gewollt. Wenn es `singeMain` gibt, wird dir nur `onOverlayUpdate` weggenommen. Alles andere -- `onInputPressed`, `onInputReleased`, `onKeyPressed`, `onSoundCompleted`, `onShutdown`, alle -- wird genau so ausgelöst wie immer, in den Lücken zwischen deinen Yields. Die übliche Art, ein Spiel in diesem Modell zu schreiben, ist also die, die du gerade benutzt hast: die Geschichte in `singeMain` und ein kleiner Callback, der ein Ereignis auffängt und der Geschichte eine Notiz hinterlässt, die sie finden kann. `waitForButton` setzt `pressed` auf `false`, bevor es zu warten anfängt, und wirft damit alles weg, worauf der Spieler vorher herumgehämmert hat. Lass diese Zeile weg, und ein Knopf, der während des Videos gedrückt wurde, zählt als Antwort auf eine Frage, die noch gar nicht gestellt wurde. [#_which_one_to_use] === Welches du nimmst Keines der beiden Modelle ist das bessere. Sie passen zu verschiedenen Aufgaben, und ein echtes Spiel nimmt das, was passt. Das Thread-Modell passt zu allem, was ein *Ablauf* ist. Spiel das ab, dann frag, dann spiel jenes ab. Ein Vorspann. Eine Zwischensequenz. Ein Tutorial. Ein Bosskampf mit drei Phasen. Alles, was du ganz natürlich als nummerierte Liste beschreiben würdest, ist ein `singeMain`, das darauf wartet, geschrieben zu werden, und nichts ist so sehr ein Ablauf wie Video, und darum kommt Teil drei genau hier darauf. Das ereignisgesteuerte Modell passt zu allem, was eine *Situation* ist. Zehn Gegner und ein Spieler, alle in Bewegung, alle stoßen zusammen, keiner in irgendeiner Reihenfolge. Das Spiel, das du in Lektion sieben geschrieben hast, hat überhaupt keinen Ablauf: Jeder Frame ist dieselbe Frage, noch einmal gestellt, und das ist genau die Form von `onOverlayUpdate`. Schreib das als Liste von Schritten, und du gewinnst nichts und verlierst die Klarheit. Wenn ein Spiel beides ist (und das sind die meisten), wird es auch als beides geschrieben. `singeMain` geht der Reihe nach durch die Level, und in jedem Level erledigt eine Schleife sechzigmal pro Sekunde die Arbeit aus Lektion sieben, bis das Level endet. [#_try_it_15] === Probier es aus . *Füg einen Schritt hinzu.* Lass den Film nach `waitForButton` stattdessen zurückspringen: `discSkipToFrame(0)`, dann `playTo(60)`, dann weiter wie vorher. Zwei neue Zeilen an einer Stelle, und sonst muss nichts im Skript davon wissen. . *Füg denselben Schritt in das andere ein.* Mach das jetzt in `events.singe`. Zähl, an wie vielen Stellen du etwas ändern musstest. . *Schreib `waitFrames`.* Eine vierte Hilfsfunktion neben den anderen drei, die eine bestimmte Zahl von Video-Frames wartet statt einer bestimmten Zahl von Sekunden. Benutz `discGetFrame` und merk dir, wo du angefangen hast. . *Lass ein Yield weg.* Lösch das `singeYield()` in `playTo` und starte es. Mach dich bereit, das Terminal zu schließen, denn nichts anderes hält es an. Setz es jetzt wieder ein, lösch stattdessen `drawFrame()` und finde an dem, was du siehst, heraus, warum der Frame-Zähler stehen bleibt, der Film aber nicht. . *Mach eine Schleife daraus.* Pack den ganzen Inhalt von `singeMain` in `while true do ... end`, damit der Ablauf von vorn anfängt, statt zu enden. Mit Esc kommst du trotzdem heraus, denn das war nie deine Aufgabe. [#_break_it_on_purpose_14] === Mach es absichtlich kaputt Verschieb die Zeile `dofile("Singe/Framework.singe")` vom Ende von `steps.singe` an den Anfang und starte es. Es gibt keinen Fehler. Nichts wird ausgegeben. Das Fenster geht auf, der erste Frame des Films steht da, und mehr passiert nie. Das ist die schlimmste Art von Bug, und genau darum lohnt es sich, ihm absichtlich zu begegnen. Aus Sicht der Engine ist nichts schiefgegangen: Dein Skript wurde geladen, das Framework wurde geladen, und keines von beiden hat ein `onOverlayUpdate` definiert, also hat die Engine nichts aufzurufen. Die Disc steht auf Frame eins, pausiert, genau so, wie die Engine sie hinterlassen hat, denn das `discPlay`, das sie gestartet hätte, steckt in einem `singeMain`, das nie jemand ausführt. Wenn ein Spiel im Thread-Modell überhaupt nichts tut, sieh dir zuerst die letzte Zeile der Datei an. Fast immer liegt es daran. [#_what_you_learned_15] === Was du gelernt hast * Singe kennt zwei Arten, ein Skript zu schreiben, und du wählst, indem du `singeMain` definierst oder nicht. * Im Thread-Modell ist dein Spiel eine Liste von Schritten und läuft von oben nach unten. * `singeYield()` gibt der Engine einen Frame zurück und macht mit der nächsten Zeile weiter. * Warten ist eine `while`-Schleife mit einem `singeYield` darin. Die Wartefunktion, die du brauchst, schreibst du selbst; die Engine liefert keine. * Nichts wird gezeichnet, wenn du es nicht zeichnest, vor einem Yield, in jedem Frame. * `dofile("Singe/Framework.singe")` kommt ans *Ende* eines Skripts im Thread-Modell. * Schreib kein eigenes `onOverlayUpdate`, wenn es `singeMain` gibt. * Jeder andere Callback wird weiterhin ausgelöst, also lassen sich Ereignisse und Schritte frei mischen. * Wenn `singeMain` zurückkehrt, endet das Spiel. * Abläufe wollen das Thread-Modell, Situationen das ereignisgesteuerte. [#_next_time_15] === Beim nächsten Mal Du kannst jetzt „spiel das ab, dann frag, dann spiel jenes ab“ als drei Zeilen schreiben. Ein verzweigtes Videospiel ist nichts anderes als dieser Satz, ein paar hundert Mal, wobei die Antworten entscheiden, welche Zeile als Nächstes kommt. Lektion sechzehn baut eines, und der interessante Teil ist am Ende gar nicht das Video. Es ist, die Karte davon, was wohin führt, in einer Form zu halten, die du noch lesen kannst, wenn das Spiel vierzig Szenen hat.