singe/docs/de/lessons/15-threaded.adoc

420 lines
15 KiB
Text

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