420 lines
15 KiB
Text
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.
|