[#_lesson_14_playing_video] == Lektion 14: Video abspielen image::learn/14-video.png[Die fertige Lektion, 480] Singe kann einen Film abspielen und dein Spiel darin laufen lassen. Das Bild wird zur Welt, und das Overlay, auf dem du seit Lektion eins zeichnest, liegt obendrauf. In dieser Lektion startest du ein Video, findest heraus, welcher Frame gerade zu sehen ist, hältst es an und springst zu jedem beliebigen Moment. Vor alldem muss eine Sache klar gesagt werden, denn sie entscheidet, ob du diesen Teil des Buchs überhaupt lesen solltest. [#_a_game_plays_video_only_if_it_asks_to] === Ein Spiel spielt nur dann Video ab, wenn es darum bittet Singe ist eine Spiel-Engine. Alles, was du in Teil eins und zwei gebaut hast -- die Sprites, der Sound, die Kollisionen, die Punkte, der Spielordner -- ist das ganze Singe, und nichts davon hat mit Video zu tun. Was Teil drei hinzufügt, ist eine Zugabe: Ein Spiel kann darum bitten, dass hinter ihm ein Film läuft, und dann ist der Film die Welt statt einer schwarzen Zeichenfläche. Sonst ändert sich nichts. `onOverlayUpdate` wird immer noch sechzigmal pro Sekunde aufgerufen. `overlayPrint` zählt immer noch in Zeichenzellen. Deine Sprites werden dort gezeichnet, wo sie vorher gezeichnet wurden. Ein Spiel mit Video und ein Spiel ohne werden auf dieselbe Art geschrieben, mit denselben Funktionen, und teilen sich dasselbe Buch. Wenn das Spiel, das du machen willst, also kein Video hat, kannst du diesen Teil jetzt zuklappen und zu Teil vier gehen. Nichts in Teil vier hängt von irgendetwas in Teil drei ab, und die einzigen Lektionen, die diese hier brauchen, sind die vier, die nach ihr kommen. Noch da? Gut. [#_you_already_have_a_video] === Du hast schon ein Video Du musst keins suchen, keins kaufen und keins machen. Als du Singe in Lektion eins zum ersten Mal gestartet hast, ist in deinem Arbeitsordner ein Ordner namens `Singe` aufgetaucht. Öffne ihn und sieh nach: Unter den Dateien, die die Engine entpackt hat, ist `menuBackground.mkv`. Das ist der Hintergrund, den das eigene Menü der Engine benutzt, es ist 720 mal 480 Pixel groß und 420 Frames lang -- etwa vierzehn Sekunden. Das ist das Video für diese Lektion und die beiden danach. Leg in deinem Arbeitsordner, neben den Ordnern aus den anderen Lektionen, einen Ordner namens `movie` an und darin eine Datei namens `movie.singe`. Mehr musst du nicht vorbereiten. Du startest Singe aus dem Arbeitsordner, wie seit Lektion eins, also liegt der Ordner `Singe` samt Video direkt neben deinem Spiel, und `Singe/menuBackground.mkv` ist der Name, den du der Engine gleich gibst. Kopieren musst du nichts. [#_type_this_in_2] === Tipp das ein [source,lua] ---- dofile("Singe/Framework.singe") function onOverlayUpdate() overlayClear() overlayPrint(2, 2, "Frame " .. discGetFrame()) return OVERLAY_UPDATED end discPlay() ---- Speichere die Datei. [#_run_it_2] === Starte es Das Video gehört nicht zu deinem Skript, also lädst du es auch nicht in deinem Skript. Du gibst es der Engine auf der Befehlszeile mit, mit `-v`: ---- Singe -R -v Singe/menuBackground.mkv movie ---- `-v` ist die Kurzform von `--framefile` und nennt das Video, das das Spiel abspielen soll. Der Name deines Skripts kommt zuletzt, wie immer. `-R` ist dasselbe `-R`, das du seit Lektion eins benutzt, und es startet das Spiel immer noch neu, wenn du speicherst. Um das Bitten geht es bei `-v`. Wenn du eine Videodatei neben dein Skript legst und nichts sagst, bemerkt die Engine sie, sagt das auf der Konsole und startet dein Spiel ohne sie. Ein Spiel ist nur dann ein Videospiel, wenn jemand sagt, dass es eins ist. Ein Fenster geht auf. Ein Ball aus orangefarbenem Feuer füllt es, aus dem Feuer formt sich ein Drache, darunter erscheint das Wort `SINGE`, und dann rollt ein magentafarbenes Gitter unter einer orangefarbenen Sonne davon. In der linken oberen Ecke, über allem, zählt eine Zahl hoch. Sieh es dir bis zum Ende an. Dann sieh dir an, was am Ende passiert. [#_what_just_happened_10] === Was gerade passiert ist [source,lua] ---- discPlay() ---- Singe nennt das Video *die Disc*, und jede Funktion, die es steuert, fängt mit `disc` an. Der Name ist historisch: Die Engine hat ihr Leben damit begonnen, Spiele abzuspielen, die ihren Film auf einer Laserdisc hatten, und der Name ist an der Idee hängen geblieben. Es ist eine Videodatei auf deiner Festplatte und sonst nichts. Ein Spiel hat höchstens eine Disc, also nimmt keine der `disc`-Funktionen ein Argument, das sagt, welche. Es gibt nur die eine. Diese Zeile steht in keiner Funktion. Sie steht für sich allein unten in der Datei, und sie läuft einmal, wenn Singe dein Skript lädt -- vor dem ersten `onOverlayUpdate`, vor allem anderen. Singe hat keinen Callback `onStartup`, weil es keinen braucht: Ein Lua-Skript *ist* eine Liste von Anweisungen, und alles, was du außerhalb einer Funktion schreibst, passiert, während die Datei gelesen wird. Startcode kommt nach Konvention ganz unten hin, unter die Funktionen. Du brauchst `discPlay()`, weil die Disc nicht von selbst startet. Wenn das Spiel beginnt, parkt die Engine die Disc auf Frame eins und pausiert sie dort. Nichts bewegt sich, bis du darum bittest. [source,lua] ---- overlayPrint(2, 2, "Frame " .. discGetFrame()) ---- `discGetFrame` beantwortet eine Frage: Welcher Frame ist gerade zu sehen? Es nimmt keine Argumente und gibt eine Zahl zurück. Achte darauf, wo diese Zahl gezeichnet wird. Spalte 2, Zeile 2 -- genau dort, wo in Lektion eins `Hello!` hinkam, und sie landet an derselben Stelle auf dem Bildschirm. Das Overlay, auf dem du zeichnest, ist auf jeder Seite halb so groß wie das Video, und das ist bei einem Video von 720 mal 480 genau 360 mal 240, also genau die Größe, auf der du seit dreizehn Lektionen zeichnest. Deine Koordinaten haben sich nicht verschoben. Wenn du je ein Video in einer anderen Größe abspielst, ändert das Overlay seine Größe mit, und `overlaySetResolution` stellt es auf jede Größe, die du willst; der Eintrag in der Referenz hat die Einzelheiten. Das ist die ganze Idee des Overlays. Das Video liegt darunter. Deine Zeichnung liegt obendrauf, und überall, wo du nicht gezeichnet hast, scheint der Film durch. Darum ist `overlayClear` jetzt noch wichtiger als bisher: Lösch das Overlay, und du siehst den Film. [#_a_frame_is_an_address] === Ein Frame ist eine Adresse Die Zahl, die `discGetFrame` zurückgibt, ist eine *Framenummer*. Ein Frame ist ein einzelnes stehendes Bild. Dieses Video enthält 420 davon und zeigt etwa dreißig pro Sekunde, also dauert es etwa vierzehn Sekunden. Frames werden ab null gezählt, also hat dieses Video die Frames 0 bis 419. Die Anzahl und die letzte Nummer sind nicht dasselbe, und darüber stolpert jeder einmal. `discGetFrameCount` sagt dir die Anzahl -- 420 -- und der höchste Frame, nach dem du fragen kannst, ist eins weniger. Das Wichtige an einer Framenummer ist, dass sie sich nie ändert und nie verschiebt. Frame 110 ist der Moment, in dem der Drache Feuer speit, in dieser Kopie der Datei, auf diesem Rechner, heute und nächstes Jahr. Sekunden verschieben sich, weil ein Video nicht immer genau mit der Rate läuft, die du glaubst. Eine Framenummer ist die Adresse eines Moments, und aus Adressen ist ein Videospiel gebaut: Du schreibst dir den Frame auf, in dem der Drache erscheint, und von da an kannst du den Film dorthin schicken, wann immer du willst. Diese Zahlen zu finden ist eine Arbeit, die du einmal pro Video machst, und das Skript, das du geschrieben hast, ist das Werkzeug dafür: Lass den Film laufen, beobachte den Zähler, schreib dir die Frames auf, in denen etwas passiert. Alles in Lektion sechzehn baut auf einer Liste von Zahlen auf, die genau so gefunden wurden. [#_what_happened_at_the_end] === Was am Ende passiert ist Der Zähler hat 419 erreicht, und dann stand er wieder auf 0, und das Feuer war zurück. Wenn die Disc über das Ende hinausläuft, startet die Engine sie wieder ab Frame null. Sie hält nicht an, und sie sagt dir nichts. Ein echtes Spiel lässt es nie so weit kommen: Es beobachtet die Framenummer und schickt die Disc woandershin, bevor das Ende kommt. Weil das jemand vergessen hat, spielt ein halbfertiges Videospiel mitten in einem Kampf plötzlich seine eigene Titelsequenz ab. [#_stopping_jumping_and_asking] === Anhalten, Springen und Fragen Füg diese Funktion über `onOverlayUpdate` ein: [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then if discGetState() == DISC_PLAYING then discPause() else discPlay() end elseif what == SWITCH_LEFT then discSearch(0) elseif what == SWITCH_RIGHT then discSkipForward(30) elseif what == SWITCH_START1 then discStop() end end ---- Speichere und probier jede aus. Die Leertaste spielt und pausiert. Links geht zurück zum Anfang. Rechts springt etwa eine Sekunde weiter. Die Taste `1` hält die Disc ganz an, und was du bekommst, ist ein blauer Bildschirm -- die Farbe, die ein Laserdisc-Player zeigte, wenn er angehalten hatte, und sie ist geblieben, weil sehr viele Spiele ihre Punktetabelle darauf gezeichnet haben. Das sind fünf neue Aufrufe, und sie teilen sich in zwei Arten auf. *Fragen.* `discGetState` gibt einen von vier Namen zurück, die die Engine schon für dich festgelegt hat: `DISC_PLAYING`, `DISC_PAUSED`, `DISC_STOPPED` und `DISC_EJECTED`. Sie sind wie `OVERLAY_UPDATED` -- keine Anführungszeichen, weil sie Namen für Werte sind und kein Text. Vergleiche mit `==`, so wie du in Lektion drei Zahlen verglichen hast. *Sagen.* `discPlay` startet oder setzt fort. `discPause` friert auf dem Frame ein, der gerade zu sehen ist, und lässt ihn auf dem Bildschirm. `discStop` ist der schwerere: Das Bild wird durch den blauen Bildschirm ersetzt, `discGetFrame` antwortet `0`, bis die Disc wieder läuft, und `discPause` und die Sprungaufrufe werden ignoriert, solange sie angehalten ist. Jeder von `discPlay`, `discSearch` oder `discSkipToFrame` hebt das Anhalten wieder auf. Zwei Aufrufe schicken die Disc zu einem Frame, und der Unterschied zwischen ihnen ist das Einzige in dieser Lektion, das du dir einprägen solltest: * `discSearch(frame)` geht dorthin und *pausiert* dort. * `discSkipToFrame(frame)` geht dorthin und *spielt* von dort ab. Nimm Search, wenn du auf einem Bild parken und warten willst. Nimm Skip, wenn eine Szene anfangen soll. `discSkipForward(30)` und `discSkipBackward(30)` sind dieselbe Idee, gemessen ab dort, wo du bist, statt ab dem Anfang, und sie lassen die Disc spielen, wenn sie gespielt hat, und pausiert, wenn sie pausiert war. Es gibt auch `discPauseAtFrame`, das unter einem älteren Namen genau das tut, was `discSearch` tut. Die Referenz kennzeichnet es als alten Alias. Du wirst es in den Skripten anderer Leute sehen; schreib in deine eigenen `discSearch`. [#_waiting_is_harder_than_it_sounds] === Warten ist schwerer, als es klingt Das ist der Satz, aus dem jedes Videospiel besteht: ____ Spiel von Frame 0 bis Frame 120, dann stell dem Spieler eine Frage. ____ Versuch, ihn zu schreiben. Du kannst nicht „spiel, dann warte, dann frag“ schreiben, weil du keinen Ort hast, an dem du das schreiben könntest. Du hast `onOverlayUpdate`, das Singe sechzigmal pro Sekunde aufruft und das jedes einzelne Mal sofort zurückkehren muss. Eine Funktion, die dasäße und wartete, würde die Engine abwürgen: kein Zeichnen, keine Eingabe, kein Video. Du kannst also nicht „dann“ sagen. Du musst aus dem „dann“ eine Variable machen, die sich merkt, wie weit du gekommen bist, und sie bei jedem dieser sechzig Aufrufe prüfen: [source,lua] ---- local asking = false function onOverlayUpdate() overlayClear() if not asking and discGetFrame() >= 120 then discPause() asking = true end if asking then overlayPrint(2, 4, "Left or right?") end return OVERLAY_UPDATED end ---- Das funktioniert. Lies es, und du siehst, dass es funktioniert. Aber achte darauf, was es gekostet hat: eine Variable, deren einzige Aufgabe es ist, sich zu merken, in welchem Teil des Satzes du bist, und zwei `if`-Anweisungen, die sie ehrlich halten. Jetzt füg den Rest der Szene hinzu. Der Spieler antwortet, also spielt als Nächstes eins von zwei Dingen. Jedes davon endet irgendwo und stellt eine weitere Frage. Ein bescheidenes verzweigtes Spiel hat vierzig solcher Momente, und jeder davon will seine eigene Variable und sein eigenes `if`. Am Ende hast du ein Skript, in dem die Geschichte nirgends aufgeschrieben steht -- sie ist über ein Dutzend Flags verteilt, und um herauszufinden, was nach dem Drachen passiert, musst du sie alle lesen. Merk dir diesen Gedanken. Lektion fünfzehn ist nichts anderes als die Lösung dafür. [#_when_there_is_no_video] === Wenn es kein Video gibt Starte dein Skript wieder auf die schlichte Art, ohne `-v`: ---- Singe -R movie ---- Nichts stürzt ab. Du bekommst ein schwarzes Fenster mit `Frame 0` in der Ecke. Das lohnt sich zu verstehen, denn es ist die beruhigende Hälfte des Versprechens vom Anfang dieser Lektion. Ein Spiel ohne Disc führt trotzdem jeden `disc`-Aufruf aus, den du machst; sie tun nur nichts Nützliches. Die Engine dokumentiert genau, was sie antworten, und das ändert sich nie: `discGetFrame` und `discGetFrameCount` geben `0` zurück, `discGetState` gibt `DISC_EJECTED` zurück, `discGetWidth` und `discGetHeight` geben die Größe der Zeichenfläche zurück, und `discPlay`, `discPause`, `discSearch` und der Rest werden im Log vermerkt und ansonsten ignoriert. Du kannst kein Skript kaputt machen, indem du eine Disc-Funktion aufrufst, wenn es keine Disc gibt. Wenn ein Skript es *wissen* muss, fragt es `SINGE_DISC`. Das ist ein weiterer Name, den die Engine festlegt, bevor dein Skript läuft, und er ist `true`, wenn das Spiel ein Video hat, und `false`, wenn die Welt eine schlichte Zeichenfläche ist. So schreibt man richtig ein Spiel, das auf beide Arten laufen kann: [source,lua] ---- if not SINGE_DISC then overlayPrint(2, 2, "This game has no video.") overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv") return OVERLAY_UPDATED end ---- `not` dreht einen Wahr-oder-falsch-Wert um, wie in Lektion drei. [#_the_whole_thing_7] === Das Ganze Zusammengesetzt, mit einer kleinen Funktion, die den Zustand in Worte fasst, und einem Frame, an dem angehalten wird, ist das hier das fertige Skript. Es ist ein Frame-Finder: das Werkzeug, das du jedes Mal benutzen wirst, wenn du einem neuen Video begegnest. [source,lua] ---- dofile("Singe/Framework.singe") local stopAt = 400 local held = false local function stateName() local state = discGetState() if state == DISC_PLAYING then return "playing" elseif state == DISC_PAUSED then return "paused" elseif state == DISC_STOPPED then return "stopped" end return "not there" end function onInputPressed(what) if what == SWITCH_BUTTON1 then if discGetState() == DISC_PLAYING then discPause() else discPlay() end elseif what == SWITCH_LEFT then discSearch(0) held = false elseif what == SWITCH_RIGHT then discSkipForward(30) elseif what == SWITCH_START1 then discStop() held = false end end function onOverlayUpdate() local frame = discGetFrame() overlayClear() if not SINGE_DISC then overlayPrint(2, 2, "This game has no video.") overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv") return OVERLAY_UPDATED end if not held and frame >= stopAt then discPause() held = true end overlayPrint(2, 2, "Frame " .. frame .. " of " .. discGetFrameCount()) overlayPrint(2, 4, "The disc is " .. stateName() .. ".") overlayPrint(2, 6, "Space plays and pauses. Left rewinds.") overlayPrint(2, 7, "Right jumps on a second. 1 stops the disc.") if held then overlayPrint(2, 9, "Holding at frame " .. stopAt .. ".") end return OVERLAY_UPDATED end discPlay() ---- `held` ist dieselbe Art von Variable wie `asking` oben, und sie ist aus einem Grund da, den du testen kannst: Nimm sie heraus, lass `if frame >= stopAt then discPause() end` stehen und versuch dann, mit der Leertaste über Frame 400 hinaus weiterzumachen. Es geht nicht. Die Disc läuft eine Sechzigstelsekunde lang weiter, und das nächste `onOverlayUpdate` pausiert sie wieder, für immer. Mit `held` sagst du „das habe ich schon einmal gemacht“. [#_try_it_14] === Probier es aus . *Finde den Drachen.* Starte es, beobachte den Zähler und schreib dir den Frame auf, in dem der Drache zum ersten Mal erscheint, den Frame, in dem er Feuer speit, und den Frame, in dem das Gitter den Bildschirm übernimmt. Diese drei Zahlen wirst du in Lektion sechzehn brauchen. . *Geh es Schritt für Schritt durch.* Ändere `discSkipForward(30)` in `discSkipForward(1)` und halt die Pfeiltaste nach rechts gedrückt. Jetzt ändere `stopAt` in einen Frame mitten im Drachen und benutz `discSearch` und die Pfeiltaste, um genau darauf zu landen. . *Lass es in Schleife laufen.* Ändere das Anhalten so, dass es statt bei Frame 400 zu pausieren `discSkipToFrame(0)` aufruft. Du hast einen Attract-Modus gebaut: eine Szene, die für immer läuft, bis jemand einen Knopf drückt. . *Frag nach einem Frame, den es nicht gibt.* Ändere die Zeile mit `SWITCH_RIGHT` in `discSkipToFrame(100000)`. Es stürzt nicht ab. Lies den Eintrag zu `discSkipToFrame` in der Referenz und finde den Satz, der sagt, warum. . *Spiel es ohne das Video.* Starte `Singe -R movie` und drück dann jede Taste. Nichts, was du drückst, kann es kaputt machen. [#_break_it_on_purpose_13] === Mach es absichtlich kaputt Der Fehler, der hier am meisten Zeit kostet, ist kein Lua-Fehler. Starte das hier: ---- Singe -R -v menuBackground.mkv movie ---- Der Pfad ist falsch -- er zählt ab dem Arbeitsordner, und das Video liegt dort im Ordner `Singe`, nicht lose herum -- und Singe startet nicht. Stattdessen gibt es seine ganze Liste von Befehlszeilenoptionen aus, was beim ersten Mal erschreckend ist, und dann das hier als allerletzte Zeile: ---- Error: Unable to locate video. ---- So eine Wand aus Optionen bedeutet immer dasselbe: Die Engine ist nie bis zu deinem Spiel gekommen. Ignorier die Wand und lies die letzte Zeile. Der ist leicht. Die wenig hilfreiche Version davon ist, ganz ohne `-v` zu starten und zu vergessen, dass du das getan hast. Es gibt keinen Fehler, weil ein Spiel ohne Video ein völlig gutes Spiel ist. Du bekommst einen schwarzen Bildschirm und einen Zähler, der auf `0` festhängt, und suchst zwanzig Minuten lang in deinem Skript nach dem Fehler. Genau dafür ist der Block mit `SINGE_DISC` im fertigen Skript da. Drei Zeilen davon, und das Spiel sagt dir, was nicht stimmt, statt zu schmollen. Bau so eine Prüfung in jedes Skript ein, das ein Video braucht. [#_what_you_learned_14] === Was du gelernt hast * Ein Spiel spielt nur dann Video ab, wenn es darum bittet, und alles andere in Singe funktioniert so oder so gleich. * Singe nennt das Video die Disc. Ein Spiel hat eine, und die `disc`-Funktionen nehmen kein Handle, weil es nichts gibt, zwischen dem man wählen müsste. * Das Video kommt mit `-v` auf die Befehlszeile, nicht ins Skript. * Das Overlay wird über das Bild gezeichnet, und der Film scheint überall durch, wo du nicht gezeichnet hast. * Ein Frame ist ein einzelnes stehendes Bild, ab null gezählt, und eine Framenummer ist die Adresse eines Moments. * `discGetFrame` fragt, wo die Disc ist; `discGetFrameCount` sagt, wie viele Frames es gibt; `discGetState` sagt, was die Disc gerade tut. * `discPlay`, `discPause` und `discStop` steuern sie. `discSearch` geht zu einem Frame und pausiert; `discSkipToFrame` geht zu einem Frame und spielt. * Eine Disc, die über das Ende hinausläuft, fängt wieder bei Frame null an, ohne es dir zu sagen. * Ohne Disc ist jeder `disc`-Aufruf harmlos, und an `SINGE_DISC` erkennt ein Skript, in welcher Art von Spiel es steckt. * Du kannst in `onOverlayUpdate` nicht warten, also muss „dann“ als Variable ausgeschrieben werden. [#_next_time_14] === Beim nächsten Mal Der letzte Punkt ist der wunde. Ein Videospiel ist eine Liste von Schritten -- spiel dies, frag das, spiel jenes -- und du hast gerade gesehen, dass das Callback-Modell nicht „dann“ sagen kann ohne ein Flag und ein `if` für jeden Schritt. Lektion fünfzehn zeigt dir die andere Art, wie Singe dich ein Skript schreiben lässt, und darin ist „dann“ eine neue Zeile.