singe/docs/de/lessons/12-score.adoc

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.