singe/docs/de/lessons/14-video.adoc

471 lines
20 KiB
Text

[#_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.