607 lines
22 KiB
Text
607 lines
22 KiB
Text
[#_lesson_18_quick_time_events]
|
|
== Lektion 18: Quick-Time-Events
|
|
|
|
image::learn/18-qte.png[Die fertige Lektion, 480]
|
|
|
|
Eine Aufforderung erscheint. Du hast eine halbe Sekunde. Drück das Richtige, und die
|
|
Geschichte geht weiter; drück das Falsche oder gar nichts, und sie geht woandershin.
|
|
|
|
Das ist schon die ganze Idee, und erstaunlich viele Videospiele bestehen aus fast nichts
|
|
anderem. Jedes Teil hast du schon: Lektion vierzehn hat dir Frames gegeben, Lektion sechs
|
|
eine Liste von Datensätzen, Lektion drei Eingaben und Lektion sechzehn Verzweigungen. Diese
|
|
Lektion setzt sie zusammen und fügt eine Sache hinzu, die wirklich neu ist -- ein
|
|
Zeitfenster und das, was passiert, wenn es sich schließt.
|
|
|
|
[#_the_clock_is_the_film]
|
|
=== Die Uhr ist der Film
|
|
|
|
Alles hier wird in *Frames* gemessen, nicht in Sekunden, und dabei lohnt es sich
|
|
anzuhalten, denn das ist die eine Entscheidung, die das Ganze funktionieren lässt.
|
|
|
|
Lektion vierzehn hat gesagt, dass ein Frame die Adresse eines Augenblicks im Film ist. Die
|
|
Aufforderung muss erscheinen, wenn der Schauspieler zum Schlag ausholt, und das
|
|
Zeitfenster muss sich schließen, wenn die Faust ankommt. Das sind keine Zeiten, das sind
|
|
Stellen im Material, und du hast sie gefunden, indem du Schritt für Schritt hindurchgegangen
|
|
bist. Wenn du das Zeitfenster in Sekunden schreibst, musst du eine Stoppuhr starten, wenn
|
|
die Aufforderung erscheint, und dann können die beiden Uhren -- deine und die des Films --
|
|
auseinanderlaufen. Wenn das Video auf einem langsamen Rechner ruckelt, läuft deine
|
|
Stoppuhr weiter, und der Spieler wird für seine Hardware bestraft.
|
|
|
|
Frames können nicht auseinanderlaufen, denn es gibt nur eine Uhr. `discGetFrame()` ist
|
|
die Zeit, immer. Ein Zeitfenster sind zwei Frame-Nummern, und „wie viel ist noch übrig“
|
|
ist eine Subtraktion.
|
|
|
|
Starte mit demselben Video wie vorher:
|
|
|
|
----
|
|
Singe -R -v Singe/menuBackground.mkv moves
|
|
----
|
|
|
|
Immer noch das lila Gitter und der Sonnenuntergang. Stell dir einen Kampf vor.
|
|
|
|
[#_a_move_is_a_record]
|
|
=== Eine Aktion ist ein Datensatz
|
|
|
|
Hier ist die Tabelle, an der die ganze Lektion hängt:
|
|
|
|
[source,lua]
|
|
----
|
|
local moves = {
|
|
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
|
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
|
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
|
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
|
}
|
|
----
|
|
|
|
Vier Aktionen. Jede sagt, wann das Zeitfenster aufgeht, wann es sich schließt, welcher
|
|
Schalter die Antwort ist und was auf den Bildschirm kommt. Fünfundvierzig Frames sind bei
|
|
der Bildrate dieses Videos etwa anderthalb Sekunden, und das ist großzügig; echte Spiele
|
|
geben dir oft weniger als die Hälfte davon.
|
|
|
|
Alles, was eine Aktion zu einer Aktion macht, steht in dieser Tabelle. Nirgends weiter
|
|
unten gibt es Code, der weiß, dass es vier Aktionen gibt oder dass die zweite nach oben
|
|
geht oder dass das erste Zeitfenster bei Frame sechzig aufgeht. Eine fünfte Aktion
|
|
hinzuzufügen heißt, eine Zeile hinzuzufügen. Alle Aktionen neu zu timen heißt, Zahlen zu
|
|
ändern. Niemand muss eine Funktion lesen, um den Kampf zu ändern.
|
|
|
|
Das ist kein Trick nur für diese Lektion. Das ist der Unterschied zwischen einem Spiel, das
|
|
du feinabstimmen kannst, und einem, bei dem du es nicht kannst, und es kostet nichts, es
|
|
von Anfang an so zu machen. Wenn du merkst, dass du gerade zum vierten Mal
|
|
`if frame == 60 then` schreiben willst, hör auf und mach stattdessen eine Tabelle.
|
|
|
|
[#_what_happens_when]
|
|
=== Was wann passiert
|
|
|
|
Mit einer Aktion können drei Dinge passieren, und das sind drei verschiedene Ausgänge,
|
|
nicht zwei:
|
|
|
|
* Der Spieler drückt innerhalb des Zeitfensters den richtigen Schalter. Gib Punkte und
|
|
spiel weiter.
|
|
* Der Spieler drückt innerhalb des Zeitfensters einen *falschen* Schalter. Er hat es
|
|
versucht und ist gescheitert.
|
|
* Das Zeitfenster schließt sich ganz ohne Antwort. Er ist erstarrt.
|
|
|
|
Fauler Code behandelt die letzten beiden gleich. Spiele tun das fast nie, denn sie fühlen
|
|
sich beim Spielen völlig verschieden an: Falsch zu raten ist ein Fehler, und zu erstarren
|
|
ist ein anderer Fehler, und der Film geht meistens für jeden woandershin. Hier schickt die
|
|
falsche Antwort die Disc zu Frame `362` und keine Antwort zu Frame `388`, und bei diesem
|
|
Material heißt das zwei etwas verschiedene Mengen Sonnenuntergang. Bei echtem Material
|
|
wären es zwei verschiedene Einstellungen davon, wie du verlierst.
|
|
|
|
Also bekommt jeder Datensatz während des Durchgangs ein fünftes Feld -- `answer` -- mit
|
|
dem Wert `nil`, bis mit der Aktion etwas passiert, und dann einem von drei Strings:
|
|
|
|
[source,lua]
|
|
----
|
|
move.answer = "right"
|
|
move.answer = "wrong"
|
|
move.answer = "late"
|
|
----
|
|
|
|
`nil` ist der Wert, den Lua dir für ein Feld gibt, das niemand gesetzt hat. Du hast ihn
|
|
in Lektion drei kennengelernt. Hier verdient er sich seinen Platz: „noch keine Antwort“
|
|
ist wirklich ein anderer Zustand als jede der drei Antworten, und `nil` sagt das, ohne dass
|
|
du dir ein Wort dafür ausdenken musst.
|
|
|
|
[#_finding_the_move_that_is_being_asked]
|
|
=== Die Aktion finden, nach der gerade gefragt wird
|
|
|
|
Alles andere muss eine Sache wissen: Welche Aktion ist gerade offen, wenn überhaupt eine?
|
|
|
|
[source,lua]
|
|
----
|
|
local function moveAt(frame)
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame >= move.first and frame <= move.last then
|
|
return move
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
----
|
|
|
|
Geh die Liste durch und antworte mit der ersten Aktion, deren Zeitfenster diesen Frame
|
|
abdeckt und die noch nicht beantwortet ist. Passt nichts, antworte mit `nil`.
|
|
|
|
Dass hier absichtlich `nil` zurückgegeben wird, ist bemerkenswert. Eine Funktion, die
|
|
manchmal keine Antwort hat, sollte das sagen, und „keine Aktion ist offen“ ist eine
|
|
völlig gute Antwort. Der Aufrufer schreibt dann `if move ~= nil then`, und der Code, der
|
|
die Aufforderung zeichnet, läuft nie, wenn es nichts zum Auffordern gibt.
|
|
|
|
Der Test `move.answer == nil` verhindert, dass nach einer Aktion zweimal gefragt wird. In
|
|
dem Moment, in dem der Spieler sie richtig hat, verschwindet die Aufforderung, auch wenn
|
|
das Zeitfenster noch offen ist -- und genau das soll sie tun, denn er hat ja geantwortet.
|
|
|
|
[#_reading_the_answer]
|
|
=== Die Antwort lesen
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if not isDirection(what) then
|
|
return
|
|
end
|
|
|
|
local move = moveAt(discGetFrame())
|
|
if move == nil then
|
|
return
|
|
end
|
|
|
|
if what == move.switch then
|
|
move.answer = "right"
|
|
score = score + RIGHT_SCORE
|
|
else
|
|
move.answer = "wrong"
|
|
endRun("WRONG WAY", WRONG_FRAME)
|
|
end
|
|
end
|
|
----
|
|
|
|
Lies es von oben nach unten. Wenn der Schalter keine der vier Richtungen ist, geht er uns
|
|
nichts an -- raus. Wenn keine Aktion offen ist, bedeutet ein Druck auf eine Richtung
|
|
nichts -- raus. Sonst gibt es genau eine Aktion, mit der verglichen wird, und einen
|
|
Vergleich, den es zu machen gilt.
|
|
|
|
Diese beiden frühen `return`-Zeilen sind Wächter: Sie werfen die Fälle hinaus, die dich
|
|
nicht interessieren, damit der Code darunter immer nur in der einen Situation läuft, für
|
|
die er geschrieben wurde. Andersherum geschrieben, mit der eigentlichen Arbeit drei Ebenen
|
|
tief in drei `if`-Blöcken verschachtelt, sagt es dasselbe und ist viel schwerer zu lesen.
|
|
|
|
`isDirection` ist eine winzige Funktion, die nichts anderes tut, als die erste Zeile kurz
|
|
zu halten:
|
|
|
|
[source,lua]
|
|
----
|
|
local function isDirection(what)
|
|
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
|
end
|
|
----
|
|
|
|
Achte darauf, dass sie den Vergleich selbst zurückgibt, statt
|
|
`if ... then return true else return false end` zu sagen. Ein Vergleich ist schon `true`
|
|
oder `false`. Zu prüfen, ob etwas wahr ist, nur um dann zu sagen, dass es wahr ist, ist
|
|
eine Gewohnheit, die man früh ablegen sollte.
|
|
|
|
[#_the_window_closing]
|
|
=== Das Zeitfenster schließt sich
|
|
|
|
Niemand drückt etwas, und die Frames ziehen weiter vorbei. Irgendetwas muss das bemerken.
|
|
Dieses Etwas ist `onOverlayUpdate`, denn es läuft in jedem Frame, ob der Spieler etwas tut
|
|
oder nicht:
|
|
|
|
[source,lua]
|
|
----
|
|
if not runOver then
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame > move.last then
|
|
move.answer = "late"
|
|
endRun("TOO SLOW", LATE_FRAME)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
Eine Aktion, die nicht beantwortet ist und deren letzter Frame schon vorbei ist, wurde
|
|
verpasst. `break` verlässt die Schleife sofort, denn sobald der Durchgang vorbei ist, hat
|
|
es keinen Sinn, sich den Rest anzusehen.
|
|
|
|
`endRun` ist die Verzweigung, und es ist dieselbe Verzweigung, die du in Lektion sechzehn
|
|
geschrieben hast:
|
|
|
|
[source,lua]
|
|
----
|
|
local function endRun(reason, frame)
|
|
verdict = reason
|
|
runOver = true
|
|
discSkipToFrame(frame)
|
|
end
|
|
----
|
|
|
|
Drei Zeilen. Merk dir, was gesagt werden soll, merk dir, dass der Durchgang vorbei ist,
|
|
und schick den Film woandershin. Ein verpasstes Quick-Time-Event ist kein besonderer
|
|
Mechanismus. Es ist eine Entscheidung, die der Spieler getroffen hat, indem er keine
|
|
getroffen hat, und der Film verzweigt darauf genau so, wie er bei einer Tür verzweigt, die
|
|
der Spieler gewählt hat.
|
|
|
|
[#_a_gauge_that_drains]
|
|
=== Eine Anzeige, die sich leert
|
|
|
|
Der Spieler kann keine Frame-Nummern sehen. Er muss sehen, wie die Zeit abläuft, und er
|
|
muss es sehen, ohne von der Action wegzuschauen, und das heißt: eine Form, die sich
|
|
ändert, statt einer Zahl, die zählt.
|
|
|
|
Zwei Zahlen geben dir alles:
|
|
|
|
[source,lua]
|
|
----
|
|
local left = move.last - frame
|
|
local span = move.last - move.first
|
|
----
|
|
|
|
`span` ist, wie lang das Zeitfenster ist. `left` ist, wie viel davon noch übrig ist. Der
|
|
Balken ist `left` von `span` der vollen Breite:
|
|
|
|
[source,lua]
|
|
----
|
|
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
|
----
|
|
|
|
`//` ist eine Division, die den Nachkommateil wegwirft, und das willst du hier, denn einen
|
|
Balken, der hundertdreiunddreißig und ein Drittel Pixel breit ist, gibt es nicht. Multiplizier, bevor
|
|
du teilst: `GAUGE_W * left // span` und nicht `GAUGE_W * (left // span)`. Sonst wird
|
|
der Nachkommateil weggeworfen, solange er noch die einzige Information ist, die du hast,
|
|
und der Balken springt zwischen voll und leer hin und her, ohne irgendetwas dazwischen.
|
|
|
|
Ein gefülltes Rechteck gibt es im Overlay nicht. `overlayBox` zeichnet einen Umriss, und
|
|
den willst du für den Rahmen um die Anzeige, und die Füllung machst du selbst:
|
|
|
|
[source,lua]
|
|
----
|
|
local function fillBar(x, y, width, height)
|
|
if width < 1 then
|
|
return
|
|
end
|
|
for row = 0, height - 1 do
|
|
overlayLine(x, y + row, x + width - 1, y + row)
|
|
end
|
|
end
|
|
----
|
|
|
|
Achtzehn waagerechte Linien, übereinandergestapelt. Der Wächter oben ist wichtig: Im
|
|
allerletzten Frame des Zeitfensters ist `left` null, die Breite ist null, und ohne ihn ist
|
|
`x + width - 1` ein Pixel *links* von `x`, und du zeichnest eine kurze Linie rückwärts
|
|
statt gar nichts.
|
|
|
|
Der letzte Schliff ist ein Farbwechsel, wenn es eng wird:
|
|
|
|
[source,lua]
|
|
----
|
|
if left * 3 < span then
|
|
colorForeground(255, 60, 60, 255)
|
|
else
|
|
colorForeground(255, 200, 0, 255)
|
|
end
|
|
----
|
|
|
|
Im letzten Drittel wird der Balken rot. So geschrieben -- die übrige Zeit multiplizieren,
|
|
statt die Spanne zu teilen -- bleibt alles bei ganzen Zahlen und funktioniert für ein
|
|
Zeitfenster jeder Länge.
|
|
|
|
[#_the_whole_thing_10]
|
|
=== Das Ganze
|
|
|
|
[source,lua]
|
|
----
|
|
-- Programmieren lernen mit Singe -- Lektion 18: Quick-Time-Events
|
|
|
|
dofile("Singe/Framework.singe")
|
|
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
|
|
local FIRST_FRAME = 20
|
|
local WRONG_FRAME = 362
|
|
local LATE_FRAME = 388
|
|
local LAST_FRAME = 415
|
|
|
|
local PROMPT_COL = 54
|
|
local PROMPT_ROW = 22
|
|
local GAUGE_X = 210
|
|
local GAUGE_Y = 330
|
|
local GAUGE_W = 300
|
|
local GAUGE_H = 18
|
|
|
|
local RIGHT_SCORE = 100
|
|
|
|
local moves = {
|
|
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
|
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
|
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
|
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
|
}
|
|
|
|
local score = 0
|
|
local runOver = false
|
|
local verdict = ""
|
|
|
|
|
|
local function fillBar(x, y, width, height)
|
|
if width < 1 then
|
|
return
|
|
end
|
|
for row = 0, height - 1 do
|
|
overlayLine(x, y + row, x + width - 1, y + row)
|
|
end
|
|
end
|
|
|
|
|
|
local function isDirection(what)
|
|
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
|
end
|
|
|
|
|
|
local function moveAt(frame)
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame >= move.first and frame <= move.last then
|
|
return move
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
local function startRun()
|
|
score = 0
|
|
runOver = false
|
|
verdict = ""
|
|
for _, move in ipairs(moves) do
|
|
move.answer = nil
|
|
end
|
|
discSkipToFrame(FIRST_FRAME)
|
|
end
|
|
|
|
|
|
local function endRun(reason, frame)
|
|
verdict = reason
|
|
runOver = true
|
|
discSkipToFrame(frame)
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if runOver then
|
|
if what == SWITCH_START1 then
|
|
startRun()
|
|
end
|
|
return
|
|
end
|
|
|
|
if not isDirection(what) then
|
|
return
|
|
end
|
|
|
|
local move = moveAt(discGetFrame())
|
|
if move == nil then
|
|
return
|
|
end
|
|
|
|
if what == move.switch then
|
|
move.answer = "right"
|
|
score = score + RIGHT_SCORE
|
|
else
|
|
move.answer = "wrong"
|
|
endRun("WRONG WAY", WRONG_FRAME)
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local frame = discGetFrame()
|
|
|
|
if not runOver then
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame > move.last then
|
|
move.answer = "late"
|
|
endRun("TOO SLOW", LATE_FRAME)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
|
if not runOver then
|
|
verdict = "CLEAR"
|
|
runOver = true
|
|
end
|
|
discSearch(LAST_FRAME)
|
|
end
|
|
|
|
overlayClear()
|
|
|
|
local move = nil
|
|
if not runOver then
|
|
move = moveAt(frame)
|
|
end
|
|
|
|
if move ~= nil then
|
|
local left = move.last - frame
|
|
local span = move.last - move.first
|
|
|
|
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
|
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayBox(GAUGE_X - 2, GAUGE_Y - 2, GAUGE_X + GAUGE_W + 1, GAUGE_Y + GAUGE_H + 1)
|
|
|
|
if left * 3 < span then
|
|
colorForeground(255, 60, 60, 255)
|
|
else
|
|
colorForeground(255, 200, 0, 255)
|
|
end
|
|
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
|
end
|
|
|
|
overlayPrint(2, 1, "SCORE " .. score .. " FRAME " .. frame)
|
|
|
|
if runOver then
|
|
overlayPrint(2, 3, verdict)
|
|
overlayPrint(2, 4, "PRESS 1 TO RUN IT AGAIN")
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
startRun()
|
|
----
|
|
|
|
Die Pfeiltasten beantworten die Aufforderungen. Schaff alle vier, und der Film läuft bis
|
|
zum Ende und bleibt dort bei `CLEAR` stehen. Mach eine falsch oder setz dich auf deine
|
|
Hände, und er springt zu einem anderen Frame, mit einem anderen Wort auf dem Bildschirm.
|
|
Mit `1` geht es noch einmal los.
|
|
|
|
[#_what_just_happened_14]
|
|
=== Was gerade passiert ist
|
|
|
|
Das meiste hast du schon gelesen. Vier Stücke sind einen zweiten Blick wert.
|
|
|
|
[source,lua]
|
|
----
|
|
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
|
----
|
|
|
|
Das Ende des Films muss abgefangen werden, sonst läuft er über das Ende hinaus, und es gibt
|
|
nichts mehr zu sehen. `discSearch` geht zu einem Frame und *pausiert* dort, und genau das
|
|
will eine letzte Einstellung. Die Hälfte mit `discGetState()` verhindert, dass das in jedem
|
|
der folgenden Frames wieder passiert: Sobald die Disc pausiert ist, ist sie nicht mehr
|
|
`DISC_PLAYING`, also ist die Bedingung falsch, und der Sprung wird nicht sechzigmal pro
|
|
Sekunde wiederholt. `discGetState` und die Konstanten `DISC_*` stehen in der Referenz unter
|
|
`discGetState`.
|
|
|
|
[source,lua]
|
|
----
|
|
local move = nil
|
|
if not runOver then
|
|
move = moveAt(frame)
|
|
end
|
|
----
|
|
|
|
`onOverlayUpdate` fragt selbst noch einmal nach der offenen Aktion, statt es sich von
|
|
`onInputPressed` sagen zu lassen. Das ist Absicht. Es gibt genau eine Funktion, die weiß,
|
|
welche Aktion offen ist, und alle fragen sie. Käme die Aufforderung aus einer Variablen,
|
|
die der Eingabecode auf dem neuesten Stand hält, dann würden sich zwei Stücke Code
|
|
dieselbe Tatsache merken, und früher oder später läge eines davon falsch. Wenn du die Wahl
|
|
zwischen Fragen und Merken hast, frag.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
|
----
|
|
|
|
`overlayPrint` zählt in Zeichenzellen, wie schon seit Lektion eins, also ist die
|
|
Aufforderung nach Augenmaß platziert: Spalte vierundfünfzig liegt für Wörter dieser Länge
|
|
nah genug an der Mitte. Nah genug, nicht richtig, und „PRESS RIGHT“ endet drei Zeichen
|
|
weiter rechts als „PRESS UP“. Lektion neunzehn lädt eine echte Schriftart und mit ihr die
|
|
Möglichkeit, einen String in Pixeln auszumessen und richtig zu zentrieren.
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if runOver then
|
|
if what == SWITCH_START1 then
|
|
startRun()
|
|
end
|
|
return
|
|
end
|
|
----
|
|
|
|
Ein Callback, zwei völlig verschiedene Aufgaben, ausgewählt durch `runOver`. Während der
|
|
Durchgang läuft, bedeuten die Richtungen Aktionen; sobald er vorbei ist, bedeutet nur noch
|
|
die Starttaste etwas. Eine Variable, die entscheidet, welche Regeln gelten, ist ein
|
|
*Zustand*, und so hat Lektion zwölf das auch genannt, und ein Spiel besteht zum größten
|
|
Teil aus einer kleinen Zahl davon.
|
|
|
|
[#_try_it_18]
|
|
=== Probier es aus
|
|
|
|
. *Mach es schwerer.* Ändere das `last` der ersten Aktion von `105` in `75`. Fünfzehn
|
|
Frames sind eine halbe Sekunde. Spiel es und finde heraus, ob du das schaffst.
|
|
. *Füg eine Aktion hinzu.* Schreib eine fünfte Zeile in die Tabelle, mit einem
|
|
Zeitfenster zwischen den Frames `370` und `400`, und setz `LAST_FRAME` auf `418` hoch.
|
|
Keine andere Zeile solltest du anfassen müssen. Wenn doch, weiß irgendetwas im Code zu
|
|
viel über die Tabelle.
|
|
. *Bewerte die Geschwindigkeit.* Gib mehr Punkte für frühes Antworten. Rechne in
|
|
`onInputPressed`, dort, wo die richtige Antwort Punkte bekommt, `left` und `span` aus
|
|
`move` und `discGetFrame()` so aus wie die Anzeige, und addiere
|
|
`RIGHT_SCORE * left // span` statt `RIGHT_SCORE`. Entscheide, ob das Spiel dadurch
|
|
besser wird oder nur hektischer.
|
|
. *Gib einer falschen Antwort ihre eigene Folge.* Statt den Durchgang zu beenden, lass
|
|
einen falschen Druck fünfzig Punkte kosten und das Zeitfenster für einen neuen Versuch
|
|
offen. Du musst aufhören, `move.answer` zu setzen, und dabei begegnest du dem Grund,
|
|
warum es überhaupt gesetzt wurde.
|
|
. *Zwei Aufforderungen auf einmal.* Lass die Zeitfenster zweier Aktionen sich überlappen
|
|
und sieh, was passiert. `moveAt` gibt den ersten Treffer zurück, also wird immer nur nach
|
|
einer gefragt. Entscheide, was du willst und was es dafür bräuchte.
|
|
|
|
[#_break_it_on_purpose_17]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Lösch das `break` aus der Schleife, die verpasste Aktionen findet, und verpass die erste
|
|
mit Absicht. Der Durchgang endet, die Disc springt, und alles sieht richtig aus.
|
|
|
|
Setz jetzt `debugPrint(move.name .. " missed")` neben `move.answer = "late"` und mach es
|
|
noch einmal. Die Konsole zeigt eine Zeile, wie zu erwarten.
|
|
|
|
Dann nimm auch noch das `if not runOver then` um die Schleife heraus und verpass wieder
|
|
die erste Aktion:
|
|
|
|
----
|
|
LEFT missed
|
|
UP missed
|
|
RIGHT missed
|
|
DOWN missed
|
|
----
|
|
|
|
Die erste Zeile kommt, wenn sich das Zeitfenster schließt, wie vorher. Die anderen drei
|
|
kommen zusammen im allernächsten Frame, in dem Augenblick, in dem die Disc bei 388 landet:
|
|
Jedes übrige Zeitfenster liegt jetzt in der Vergangenheit, also wird jede unbeantwortete
|
|
Aktion auf einmal verpasst, und jede ruft `endRun` auf und springt noch einmal. Das Spiel
|
|
funktioniert immer noch mehr oder weniger, und genau das macht es gefährlich. Drei
|
|
zusätzliche Sprünge in einem einzigen Frame, dreimal wird das Urteil überschrieben, und
|
|
nichts auf dem Bildschirm sagt es dir.
|
|
|
|
Auch hier gibt es keine Fehlermeldung. Die Lehre ist, dass dieses `break` und dieser
|
|
Wächter keine Ordnungsliebe sind. Sie sind die zwei Zeilen, die sagen „das ist schon
|
|
entschieden“, und Code, der etwas immer wieder entscheidet, das schon entschieden ist, ist
|
|
eine der zuverlässigsten Quellen von Bugs, die du je schreiben wirst.
|
|
|
|
[#_what_you_learned_18]
|
|
=== Was du gelernt hast
|
|
|
|
* Ein Quick-Time-Event ist eine Aufforderung, ein Zeitfenster und eine Folge.
|
|
* Miss das Zeitfenster in Frames, denn der Film ist die Uhr, und es darf immer nur eine
|
|
Uhr geben.
|
|
* Halte die Aktionen in einer Tabelle von Datensätzen, damit eine Aktion hinzuzufügen
|
|
heißt, eine Zeile hinzuzufügen.
|
|
* Richtig, falsch und keine Antwort sind drei Ausgänge, und sie führen meistens an drei
|
|
verschiedene Orte.
|
|
* `nil` in einem Feld eines Datensatzes ist ein echter Zustand: Noch hat niemand
|
|
geantwortet.
|
|
* Eine Funktion beantwortet „Welche Aktion ist offen?“, und alle fragen sie, statt eine
|
|
Kopie aufzuheben.
|
|
* Wächter -- ein frühes `return` für die Fälle, die dich nicht interessieren -- halten
|
|
die eigentliche Arbeit unverschachtelt.
|
|
* `//` teilt und wirft den Nachkommateil weg; multiplizier, bevor du teilst.
|
|
* `overlayBox` zeichnet einen Umriss, also ist ein gefüllter Balken ein Stapel von
|
|
`overlayLine`-Aufrufen.
|
|
* Eine verpasste Aktion ist eine Verzweigung, und `discSkipToFrame` nimmt sie, genau wie
|
|
in Lektion sechzehn.
|
|
|
|
[#_next_time_18]
|
|
=== Beim nächsten Mal
|
|
|
|
Teil drei ist fertig. Du kannst Film abspielen, auf ihn warten, an ihm verzweigen, auf ihn
|
|
zielen und den Spieler gegen ihn antreten lassen. Was du über all das geschrieben hast, ist
|
|
die Konsolenschrift der Engine, die sechs Pixel breit ist und für Diagnosen gedacht. In
|
|
Lektion neunzehn lädst du eine echte Schrift, misst sie aus und sorgst dafür, dass die
|
|
Wörter auf dem Bildschirm so aussehen, als gehörten sie zum Spiel.
|