535 lines
20 KiB
Text
535 lines
20 KiB
Text
[#_lesson_16_branching]
|
|
== Lektion 16: Verzweigungen
|
|
|
|
image::learn/16-branching.png[Die fertige Lektion, 480]
|
|
|
|
Ein verzweigtes Videospiel ist ein Film mit Gabelungen darin. Eine Szene läuft, der
|
|
Spieler entscheidet, und als Nächstes läuft eine andere Szene. Mach das zwanzig- oder
|
|
dreißigmal, und du hast ein Spiel, das die Leute jahrelang immer wieder spielen, weil sie
|
|
das Ende suchen, das sie noch nicht gesehen haben.
|
|
|
|
In dieser Lektion baust du eines. Es hat vier Szenen, zwei Entscheidungen, einen Timer
|
|
bei jeder Entscheidung, einen Tod und drei Leben. Der Code, der es abspielt, ist etwa
|
|
dreißig Zeilen lang, und er wäre immer noch etwa dreißig Zeilen lang, wenn das Spiel
|
|
zweihundert Szenen hätte -- denn das Spiel selbst wird überhaupt nicht in Code geschrieben.
|
|
|
|
[#_imagine_it_is_a_dragon]
|
|
=== Stell dir vor, es ist ein Drache
|
|
|
|
Das Video, das jeder hat, ist `Singe/menuBackground.mkv`, dieselben 420 Frames, die du
|
|
seit Lektion vierzehn steuerst. Es ist ein Feuerball, ein Drache aus Papier, das Wort
|
|
`SINGE` und dann ein magentafarbenes Gitter, das unter einer orangefarbenen Sonne endlos
|
|
davonrollt.
|
|
|
|
Ein Drama ist es nicht. Niemand darin könnte in Gefahr geraten, und dem Sonnenuntergang
|
|
ist egal, wie du dich entscheidest.
|
|
|
|
Spiel trotzdem mit. Für zwei Minuten ist der Drache echt genug, und alles, was du gleich
|
|
schreibst, funktioniert genau gleich, wenn das Material ein Stuntman ist, der von einer
|
|
Klippe stürzt. Hier ist die Karte, und die Frame-Nummern sind die, die du in Lektion
|
|
vierzehn selbst herausgefunden hast:
|
|
|
|
[cols="1,1,3",options="header"]
|
|
|===
|
|
| Szene | Frames | Was passiert
|
|
| `arrive` | 0 bis 119 | Feuer, und der Drache landet. *Left or Right?*
|
|
| `sunset` | 120 bis 259 | Er speit Feuer. *Duck Left or run Right?*
|
|
| `pit` | 260 bis 339 | Du wirst gefressen. Du verlierst ein Leben.
|
|
| `escape` | 340 bis 410 | Das Gitter rollt davon, und du bist entkommen.
|
|
|===
|
|
|
|
Links ist sozusagen immer richtig: Links von `arrive` führt zu `sunset`, links von
|
|
`sunset` bringt dich zu `escape`. Rechts führt beide Male in die `pit`, und zu langes
|
|
Zögern ebenso.
|
|
|
|
[#_the_map_is_data_not_code]
|
|
=== Die Karte ist Daten, kein Code
|
|
|
|
Hier ist das ganze Spiel, und es ist eine Tabelle.
|
|
|
|
[source,lua]
|
|
----
|
|
local scenes = {
|
|
arrive = {
|
|
first = 0,
|
|
last = 119,
|
|
ask = 30,
|
|
prompt = "The dragon lands. Left or Right?",
|
|
left = "sunset",
|
|
right = "pit",
|
|
goesTo = "pit"
|
|
},
|
|
sunset = {
|
|
first = 120,
|
|
last = 259,
|
|
ask = 170,
|
|
prompt = "It breathes fire. Duck Left or run Right?",
|
|
left = "escape",
|
|
right = "pit",
|
|
goesTo = "pit"
|
|
},
|
|
pit = {
|
|
first = 260,
|
|
last = 339,
|
|
death = true,
|
|
goesTo = "arrive"
|
|
},
|
|
escape = {
|
|
first = 340,
|
|
last = 410
|
|
}
|
|
}
|
|
----
|
|
|
|
Eine Tabelle mit Namen statt Zahlen auf der linken Seite, genau wie in Lektion sechs, und
|
|
jeder Wert darin ist wieder eine Tabelle. Jede dieser inneren Tabellen ist ein
|
|
*Datensatz*: eine Szene, Feld für Feld beschrieben.
|
|
|
|
`scenes.arrive` ist die erste Szene. `scenes.arrive.first` ist `0`.
|
|
`scenes.arrive.left` ist der String `"sunset"`, und das ist der Name einer anderen Szene
|
|
in derselben Tabelle -- so werden die Gabelungen aufgeschrieben. Eine Szene enthält nicht
|
|
die Szene nach ihr. Sie enthält den *Namen* der Szene nach ihr, und der Name wird
|
|
nachgeschlagen, wenn es so weit ist.
|
|
|
|
Die Felder bedeuten Folgendes:
|
|
|
|
`first` und `last`:: Die Frames, zwischen denen die Szene läuft.
|
|
`ask`:: Der Frame, in dem die Frage erscheint. Der Spieler hat von `ask` bis `last` Zeit
|
|
zu antworten, und das ist der Timer.
|
|
`prompt`:: Was die Frage sagt.
|
|
`left` und `right`:: Die Szene, zu der die jeweilige Antwort führt.
|
|
`goesTo`:: Wohin die Szene geht, wenn niemand gewählt hat -- weil dem Spieler die Zeit
|
|
ausgegangen ist oder weil es gar keine Frage gab. Bei `pit`, einem Tod, ist es der Ort,
|
|
an den der neue Versuch zurückkehrt.
|
|
`death`:: Nur vorhanden, und dann `true`, bei einer Szene, die ein Leben kostet.
|
|
|
|
An dieser Tabelle sind drei Dinge bemerkenswert.
|
|
|
|
Das erste: Sie ist das Designdokument. Du kannst sie jemandem geben, der noch nie eine
|
|
Zeile Lua geschrieben hat, und er kann das Spiel aus ihr herauslesen. Wenn sich der
|
|
Drache dreifach gabeln soll, fügst du ein Feld hinzu. Wenn du eine weitere Szene willst,
|
|
fügst du einen weiteren Datensatz hinzu. Du suchst nicht nach dem Code, der Szenen
|
|
abspielt, denn den änderst du nicht.
|
|
|
|
Das zweite: Die Datensätze haben nicht alle dieselbe Form. `pit` hat kein `ask`, kein
|
|
`prompt` und kein `left` -- darin gibt es nichts zu wählen. `escape` hat kein `goesTo`,
|
|
denn es ist das Ende und führt nirgendwohin. Ein Feld, das nicht in einer Tabelle steht,
|
|
liest sich als `nil`, das Wort, das du in Lektion drei für „da ist nichts“ kennengelernt
|
|
hast, und nach `scenes.escape.goesTo` zu fragen ist kein Fehler. Es ist `nil`, und `nil`
|
|
ist eine Antwort. Code, der diese Datensätze liest, muss darauf vorbereitet sein, und
|
|
gleich siehst du die eine Zeile, die es ist.
|
|
|
|
Das dritte ist, was die Tabelle *nicht* enthält: Nichts darin zeichnet, spielt ab oder
|
|
wartet. Sie ist eine Beschreibung. Diese Trennung ist der ganze Trick dieser Lektion.
|
|
|
|
[#_one_function_that_plays_any_scene]
|
|
=== Eine Funktion, die jede Szene abspielt
|
|
|
|
Das ist die Code-Hälfte, und es gibt nur eine Funktion von nennenswerter Größe.
|
|
Geschrieben im Thread-Modell aus Lektion fünfzehn, denn nichts ist so sehr ein Ablauf
|
|
wie ein verzweigtes Spiel.
|
|
|
|
[source,lua]
|
|
----
|
|
local function playScene(name)
|
|
local scene = scenes[name]
|
|
|
|
discSkipToFrame(scene.first)
|
|
while discGetFrame() < scene.last do
|
|
local frame = discGetFrame()
|
|
local asking = scene.ask ~= nil and frame >= scene.ask
|
|
|
|
if not asking then
|
|
answer = nil
|
|
elseif answer == "left" or answer == "right" then
|
|
return scene[answer]
|
|
end
|
|
drawScene(scene, frame, asking)
|
|
singeYield()
|
|
end
|
|
discPause()
|
|
|
|
if scene.death then
|
|
lives = lives - 1
|
|
if lives == 0 then
|
|
ending = "The dragon wins. Game over."
|
|
return nil
|
|
end
|
|
waitForGo("Press the space bar to try again.")
|
|
end
|
|
|
|
return scene.goesTo
|
|
end
|
|
----
|
|
|
|
Sie nimmt den Namen einer Szene, spielt sie ab und gibt den Namen der Szene zurück, die
|
|
als Nächstes kommt. Das ist ihre ganze Abmachung, und das Spiel besteht dann aus drei
|
|
Zeilen:
|
|
|
|
[source,lua]
|
|
----
|
|
function singeMain()
|
|
local scene = "arrive"
|
|
|
|
while scene ~= nil do
|
|
scene = playScene(scene)
|
|
end
|
|
waitForGo(ending .. " Space to quit.")
|
|
end
|
|
----
|
|
|
|
Fang bei `arrive` an. Spiel die Szene ab, die du gerade in der Hand hast, und nimm in die
|
|
Hand, was sie dir zurückgibt. Wenn sie `nil` zurückgibt, gibt es keine nächste Szene, die
|
|
Schleife endet, und das Spiel ist vorbei.
|
|
|
|
[#_the_whole_thing_8]
|
|
=== Das Ganze
|
|
|
|
Leg `dragon.singe` in deinem Ordner `movie` an und tipp das hier ein. Es ist das bisher
|
|
längste Skript im Buch, und jedes Stück davon ist erklärt, bis auf die zwei kleinen
|
|
Hilfsfunktionen, die danach kommen.
|
|
|
|
[source,lua]
|
|
----
|
|
local FPS = 30
|
|
|
|
local scenes = {
|
|
arrive = {
|
|
first = 0,
|
|
last = 119,
|
|
ask = 30,
|
|
prompt = "The dragon lands. Left or Right?",
|
|
left = "sunset",
|
|
right = "pit",
|
|
goesTo = "pit"
|
|
},
|
|
sunset = {
|
|
first = 120,
|
|
last = 259,
|
|
ask = 170,
|
|
prompt = "It breathes fire. Duck Left or run Right?",
|
|
left = "escape",
|
|
right = "pit",
|
|
goesTo = "pit"
|
|
},
|
|
pit = {
|
|
first = 260,
|
|
last = 339,
|
|
death = true,
|
|
goesTo = "arrive"
|
|
},
|
|
escape = {
|
|
first = 340,
|
|
last = 410
|
|
}
|
|
}
|
|
|
|
local answer = nil
|
|
local ending = "You got out alive."
|
|
local lives = 3
|
|
|
|
|
|
local function drawScene(scene, frame, asking)
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Lives: " .. lives)
|
|
if scene.death then
|
|
overlayPrint(2, 4, "That did not go well.")
|
|
elseif asking then
|
|
overlayPrint(2, 4, scene.prompt)
|
|
overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.")
|
|
end
|
|
end
|
|
|
|
|
|
local function waitForGo(text)
|
|
answer = nil
|
|
while answer ~= "go" do
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Lives: " .. lives)
|
|
overlayPrint(2, 4, text)
|
|
singeYield()
|
|
end
|
|
end
|
|
|
|
|
|
local function playScene(name)
|
|
local scene = scenes[name]
|
|
|
|
discSkipToFrame(scene.first)
|
|
while discGetFrame() < scene.last do
|
|
local frame = discGetFrame()
|
|
local asking = scene.ask ~= nil and frame >= scene.ask
|
|
|
|
if not asking then
|
|
answer = nil
|
|
elseif answer == "left" or answer == "right" then
|
|
return scene[answer]
|
|
end
|
|
drawScene(scene, frame, asking)
|
|
singeYield()
|
|
end
|
|
discPause()
|
|
|
|
if scene.death then
|
|
lives = lives - 1
|
|
if lives == 0 then
|
|
ending = "The dragon wins. Game over."
|
|
return nil
|
|
end
|
|
waitForGo("Press the space bar to try again.")
|
|
end
|
|
|
|
return scene.goesTo
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
answer = "left"
|
|
elseif what == SWITCH_RIGHT then
|
|
answer = "right"
|
|
elseif what == SWITCH_BUTTON1 then
|
|
answer = "go"
|
|
end
|
|
end
|
|
|
|
|
|
function singeMain()
|
|
local scene = "arrive"
|
|
|
|
while scene ~= nil do
|
|
scene = playScene(scene)
|
|
end
|
|
waitForGo(ending .. " Space to quit.")
|
|
end
|
|
|
|
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
Starte es:
|
|
|
|
----
|
|
Singe -R -v Singe/menuBackground.mkv movie/dragon.singe
|
|
----
|
|
|
|
Spiel es vier- oder fünfmal. Geh zweimal nach links und entkomme. Geh nach rechts und
|
|
werde gefressen. Setz dich auf deine Hände und werde trotzdem gefressen. Verlier alle
|
|
drei Leben.
|
|
|
|
[#_what_just_happened_12]
|
|
=== Was gerade passiert ist
|
|
|
|
[source,lua]
|
|
----
|
|
discSkipToFrame(scene.first)
|
|
while discGetFrame() < scene.last do
|
|
----
|
|
|
|
Eine Szene ist ein Sprung, gefolgt von Warten, und das ist das Muster aus Lektion
|
|
fünfzehn, nur dass die Zahlen aus dem Datensatz kommen, statt eingetippt zu sein.
|
|
`discSkipToFrame` geht zu einem Frame und spielt ab dort ab, und genau das ist ein
|
|
Szenenwechsel.
|
|
|
|
Die Frame-Nummer stimmt in dem Augenblick, in dem der Aufruf zurückkehrt, also glaubt
|
|
das `while` nie, es sei noch in der Szene, die du gerade verlassen hast.
|
|
|
|
[source,lua]
|
|
----
|
|
local asking = scene.ask ~= nil and frame >= scene.ask
|
|
----
|
|
|
|
Die eine Zeile, die auf ein fehlendes Feld vorbereitet ist, und der Grund, warum die
|
|
Todesszene `ask` weglassen kann.
|
|
|
|
`and` heißt, dass beide Hälften wahr sein müssen, wie schon in Lektion drei, und Lua
|
|
prüft zuerst die linke Hälfte. Wenn `scene.ask` `nil` ist, ist die linke Hälfte falsch,
|
|
Lua hört dort auf, und der Vergleich auf der rechten Seite findet nie statt. Und das ist
|
|
auch gut so, denn `nil` mit einer Zahl zu vergleichen ist ein Fehler, und dem begegnest
|
|
du am Ende dieser Lektion.
|
|
|
|
`asking` ist die Antwort auf „Steht die Frage gerade auf dem Bildschirm?“, einmal pro
|
|
Frame ausgerechnet und dreimal benutzt.
|
|
|
|
[source,lua]
|
|
----
|
|
if not asking then
|
|
answer = nil
|
|
elseif answer == "left" or answer == "right" then
|
|
return scene[answer]
|
|
end
|
|
----
|
|
|
|
Solange die Frage nicht zu sehen ist, wird alles weggeworfen, was der Spieler drückt. Das
|
|
muss so sein, sonst beantwortet ein Spieler, der während des Films auf der linken
|
|
Pfeiltaste lehnt, eine Frage, die er noch gar nicht gelesen hat.
|
|
|
|
Sobald die Frage zu sehen ist, beendet eine Antwort die Szene sofort. `return scene[answer]`
|
|
ist einen zweiten Blick wert: `answer` enthält den String `"left"` oder `"right"`,
|
|
und `scene["left"]` ist dasselbe wie `scene.left`. Wenn du den Namen in eckige Klammern
|
|
setzt statt hinter einen Punkt, kannst du ein Feld nachschlagen, dessen Namen du in einer
|
|
Variablen hast. Zwei Zeilen Code für beide Antworten, und bei fünf Antworten wären es
|
|
immer noch zwei Zeilen.
|
|
|
|
Das `elseif` erklärt auch, warum nur `"left"` und `"right"` durchgelassen werden. Die
|
|
Leertaste setzt `answer` auf `"go"`, und `scene["go"]` ist `nil`, also würde ein
|
|
verirrter Druck auf die Leertaste während einer Frage das Spiel stillschweigend beenden.
|
|
Sag, was du annimmst.
|
|
|
|
[source,lua]
|
|
----
|
|
discPause()
|
|
----
|
|
|
|
Direkt nach der Schleife, und leicht zu vergessen. Die Szene hat ihren letzten Frame
|
|
erreicht, aber die Disc hat keine Ahnung, dass die Szene vorbei ist -- sie spielt einfach
|
|
weiter in das Material, das zufällig als Nächstes in der Datei liegt, und in einem echten
|
|
Spiel ist das die Mitte einer Szene von ganz woanders. Halte sie in dem Moment an, in dem
|
|
du sie nicht mehr willst.
|
|
|
|
[source,lua]
|
|
----
|
|
if scene.death then
|
|
lives = lives - 1
|
|
----
|
|
|
|
`scene.death` ist bei jeder Szene außer `pit` `nil`, und `nil` zählt in einem `if` als
|
|
falsch, also liest sich das als „wenn diese Szene ein Tod war“. Du musst `death = false`
|
|
nicht in die anderen drei Datensätze schreiben, und du solltest es auch nicht: Dass das
|
|
Feld fehlt, sagt es genauso gut, und dann gibt es nur eine Stelle, die entscheidet, welche
|
|
Szenen tödlich sind.
|
|
|
|
Der neue Versuch steckt in den zwei Zeilen darunter. Nimm ein Leben weg, und wenn noch
|
|
welche übrig sind, bleib auf dem Frame stehen und warte auf die Leertaste. Dann schickt
|
|
`return scene.goesTo` den Spieler zurück zu `arrive`, und das weiß der Datensatz. Ändere
|
|
`goesTo` in `"sunset"`, und aus dem Neustart wird ein Checkpoint -- ein Wort, und die
|
|
Schwierigkeit des Spiels ändert sich.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.")
|
|
----
|
|
|
|
Der Timer. `scene.last - frame` ist die Zahl der Frames, die noch übrig sind, und wenn du
|
|
durch die Frames in einer Sekunde teilst, werden daraus Sekunden. `math.ceil` rundet eine
|
|
Zahl auf die nächste ganze Zahl auf, also zeigt der Countdown `3`, `2`, `1` und nie `0.4`
|
|
oder `2.966666`.
|
|
|
|
`FPS` ist `30`, weil dieses Video mit etwas über dreißig Frames pro Sekunde läuft. Es ist
|
|
die einzige Zahl im Skript, die geschätzt ist, und sie ist eine benannte Schätzung oben in
|
|
der Datei statt einer `30`, die mitten in einer Zeile vergraben ist, damit es genau eine
|
|
Sache zu ändern gibt, wenn du das Video wechselst.
|
|
|
|
Achte darauf, woran der Timer gemessen wird: am Film. Der Spieler läuft gegen das Video
|
|
um die Wette, nicht gegen eine Uhr, also kann die Frage nie länger bleiben als die
|
|
Einstellung, zu der sie gehört. Das ist der Grund, warum verzweigte Spiele in Frames
|
|
zählen.
|
|
|
|
[#_why_not_write_it_out_by_hand]
|
|
=== Warum nicht alles von Hand ausschreiben
|
|
|
|
Du könntest dieses Spiel als eine lange Funktion schreiben. Spiel die Frames 0 bis 119
|
|
ab; wenn links, spiel 120 bis 259 ab; wenn wieder links, spiel 340 bis 410 ab; sonst
|
|
spiel 260 bis 339 ab, nimm ein Leben weg, geh zurück an den Anfang. Bei vier Szenen wäre
|
|
das kürzer als das, was du jetzt hast.
|
|
|
|
Bei vierzig Szenen wäre es eine Katastrophe, und es lohnt sich, genau zu sagen, warum.
|
|
|
|
Die Frame-Nummern wären über den ganzen Code verstreut, jede einmal hingeschrieben und
|
|
nie benannt, also hieße es, jede Stelle zu suchen, an der sie vorkommt, wenn du änderst,
|
|
wo eine Szene anfängt. Die Form der Geschichte wäre in der Form der `if`-Anweisungen
|
|
vergraben, also könnte niemand das Spiel sehen, ohne alles zu lesen. Und eine Szene
|
|
hinzuzufügen hieße, den Code zu ändern, der abspielt, also den Code, der schon
|
|
funktioniert -- jede neue Szene eine weitere Gelegenheit, eine alte kaputt zu machen.
|
|
|
|
Die Version, die du geschrieben hast, hat ein Stück Code, das Szenen abspielt, und ein
|
|
Stück Daten, das sagt, welche Szenen es gibt. Eine Szene hinzuzufügen berührt nur die
|
|
Daten. Das ist die zusätzliche halbe Stunde wert, und dieselbe Idee steckt auch hinter
|
|
der Datei `games.dat` aus Lektion dreizehn: Beschreib die Sache und schreib dann ein
|
|
Stück Code, das die Beschreibung liest.
|
|
|
|
[#_try_it_16]
|
|
=== Probier es aus
|
|
|
|
. *Füg ein Ende hinzu.* Gib `escape` eine Frage und eine zweite Endszene, die Frames von
|
|
irgendwo im Film wiederverwendet. Nur die Tabelle ändert sich.
|
|
. *Mach es fair.* Setz `ask` in beiden Szenen herunter, sodass der Spieler fünf Sekunden
|
|
statt drei hat, und dann herauf, sodass er eine hat. Finde die Zahl, die du wirklich
|
|
ausliefern würdest.
|
|
. *Verschieb den Checkpoint.* Ändere das `goesTo` von `pit` in `"sunset"` und spiel es.
|
|
Dann überleg, was passiert, wenn ein Spieler in `arrive` stirbt -- und ob die
|
|
Todesszene wissen sollte, woher sie kam, statt es gesagt zu bekommen.
|
|
. *Zähl, was der Spieler gesehen hat.* Leg neben `scenes` eine Tabelle an, die festhält,
|
|
welche Szenennamen schon gespielt wurden, und zeig die Anzahl in der Ecke. Jetzt hast du
|
|
ein Spiel, das sagen kann: „Du hast drei von fünf Enden gefunden.“
|
|
. *Mach die Karte kaputt.* Ändere das `left` von `arrive` in `"cave"`, eine Szene, die
|
|
es nicht gibt. Starte es, geh nach links und lies, was Lua sagt. Dieser Fehler ist der
|
|
häufigste, den ein datengesteuertes Spiel hat, und wenn du seine Form kennst, sparst
|
|
du dir eine Stunde.
|
|
|
|
[#_break_it_on_purpose_15]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Nimm die Absicherung aus der Zeile mit `asking` heraus, sodass sie so lautet:
|
|
|
|
[source,lua]
|
|
----
|
|
local asking = frame >= scene.ask
|
|
----
|
|
|
|
Starte es und geh nach rechts. Die Szene `arrive` läuft problemlos, denn `arrive` hat ein
|
|
`ask`. Dann beginnt die Szene `pit`, die keins hat, und das Spiel bleibt mit dieser
|
|
Meldung stehen:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': movie/dragon.singe:68: attempt to compare nil with number
|
|
stack traceback:
|
|
[C]: in function 'error'
|
|
Singe/Framework.singe:603: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
Lies es so, wie Lektion eins es dir beigebracht hat. Die Datei und die Zeile kommen
|
|
zuerst, und sie stimmen genau: Zeile 68 ist die Zeile, die du gerade geändert hast. Die
|
|
Beschwerde ist der Rest: Du hast Lua gefragt, ob eine Zahl größer als `nil` ist, und Lua
|
|
hat keine Ahnung. `scene.ask` ist `nil`, weil der Datensatz `pit` kein Feld `ask` hat,
|
|
und ein Feld, das nicht da ist, ist `nil`.
|
|
|
|
Ein Teil dieser Meldung verdient eine Erklärung, denn er ist eine Lüge durch Weglassen.
|
|
Die genannte Funktion ist `onOverlayUpdate`, und die hast du nicht geschrieben. Das ist
|
|
die eigene des Frameworks, die, von der Lektion fünfzehn gesagt hat, dass sie installiert
|
|
wird, um dein `singeMain` anzutreiben. Der Fehler kam durch sie heraus, also ist das der
|
|
Name, den die Engine gerade zur Hand hatte, und der Traceback darunter -- die Liste der
|
|
Funktionen, die liefen, die innerste zuerst -- kann nur zeigen, wie das Framework deinen Fehler
|
|
weiterreicht. Ignorier beides. Die Datei und die Zeile ganz oben sind deine, und sie sind die,
|
|
die stimmen.
|
|
|
|
[#_what_you_learned_16]
|
|
=== Was du gelernt hast
|
|
|
|
* Ein verzweigtes Spiel ist eine Menge von Szenen, und jede davon nennt die Szenen, die
|
|
nach ihr kommen.
|
|
* Halte die Szenen als Daten -- eine Tabelle von Datensätzen -- und der Code, der sie
|
|
abspielt, bleibt eine einzige Funktion, egal wie groß das Spiel wird.
|
|
* Ein Datensatz enthält den Namen der nächsten Szene, nicht die Szene selbst, und der
|
|
Name wird nachgeschlagen, wenn er gebraucht wird.
|
|
* Ein Feld, das nicht in einem Datensatz steht, ist `nil`, und das ist kein Fehler, aber
|
|
Code, der Datensätze liest, muss damit rechnen.
|
|
* `scene[answer]` schlägt das Feld nach, dessen Name in einer Variablen steht;
|
|
`scene.left` und `scene["left"]` sind dasselbe Feld.
|
|
* Häng den Timer an das Video: Eine Entscheidung dauert von einem Frame bis zu einem
|
|
anderen, also kann sie nie länger dauern als ihre eigene Einstellung.
|
|
* Wirf alles weg, was der Spieler gedrückt hat, bevor die Frage erschien.
|
|
* Halte die Disc an, sobald eine Szene endet, sonst spielt sie weiter in das Material der
|
|
nächsten Szene.
|
|
* Ein fehlendes Feld, das für falsch steht -- `death` bei drei von vier Datensätzen --
|
|
hält die Beschreibung kurz und ehrlich.
|
|
* `nil` mit einer Zahl zu vergleichen ist ein Fehler, und mit `and` vermeidest du ihn.
|
|
|
|
[#_next_time_16]
|
|
=== Beim nächsten Mal
|
|
|
|
Dein Spiel fragt nach links oder rechts. Die Arcade-Automaten, in denen diese Art von
|
|
Spiel groß geworden ist, fragten etwas Schwierigeres: Ziel auf das Ding auf dem
|
|
Bildschirm und schieß darauf. In Lektion siebzehn geht es um Lightguns -- wohin der
|
|
Spieler zielt, was das Spiel damit macht und die Kalibrierung, ohne die ein echter
|
|
Automat nicht auskommt.
|