510 lines
22 KiB
Text
510 lines
22 KiB
Text
[#_lesson_12_score_lives_and_game_over]
|
|
== Lektion 12: Punkte, Leben und Game Over
|
|
|
|
image::learn/12-score.png[Die fertige Lektion, 480]
|
|
|
|
Dein Spiel zählt Punkte, nimmt Leben und sagt GAME OVER. Es ist fast ein Spiel. Drei Dinge fehlen
|
|
noch. Es hat keine Eingangstür: Sobald es geladen ist, steckt es mitten in der Action. Es vergisst:
|
|
Beende es, und die beste Punktzahl, die je jemand erreicht hat, geht mit. Und es merkt sich, was es
|
|
gerade tut, mit zwei Wahr-oder-falsch-Variablen, `over` und `dying`, und das ist die Falle, vor der
|
|
dich Lektion sieben gewarnt hat.
|
|
|
|
Diese Lektion behebt alle drei. Eine Variable dafür, was das Spiel gerade tut, ein
|
|
Titelbildschirm, von dem aus es losgeht, ein Highscore, der morgen noch da ist, und ein zweites
|
|
Spiel, das genau wie das erste anfängt.
|
|
|
|
[#_a_game_is_always_in_exactly_one_state]
|
|
=== Ein Spiel ist immer in genau einem Zustand
|
|
|
|
Sieh irgendeinem Arcade-Automaten zwei Minuten lang zu. Er zeigt einen Titel und wartet auf
|
|
jemanden. Jemand drückt Start, und er spielt. Dem Spieler gehen die Leben aus, und der Automat sagt
|
|
das, dann wartet er wieder. Der Automat tut immer genau eins dieser Dinge.
|
|
|
|
Dodge in Lektion sieben hat das in einer Variablen festgehalten, die ein Wort hielt: `"waiting"`,
|
|
`"playing"` oder `"over"`. Das Spiel aus Lektion neun ist zu einem `over` zurückgekehrt, das nur
|
|
wahr oder falsch sein kann, und Lektion zehn hat `dying` danebengestellt. Zähl, was zwei davon
|
|
sagen können. Jede ist `true` oder `false`, also ergeben sie zusammen vier Kombinationen. Drei
|
|
bedeuten etwas: Spielen, Sterben und Vorbei. Die vierte, beide auf einmal, ist Unsinn, und dein
|
|
Code muss sorgfältig darauf achten, sie nie zu erzeugen. Er achtet darauf: Sieh dir `endGame` an,
|
|
das `dying` zurück auf `false` setzt, bevor es `over` auf `true` setzt. Jetzt füg einen
|
|
Titelbildschirm hinzu. Das ist ein drittes Flag, acht Kombinationen, und vier davon Unsinn. Das ist
|
|
genau die Falle aus Lektion sieben, und hier gehen kleine Spiele sterben.
|
|
|
|
Mach es also wie Dodge. Vier Zustände, eine Variable. Schreib die Namen der Zustände unter deine
|
|
anderen Konstanten:
|
|
|
|
[source,lua]
|
|
----
|
|
local TITLE = "title"
|
|
local PLAYING = "playing"
|
|
local DYING = "dying"
|
|
local OVER = "over"
|
|
----
|
|
|
|
Dann nimm `over` und `dying` aus deinen Variablen heraus und setz eine Zeile an ihre Stelle:
|
|
|
|
[source,lua]
|
|
----
|
|
local state = TITLE
|
|
----
|
|
|
|
Dodge hat die Wörter selbst benutzt. Namen in Großbuchstaben sind nicht schlauer, und sie fangen
|
|
keinen Tippfehler ab: Lua behandelt einen falsch geschriebenen Namen als `nil` und ein falsch
|
|
geschriebenes Wort als ein Wort, zu dem nichts passt, und beides gibt dir ein Spiel, das
|
|
stillschweigend nicht mehr reagiert. Was dir die Namen einbringen, ist eine Liste. Jeder Zustand,
|
|
in dem das Spiel sein kann, steht einmal aufgeschrieben oben in der Datei, wo du alle vier auf
|
|
einen Blick siehst.
|
|
|
|
Überall, wo das Spiel früher ein Flag gesetzt hat, setzt es jetzt den Zustand. In `updateRocks`,
|
|
wo das letzte Leben verloren geht:
|
|
|
|
[source,lua]
|
|
----
|
|
if lives <= 0 then
|
|
state = DYING
|
|
deathChannel = channel
|
|
----
|
|
|
|
In `endGame` werden aus zwei Zeilen eine, und die unsinnige Kombination ist weg, weil es nichts
|
|
mehr zu kombinieren gibt:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
deathChannel = -1
|
|
state = OVER
|
|
musicStop(theme, 1500)
|
|
end
|
|
----
|
|
|
|
Und in `startGame` werden die beiden Zeilen, die beide Flags zurückgesetzt haben, zu
|
|
`state = PLAYING`.
|
|
|
|
[#_one_place_that_decides]
|
|
=== Eine Stelle, die entscheidet
|
|
|
|
In jedem Frame trifft `onOverlayUpdate` die beiden Entscheidungen, auf die es ankommt: was sich
|
|
bewegt und welche Wörter obendrauf kommen. Was sich bewegt, ist ein einziger Test:
|
|
|
|
[source,lua]
|
|
----
|
|
if state == PLAYING then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
----
|
|
|
|
Das ersetzt `if not over and not dying then`, und es sagt, was es meint. Auf dem
|
|
Titelbildschirm, während die letzte Explosion spielt, und wenn das Spiel vorbei ist, bewegt sich
|
|
nichts. Das bekommst du, indem du die Funktionen nicht aufrufst, die Dinge bewegen. Ein Spiel
|
|
anzuhalten ist kein besonderes Feature; es ist Weglassen.
|
|
|
|
Die Welt wird trotzdem in jedem Frame gezeichnet, egal in welchem Zustand, genau wie Dodge sie
|
|
gezeichnet hat. Nur die Wörter ändern sich. Ersetz die Zeile `drawHud()` hierdurch:
|
|
|
|
[source,lua]
|
|
----
|
|
if state == TITLE then
|
|
drawTitle()
|
|
else
|
|
drawHud()
|
|
end
|
|
----
|
|
|
|
`drawTitle` ist neu, und es kommt nach `drawStars`:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawTitle()
|
|
overlayPrint(25, 6, "R O C K S")
|
|
overlayPrint(25, 9, "BEST " .. best)
|
|
overlayPrint(22, 12, "PRESS 1 TO PLAY")
|
|
end
|
|
----
|
|
|
|
`best` ist die bisher beste Punktzahl, und sie kommt im nächsten Abschnitt dazu; bis dahin läuft
|
|
das Spiel nicht. Vor dem ersten Spiel ist die Liste der Felsen leer, also zeigt der
|
|
Titelbildschirm die vorbeiziehenden Sterne und das Schiff, das unten wartet, und mehr braucht ein
|
|
Titelbildschirm nicht.
|
|
|
|
Eine Zeile muss weg, sonst siehst du den Titel nie. Lösch das `startGame()` ganz unten in der
|
|
Datei. Das Spiel beginnt jetzt auf seinem Titelbildschirm, und *Neu anfangen* sagt, warum diese
|
|
Zeile wichtig ist.
|
|
|
|
`drawHud` stellt eine eigene Frage. Ändere sein `if over then` in:
|
|
|
|
[source,lua]
|
|
----
|
|
if state == OVER then
|
|
----
|
|
|
|
Der Eingabe-Callback braucht dieselbe Art von Nachdenken. Hier ist er vollständig:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
goLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
goRight = true
|
|
elseif what == SWITCH_BUTTON1 and state == PLAYING then
|
|
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
|
|
soundPlay(shootClip, 0, 35)
|
|
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
|
|
soundPlay(clickClip)
|
|
startGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
Lies ihn von oben. Die Pfeiltasten werden in jedem Zustand festgehalten, denn sie sagen nur,
|
|
welche Tasten unten sind, und nichts bewegt das Schiff, außer der Zustand ist `PLAYING`. Feuern
|
|
bedeutet nur beim Spielen etwas. Und `1` startet ein Spiel vom Titelbildschirm oder vom
|
|
Game-Over-Bildschirm aus, aber nicht, solange die letzte Explosion noch klingt: Das Spiel ist erst
|
|
vorbei, wenn es das sagt. Die Klammern funktionieren wie beim Rechnen. Das `or` darin wird zuerst
|
|
ausgerechnet, und dann verbindet das `and` es mit der Taste.
|
|
|
|
[#_score_and_lives]
|
|
=== Punkte und Leben
|
|
|
|
Beides hast du seit Lektion neun, und daran ändert sich kaum etwas. Neu ist die beste Punktzahl,
|
|
und die fängt als gewöhnliche Variable an. Stell sie zu den anderen, unter `lives`:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = 0
|
|
----
|
|
|
|
Zeig sie in der Zeile oben quer über den Bildschirm, zwischen den anderen beiden. In `drawHud`:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(1, 1, "SCORE " .. score)
|
|
overlayPrint(25, 1, "BEST " .. best)
|
|
overlayPrint(50, 1, "LIVES " .. lives)
|
|
----
|
|
|
|
Und gib `endGame` die Aufgabe, einen Rekord zu bemerken:
|
|
|
|
[source,lua]
|
|
----
|
|
if score > best then
|
|
best = score
|
|
end
|
|
----
|
|
|
|
Starte es. Der Titel sagt `BEST 0`. Drück `1`, schieß etwas ab, verlier, und der
|
|
Game-Over-Bildschirm zeigt deine Punktzahl als die beste. Drück wieder `1`, und die beste bleibt,
|
|
während die Punkte von vorn anfangen. Jetzt beende es, starte es noch einmal und sieh dir den Titel
|
|
an: `BEST 0`. Der Rekord hat genau so lange gelebt wie das Programm. Das ist die zweite Hälfte
|
|
dieser Lektion.
|
|
|
|
Wo du gerade bei den Leben bist: Lektion neun hat den Test als `lives <= 0` geschrieben, nicht als
|
|
`lives == 0`, und dabei sollte es bleiben. Heute kann nichts zwei Leben auf einmal nehmen, also
|
|
können die beiden nur übereinstimmen. An dem Tag, an dem du einen Felsen einbaust, der zwei
|
|
kostet, steigt `== 0` glatt über die Null hinweg, und der Spieler spielt mit minus einem Leben
|
|
weiter, während `<= 0` weiter funktioniert. Frag nach dem, was du meinst.
|
|
|
|
[#_starting_again]
|
|
=== Neu anfangen
|
|
|
|
Das `startGame()` unten in der Datei hast du schon gelöscht. In Lektion zehn hat diese Zeile das
|
|
erste Spiel gestartet und das Drücken von `1` jedes spätere, und das sind zwei Wege hinein. Jetzt
|
|
gibt es einen. Jedes Spiel, das erste eingeschlossen, beginnt, wenn jemand `1` drückt und
|
|
`startGame` läuft, also fangen das erste und das fünfte Spiel genau gleich an.
|
|
|
|
`startGame` muss alles zurücksetzen, was ein Spiel verändert:
|
|
|
|
[source,lua]
|
|
----
|
|
function startGame()
|
|
shipX = (screenWidth - shipWidth) / 2
|
|
score = 0
|
|
lives = START_LIVES
|
|
state = PLAYING
|
|
deathChannel = -1
|
|
shots = {}
|
|
for i = 1, ROCK_COUNT do
|
|
rocks[i] = {}
|
|
newRock(rocks[i])
|
|
end
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
end
|
|
----
|
|
|
|
Genauso wichtig ist zu wissen, was es in Ruhe lässt, und warum. `best` überlebt jedes Spiel; das
|
|
ist seine ganze Aufgabe. Die Sterne sind Kulisse. Und `goLeft` und `goRight` gehören überhaupt
|
|
nicht dem Spiel. Sie gehören der Tastatur, und nur die Tastatur sollte sie ändern.
|
|
|
|
Das Letzte ist ein Fehler, den du gerade behoben hast, ohne es zu merken. Der Eingabe-Callback aus
|
|
Lektion zehn fing mit `if over then` an, und solange das Spiel vorbei war, hat er jede Taste außer
|
|
`1` weggeworfen. Drück die Pfeiltaste nach rechts, während GAME OVER zu sehen ist, halt sie
|
|
gedrückt und drück `1`. Das neue Schiff rührt sich nicht: Das Drücken der Pfeiltaste wurde
|
|
weggeworfen, das Loslassen ist noch nicht passiert, und so ist `goRight` gleich `false`, während
|
|
die Taste eindeutig unten ist. Das Schiff bleibt stehen, bis du loslässt und noch einmal drückst.
|
|
Der neue Callback hält die Pfeiltasten in jedem Zustand fest, also ist die gehaltene Taste immer
|
|
noch gehalten, wenn das Spiel beginnt.
|
|
|
|
Fehler beim Neuanfangen sind die häufigsten Fehler in jedem Spiel, und sie fühlen sich beim
|
|
Spielen alle gleich an: Das zweite Spiel ist auf subtile Weise falsch, und das erste war in
|
|
Ordnung. Wenn das passiert, lies `startGame` und stell zwei Fragen. Was hat das letzte Spiel
|
|
geändert, das dieses nicht zurücksetzt? Und was setzt dieses zurück, das das Spiel nie hätte
|
|
anfassen dürfen?
|
|
|
|
[#_a_number_that_outlives_the_program]
|
|
=== Eine Zahl, die das Programm überlebt
|
|
|
|
Alles, was dein Spiel bisher festgehalten hat -- `score`, `lives`, die Liste der Felsen -- lebt im
|
|
Speicher, und der Speicher ist in dem Moment weg, in dem das Programm aufhört.
|
|
|
|
Singe gibt jedem Spiel einen eigenen Spielstand: eine Menge von Namen mit Werten darunter, für dich
|
|
auf die Festplatte geschrieben. Er ist ein weiterer Teil des Spiele-Kits aus Lektion elf, also
|
|
dieselbe Art von Ding wie `collideRects`: schlichte Aufrufe, nichts zu laden oder freizugeben.
|
|
|
|
[source,lua]
|
|
----
|
|
saveSet("highScore", 4200)
|
|
----
|
|
|
|
`saveSet` nimmt einen Namen -- den *Schlüssel* -- und einen Wert. Den Schlüssel suchst du dir aus,
|
|
und er ändert sich nie; der Wert ist das, was du dir merken willst. Zahlen, Strings, `true` und
|
|
`false` und ganze Tabellen davon passen alle hinein. Dinge, die nur Sinn ergeben, solange das
|
|
Programm läuft, passen nicht. Ein Sprite-Handle ist nur eine Zahl, also lässt es sich speichern,
|
|
und morgen ist es eine Zahl, die nichts bedeutet. Eine Funktion lässt sich überhaupt nicht
|
|
aufschreiben, und der Versuch, eine zu speichern, hält das Spiel mit einem Fehler an, statt dich
|
|
das erst nächste Woche herausfinden zu lassen.
|
|
|
|
Das Zurücklesen ist die andere Hälfte. Ändere die Zeile, die du für `best` eingefügt hast:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore", 0)
|
|
----
|
|
|
|
Sie läuft einmal, wenn das Spiel startet. Das zweite Argument ist der *Standardwert*: was
|
|
geantwortet wird, wenn unter diesem Schlüssel noch nie etwas gespeichert wurde. Gleich mehr dazu,
|
|
denn darum dreht sich der ganze Fehler dieser Lektion.
|
|
|
|
Und lass `endGame` den Rekord aufschreiben, wenn er fällt:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
deathChannel = -1
|
|
state = OVER
|
|
musicStop(theme, 1500)
|
|
if score > best then
|
|
best = score
|
|
saveSet("highScore", best)
|
|
saveFlush()
|
|
end
|
|
end
|
|
----
|
|
|
|
Du musst Singe nicht bitten, die Datei zu schreiben. Es schreibt sie einmal am Ende jedes Frames,
|
|
in dem sich etwas geändert hat, egal wie viele Schlüssel du setzt, und noch einmal, wenn das Spiel
|
|
herunterfährt. `saveFlush` schreibt sie stattdessen sofort: Ein neuer Highscore ist einen eigenen
|
|
Schreibvorgang wert, denn ein Arcade-Automat wird an der Steckdose ausgeschaltet, und ein Frame
|
|
ist eine lange Zeit.
|
|
|
|
Starte es, stell eine Punktzahl auf, beende es und starte es noch einmal. Der Titel erinnert sich.
|
|
|
|
Der Abschnitt Spielstand in der Referenz listet den Rest der Familie auf. `saveDelete` vergisst
|
|
einen Schlüssel, `saveClear` vergisst alles -- das ist das „Highscores zurücksetzen“ in einem
|
|
Service-Menü -- und `saveGetAll` gibt dir eine Kopie von allem, und das ist nützlich, wenn du den
|
|
ganzen Spielstand ausgeben willst, während du nach etwas suchst.
|
|
|
|
[#_where_the_file_actually_goes]
|
|
=== Wo die Datei wirklich landet
|
|
|
|
Sie landet nicht neben deinem Skript, und das ist kein Zufall.
|
|
|
|
Ein fertiges Spiel liegt vielleicht auf einer Disc, auf einer Netzwerkfreigabe oder in einer
|
|
einzigen gepackten Datei (Lektion dreißig), und auf nichts davon kann man schreiben. Ein Spiel, das
|
|
neben seinem eigenen Skript speichert, funktioniert perfekt auf dem Rechner, auf dem du es
|
|
geschrieben hast, und scheitert auf der Hälfte der Rechner, auf denen es installiert wird. Singe 2
|
|
hat es so gemacht, und das war falsch.
|
|
|
|
Darum kommt alles, was Singe für ein Spiel schreibt -- der Spielstand, Screenshots, sein Log -- an
|
|
einen Ort, und `singeGetDataPath` sagt dir, wohin:
|
|
|
|
[source,lua]
|
|
----
|
|
debugPrint(singeGetDataPath())
|
|
----
|
|
|
|
Schreib das kurz direkt unter die Zeile mit `dofile` und starte es. Es sagt `data/rocks/`: der
|
|
Ordner `data`, der in deinem Arbeitsordner aufgetaucht ist, als du in Lektion eins zum allerersten
|
|
Mal etwas gestartet hast, und darin ein Ordner mit demselben Namen wie der Ordner deines Spiels.
|
|
Dort findest du `save.json`. Öffne die Datei in deinem Texteditor: Es ist reiner Text, und dein
|
|
Highscore steht darin unter dem Namen, den du ihm gegeben hast.
|
|
|
|
Das ist auch die Antwort, wann immer du eine eigene Datei brauchst. Bau den Namen aus
|
|
`singeGetDataPath()` zusammen, und sie landet irgendwo, wo man schreiben kann; schreib in einen
|
|
Namen, den du dir selbst ausgedacht hast, und früher oder später tut sie das nicht.
|
|
|
|
[#_the_first_time_there_is_nothing]
|
|
=== Beim ersten Mal ist nichts da
|
|
|
|
Wenn irgendjemand dein Spiel zum allerersten Mal startet, wurde noch nie ein Highscore gespeichert.
|
|
`saveGet` hat nichts zurückzugeben, also gibt es den Standardwert zurück, den du ihm gegeben hast:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore", 0)
|
|
----
|
|
|
|
Nichts gespeichert, also ist `best` gleich `0`, der Titelbildschirm sagt `BEST 0`, und das erste
|
|
Spiel, das irgendjemand spielt, stellt einen Rekord auf. Genau das willst du, und es kostet ein
|
|
zusätzliches Argument.
|
|
|
|
Lass die `0` weg, und `saveGet` hat keinen Standardwert, auf den es zurückgreifen kann, also
|
|
antwortet es `nil` -- der Wert, mit dem Lua „hier ist nichts“ sagt und den du in Lektion drei
|
|
kennengelernt hast. `nil` ist nicht null. Du kannst nichts dazuzählen, du kannst es nicht mit einer
|
|
Zahl vergleichen, und du kannst es nicht an einen String kleben. Es geht im ersten Frame schief,
|
|
jedes Mal, auf jedem Rechner, auf dem das Spiel noch nicht gespielt wurde -- und dazu gehört jeder
|
|
Rechner, auf dem deine Spieler es gleich installieren, aber nicht der, auf dem du testest.
|
|
|
|
Gib jedem `saveGet` einen Standardwert. Das ist die billigste Angewohnheit in diesem Buch.
|
|
|
|
[#_what_just_happened_8]
|
|
=== Was gerade passiert ist
|
|
|
|
[source,lua]
|
|
----
|
|
local TITLE = "title"
|
|
----
|
|
|
|
Eine Konstante: ein Name, den du einmal setzt und nie änderst, in Großbuchstaben, damit du auf
|
|
einen Blick siehst, dass es eine ist, wie in Lektion sieben. Lua erzwingt das nicht -- nichts
|
|
hindert dich daran, `TITLE` etwas zuzuweisen -- also sind die Großbuchstaben eine Nachricht an den
|
|
Nächsten, der es liest, und das bist du in zwei Wochen.
|
|
|
|
[source,lua]
|
|
----
|
|
if state == PLAYING then
|
|
----
|
|
|
|
Die Frage, die entscheidet, ob sich die Welt bewegt. Wenn du später einen fünften Zustand
|
|
hinzufügst -- eine Pause oder eine Bonusrunde -- fragst du als Erstes, ob sich die Welt darin
|
|
bewegt, und hierhin kommt die Antwort.
|
|
|
|
[source,lua]
|
|
----
|
|
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
|
|
----
|
|
|
|
Ohne die Klammern würde Lua zuerst das `and` verbinden und das als „die Taste ist `1` und der
|
|
Zustand ist der Titel, oder der Zustand ist vorbei“ lesen. Dann würde jede Taste, die die Zweige
|
|
darüber nicht abgefangen haben, vom Game-Over-Bildschirm aus ein Spiel starten, die Feuertaste
|
|
eingeschlossen, und ein Spieler, der noch auf den Feuerknopf hämmert, wenn der letzte Felsen
|
|
trifft, säße in dem Moment, in dem GAME OVER erscheint, in einem neuen Spiel. Die Klammern sorgen
|
|
dafür, dass das `or` zuerst passiert. Wenn du `and` und `or` mischst, setz die Klammern, auch wenn
|
|
du glaubst, die Reihenfolge zu kennen. Wer es als Nächstes liest, muss sie dann nicht kennen.
|
|
|
|
[source,lua]
|
|
----
|
|
saveSet("highScore", best)
|
|
saveFlush()
|
|
----
|
|
|
|
`saveSet` legt die Zahl in den Spielstand; `saveFlush` bringt den Spielstand jetzt auf die
|
|
Festplatte statt erst am Ende des Frames. Beide stehen im `if`, also wird die Datei nur
|
|
geschrieben, wenn der Rekord wirklich gebrochen wird, und das ist ein paarmal pro Sitzung statt
|
|
sechzigmal pro Sekunde.
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore", 0)
|
|
----
|
|
|
|
Einmal gelesen, beim Start, in eine gewöhnliche Variable. Der Rest des Spiels liest `best`, statt
|
|
noch einmal `saveGet` aufzurufen, denn der Spielstand ist eine Datei, und eine Variable ist eine
|
|
Variable. Schreib in beide, wenn der Rekord fällt.
|
|
|
|
[#_try_it_12]
|
|
=== Probier es aus
|
|
|
|
. *Geh zurück zum Titel.* Ein Arcade-Automat bleibt nicht ewig auf GAME OVER stehen. Schlag
|
|
`timerAfter` im Abschnitt Timer der Referenz nach, einem weiteren Teil des Spiele-Kits, und
|
|
benutz es in `endGame`, um den Zustand fünf Sekunden nach Spielende zurück auf `TITLE` zu
|
|
setzen. Dann drück nach einem Spielende schnell `1` und sorg dafür, dass der Timer dein neues
|
|
Spiel nicht nach drei Sekunden zurück auf den Titel wirft.
|
|
. *Zeig die Leben als Schiffe.* Zeichne `shipSprite` mit `spriteDraw` einmal pro Leben in die
|
|
rechte obere Ecke, statt die Zahl auszugeben. Eine `for`-Schleife aus Lektion vier, und
|
|
`shipWidth` sagt dir, wie weit du sie auseinandersetzen musst.
|
|
. *Speichere mehr als eine Zahl.* Füg `saveSet("gamesPlayed", saveGet("gamesPlayed",
|
|
0) + 1)` in `startGame` ein und gib es auf dem Titelbildschirm aus. Beende es, starte es noch
|
|
einmal und sieh zu, wie es sich erinnert.
|
|
. *Finde die Datei und mach sie kaputt.* Öffne `data/rocks/save.json` in deinem Editor, ändere
|
|
den Highscore in etwas Absurdes und starte das Spiel. Dann lösch die Datei ganz und starte es
|
|
noch einmal. Beides wird deinen Spielern passieren.
|
|
. *Setz ihn aus dem Spiel heraus zurück.* Lass eine Taste, die du nie aus Versehen drücken würdest,
|
|
`saveClear()` aufrufen, und sieh dir danach den Titelbildschirm an. Vorsicht: `best` ist noch im
|
|
Speicher, und `saveClear` rührt es nicht an. Finde heraus, was sonst noch passieren muss.
|
|
|
|
[#_break_it_on_purpose_11]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Nimm den Standardwert heraus, sodass die Zeile so lautet:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore")
|
|
----
|
|
|
|
Wenn du schon einen Highscore aufgestellt hast, passiert überhaupt nichts, und genau das ist die
|
|
Falle: Der Spielstand hat den Schlüssel, also gibt `saveGet` ihn zurück. Lösch zuerst
|
|
`data/rocks/save.json` und starte es dann. Der Titelbildschirm erscheint nie, und du bekommst das
|
|
hier:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': rocks/rocks.singe:101: attempt to concatenate a nil value (upvalue 'best')
|
|
stack traceback:
|
|
rocks/rocks.singe:101: in function 'drawTitle'
|
|
rocks/rocks.singe:165: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
Lies es so, wie Lektion acht es dir beigebracht hat. Datei und Zeile führen dich zu
|
|
`overlayPrint(25, 9, "BEST " .. best)` in `drawTitle`, und die Beschwerde sagt, dass etwas, das
|
|
`nil` war, an einen String geklebt wurde. `..` verbindet zwei Strings, und `nil` ist keiner. Wenn
|
|
deine Zeilen anders angeordnet sind, sind es deine Zahlen auch.
|
|
|
|
Die Zeile, die sie nennt, ist nicht die, in der der Fehler steckt. In Zeile 101 ist das Spiel
|
|
schließlich darüber gestolpert; gemacht wurde der Fehler oben in der Datei, wo nach `best` gefragt
|
|
wurde und nichts da war. Diese Lücke zwischen der Stelle, an der ein schlechter Wert entsteht, und
|
|
der, an der er bemerkt wird, ist das Schwierigste an `nil`, und der Grund, warum ein Standardwert
|
|
ein Argument kostet und einen Abend rettet.
|
|
|
|
Setz die `0` wieder ein.
|
|
|
|
[#_what_you_learned_12]
|
|
=== Was du gelernt hast
|
|
|
|
* Ein Spiel ist immer in genau einem Zustand, und Wahr-oder-falsch-Flags, die zu sagen versuchen,
|
|
in welchem, erzeugen Kombinationen, die nichts bedeuten.
|
|
* Halte den Zustand in einer Variablen fest, mit seinen möglichen Werten einmal oben in der Datei
|
|
benannt.
|
|
* `onOverlayUpdate` entscheidet, was sich bewegt und was gesagt wird, indem es den Zustand fragt.
|
|
* Ein Spiel anzuhalten heißt, die Dinge nicht aufzurufen, die es bewegen.
|
|
* Starte jedes Spiel, das erste eingeschlossen, über eine einzige Funktion, und lass diese
|
|
Funktion alles zurücksetzen, was ein Spiel verändert -- und sonst nichts.
|
|
* Tasten gehören der Tastatur. Halte sie in jedem Zustand fest.
|
|
* `saveSet` speichert einen Wert unter einem Schlüssel, und `saveGet` liest ihn bei einem späteren
|
|
Lauf zurück.
|
|
* `saveFlush` schreibt die Datei sofort, für die Dinge, die du auf keinen Fall verlieren willst.
|
|
* Der Spielstand kommt in den eigenen Datenordner des Spiels, den `singeGetDataPath` nennt. Schreib
|
|
nie neben dein Skript.
|
|
* `saveGet` ohne Standardwert antwortet `nil`, wenn nichts gespeichert wurde, und `nil` macht den
|
|
ersten Frame auf einem Rechner kaputt, auf dem dein Spiel noch nie lief.
|
|
* Wenn du `and` und `or` mischst, setz Klammern.
|
|
|
|
[#_next_time_12]
|
|
=== Beim nächsten Mal
|
|
|
|
Dein Spiel ist jetzt ein Spiel, und es ist ein Ordner, von dem nur du weißt, wie man ihn startet.
|
|
Lektion dreizehn gibt ihm eine `games.dat`, damit das eigene Menü der Engine es mit einem Titel und
|
|
einem Bild auflistet und ein Spieler, der noch nie von dir gehört hat, es starten kann, ohne
|
|
irgendetwas zu tippen.
|