singe/docs/de/lessons/30-shipping.adoc

478 lines
22 KiB
Text

[#_lesson_30_shipping_it]
== Lektion 30: Ausliefern
image::learn/30-shipping.png[Die fertige Lektion, 480]
Du hast einen Ordner. Darin liegen ein Skript, ein paar Bilder, ein paar Sounds und die
`games.dat`, die du in Lektion dreizehn geschrieben hast. Er läuft, weil alles, was er braucht,
da ist, wo du es hingelegt hast, und du weißt, wo das ist.
Diese Lektion macht aus diesem Ordner eine einzige Datei, die jemand, der dich nie getroffen
hat, auf seinen Rechner kopieren und spielen kann. Das meiste davon sind zwei Befehlszeilen. Der
Rest ist der Teil, den dir niemand beibringt: was in die Datei gehört, wessen Arbeit außer
deiner darin steckt und wie du herausfindest, ob sie irgendwo anders als hier funktioniert.
[#_one_file]
=== Eine Datei
Ein fertiges Singe-Spiel wird als eine einzige `.game`-Datei ausgeliefert. Eine packst du so:
----
Singe --pack MyGame MyGame.game
----
Der erste Name ist der Ordner deines Spiels, der zweite die Datei, die geschrieben werden soll.
Singe liest jede Datei im Ordner in eine Datenbank und sagt, was es getan hat:
----
>>> Packed 42 files (3819204 bytes) from MyGame into MyGame.game
----
Starte es so, wie du alles startest:
----
Singe MyGame.game
----
Das startet den ersten Eintrag in der `games.dat` darin; `--entry=2` startet den zweiten, für
eine Datei, die mehr als ein Spiel enthält. Um es zu installieren, kopiert der Spieler
`MyGame.game` neben das Programm Singe. Das mitgelieferte Menü findet es und listet es auf.
Mehr gibt es nicht: keinen Installer, kein Archiv zum Entpacken, keinen Ordner, der an die
richtige Stelle muss.
[#_what_packing_does_to_every_file_name]
=== Was das Packen mit jedem Dateinamen macht
Nichts in deinem Spiel muss sich ändern, damit es gepackt werden kann, und es lohnt sich zu
wissen, warum, denn genau das wird dich am ehesten überraschen.
Jeder Name, den dein Spiel benutzt -- `dofile`, `require`, `io.open`, `spriteLoad`,
`soundLoad`, `fontLoad`, `videoLoad`, eine Frame-Datei, eine `controls.cfg` -- geht durch ein
und dieselbe Suche. Bei einem losen Ordner ist diese Suche das gewöhnliche Dateisystem. Bei
einem gepackten Spiel wird der Name an drei Orten probiert, und der erste Treffer gewinnt:
. ein loser Ordner, der so heißt wie die Datei ohne ihre Endung -- `MyGame/` neben
`MyGame.game`;
. der Datenordner des Spiels, unter `files/`, wo alles gelandet ist, was das Spiel
schreibt;
. die Datenbank selbst.
Innerhalb der Datenbank werden Namen ohne Rücksicht auf Großbuchstaben verglichen, mit beiden
Arten von Schrägstrich und relativ zur Wurzel des Spiels. Ein führender Ordnername, der so heißt
wie das Spiel selbst, wird ignoriert, also finden `MyGame/art/spaceship.png`,
`art/spaceship.png` und `DIR .. "art/spaceship.png"` alle dieselbe Datei.
Daraus folgen zwei Regeln, und wenn du dem Buch gefolgt bist, hältst du dich schon an beide.
Benutze `DIR` für alles, was du lädst, damit ein Name nie davon abhängt, wo Singe gestartet
wurde. Und schreib nur unterhalb von `singeGetDataPath()`, wo `saveSet` die Dinge schon ablegt --
ein gepacktes Spiel wird schreibgeschützt geöffnet und nie beschrieben, also funktioniert ein
Spiel, das neben sich selbst speichert, lose und scheitert gepackt.
Zwei Namen gehen mit Absicht woandershin. Alles, was mit `Singe/` anfängt, bleibt im echten
Dateisystem, also funktioniert `dofile("Singe/Framework.singe")` weiter, und der Datenordner
ebenso. Und ein Name mit `..` darin, der versucht, aus dem Spiel herauszuklettern, führt
überhaupt nirgendwohin: `io.open` gibt `nil` zurück und eine Meldung, die sagt, dass der Name
aus dem Spiel hinausreicht, und `spriteLoad` und seine Verwandten melden die Datei als fehlend.
[#_the_loose_folder_still_wins]
=== Der lose Ordner gewinnt trotzdem
Den ersten Ort in der Liste wirst du jeden Tag benutzen. Ein Ordner, der so heißt wie die
gepackte Datei und neben ihr liegt, schlägt die gepackten Kopien Datei für Datei.
----
MyGame.game das gepackte Spiel
MyGame/ ein Ordner mit einer geänderten Datei darin
----
Leg eine einzige korrigierte `art/spaceship.png` hinein, und das gepackte Spiel läuft mit deinem
neuen Bild und allem anderen aus der Datenbank. Nichts muss neu gepackt werden, um eine Änderung
auszuprobieren, und du kannst deinen ganzen Arbeitsordner dort liegen lassen, während du das
Spiel fertig machst. So moddet auch ein Spieler eins, und das kannst du für ein Feature halten
oder nicht.
Daraus folgen zwei Dinge. Prüf dein Spiel einmal, während dieser Ordner einen anderen Namen
hat, sonst lieferst du eine Datei aus, die nur auf dem Rechner funktioniert, der den
Ordner hat. Und wisse, dass Packen nichts versteckt: `--unpack` ist eine dokumentierte Option,
und jeder kann sie ausführen. Pack dein Spiel, weil es dann eine einzige Datei ist, nicht weil
es dann eine verschlossene ist.
[#_patching_a_released_game]
=== Ein veröffentlichtes Spiel patchen
Jemand hat dein Spiel, in einem Skript steckt ein Fehler, und du willst ihm nicht noch einmal
vierzig Megabyte schicken.
----
Singe --patch MyGame.game fixes
----
`fixes` ist ein Ordner, der wie das Spiel aufgebaut ist -- dieselben Namen an denselben Stellen --
und nur die Dateien enthält, die sich geändert haben. Dateien, die der Patch nicht nennt,
bleiben unangetastet. Die ganze Änderung ist eine einzige Transaktion, also lässt ein
unterbrochener Patch das Spiel genau so, wie es war, statt halb ersetzt.
Ein Patch kann als eigene Datei reisen, gemacht mit dem Packer:
----
Singe --pack fixes MyGame-1.0.2.patch
Singe --patch MyGame.game MyGame-1.0.2.patch
----
Eine `.patch`-Datei braucht keine `games.dat`, kann nicht gestartet werden, und das Menü
ignoriert sie. Sie kann außerdem eine Tabelle `removed(path)` mitbringen, die Dateien nennt, die
gelöscht werden sollen, für das Artwork, das du durch etwas mit einem anderen Namen ersetzt
hast. So oder so prüft Singe das gepatchte Spiel danach genauso, wie es ein neues prüft, also
kann ein Patch deine `games.dat` nicht auf eine Datei zeigen lassen, die nicht mehr da ist.
Und um alles wieder herauszuholen:
----
Singe --unpack MyGame.game MyGameUnpacked
----
[#_what_the_packer_refuses]
=== Was der Packer ablehnt
Der Packer baut nicht aus allem Möglichen ein Spiel, und jede Ablehnung betrifft etwas, das
später kaputtgehen würde. Er lehnt einen Ordner ohne `games.dat` in seiner Wurzel ab. Er lehnt
eine `Framework.singe` ab, eine `controls.dat`, alles, was auf `exe`, `sh`, `bat` oder `cmd`
endet, und eine Datei ohne Endung, deren Name mit `singe` anfängt -- mit anderen Worten eine
Kopie der Engine oder ihrer Hilfsdateien, die sich in deinen Spielordner verirrt hat. Er lehnt
zwei Dateien ab, deren Namen sich nur in Großbuchstaben unterscheiden, weil die Suche sie nicht
auseinanderhalten kann. Er lehnt einen Eintrag auf oberster Ebene ab, der so heißt wie der
Ordner selbst. Und er prüft deine `games.dat`, bevor er festschreibt: Jeder Eintrag braucht ein
`SCRIPT`, und jedes `SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE` und `ATTRACT`, das sie nennt, muss
eine Datei sein, die wirklich darin liegt. Ein falscher Pfad wird mit dem Titel des Eintrags
gemeldet, und nichts wird geschrieben.
Jede Beschwerde nennt die Datei, und du bekommst eine Liste davon statt nur der ersten, sodass
dir ein einziger Lauf alles sagt, was zu beheben ist.
[#_what_to_include_and_what_to_leave_out]
=== Was hineingehört und was draußen bleibt
Sieh in deinen Spielordner. Manches darin ist das Spiel, und manches ist, wie das Spiel
entstanden ist.
Ausliefern: das Skript und alles, was es lädt, die `games.dat`, das Artwork, das das Menü
zeigt (`CABINET`, `MARQUEE` und den `ATTRACT`-Clip), deine Credits- und Lizenzdateien und eine
Readme, wenn du eine hast.
Draußen lassen: die Zeichnungen, aus denen du das Artwork exportiert hast, die Aufnahmen des
Sounds, die du nicht benutzt hast, deine Notizen, Testskripte, Screenshots, alles, was `old`
oder `backup` heißt, den Ordner `data` mit deinen eigenen Spielständen und deinen eigenen
Highscores darin und alles, was ein Fremder nicht lesen soll. All das lässt sich bestens packen,
und genau das ist das Problem: Es geht mit dem Spiel hinaus, und es ist für jeden nur ein
`--unpack` entfernt.
Der zuverlässige Weg herauszufinden, was du tatsächlich ausgeliefert hast, ist, es in einen
leeren Ordner zu entpacken und nachzusehen. Mach das einmal bei jeder Veröffentlichung. Es dauert
eine Minute, und es ist die einzige Prüfung, die sieht, was du vergessen hast, statt was du im
Kopf hast.
[#_credits_and_licenses]
=== Credits und Lizenzen
Wenn irgendeine Grafik, ein Sound, Musik oder eine Schriftart in deinem Spiel von jemand anderem
gemacht wurde, lieferst du dessen Arbeit aus, und die Bedingungen, unter denen er sie hergegeben
hat, sind eine Pflicht und keine Höflichkeit. Das ist keine Formsache, und es geht nicht um
Anwälte: Ein Mensch hat dieses Ding gemacht und dafür um eine bestimmte Sache gebeten, meistens
um seinen Namen.
Führ Buch, während du arbeitest, nicht erst am Ende. Eine reine Textdatei im Spielordner mit
einer Zeile pro Stück: was die Datei ist, woher sie kommt, wer sie gemacht hat, welche Lizenz und
was diese Lizenz verlangt. Es nebenbei aufzuschreiben dauert Sekunden. Es in der Nacht vor einer
Veröffentlichung aus dem Browserverlauf zu rekonstruieren ist genau der Weg, auf dem Credits
verloren gehen.
Ein paar Dinge, die du wissen solltest, bevor du sie brauchst. „Kostenlos herunterladen“ ist
keine Lizenz; such nach den eigentlichen Bedingungen. Creative Commons `BY` heißt, dass du die
verlangte Namensnennung angeben musst, und es gibt keine Ausnahme für Spiele. `CC0` und
gemeinfreie Werke verlangen nichts, und trotzdem jemanden zu nennen kostet dich eine Zeile.
Schriftarten haben ihre eigenen Lizenzen, und manche erlauben das Einbetten, andere nicht. Musik
aus einem Video darfst du fast nie benutzen. Alles, was ein Werkzeug erzeugt hat, ist nur so
sauber wie die Bedingungen des Werkzeugs.
Dann bring die Credits irgendwo unter, wo ein Spieler sie erreichen kann, und zwar im Spiel.
Nicht nur in einer Textdatei daneben -- ein gepacktes Spiel ist eine einzige Datei, und die
Textdatei steckt darin, wo niemand nachsieht. Das Skript am Ende dieser Lektion ist dieser
Bildschirm.
Singes eigene `Singe/LICENSES` ist die Form, die du übernehmen solltest: eine Zeile pro
Bibliothek, mit ihrer Version, ihrer Lizenz und ihrer Herkunft. Die Liste deines Spiels wird
kürzer sein, und die Aufgabe ist dieselbe.
[#_a_version_number_that_means_something]
=== Eine Versionsnummer, die etwas bedeutet
Dein Spiel wird eine zweite Veröffentlichung haben. Die erste bleibt da draußen, solange es das
Internet gibt, und irgendwer wird dir einen Fehlerbericht dazu schicken, ohne eine Ahnung, welche
Version er hat.
Also such dir eine Nummer aus, setz sie dorthin, wo ein Spieler sie lesen kann, und benutze nie
eine zweimal. Drei Teile sind die übliche Form -- `1.0.2` -- und was die Teile bedeuten, zählt
weniger, als dass sie immer nur steigen. Erhöh den letzten für eine Korrektur, den mittleren für
etwas Neues, den ersten, wenn du ein anderes Spiel gemacht hast. Was immer du wählst, schreib es
in deine Readme, damit sich dein zukünftiges Ich auch daran hält.
`games.dat` hat keinen Schlüssel für die Version, also trägst du die Nummer selbst. Setz sie als
Konstante ins Skript, zeig sie auf dem Credits-Bildschirm und setz sie in den Namen dessen, was
du verschickst (`MyGame-1.0.2.game`, `MyGame-1.0.2.patch`). Dann nennt der Fehlerbericht die
Version auf dem Bildschirm, und du weißt, um welche Dateien es geht.
[#_testing_on_a_machine_that_is_not_yours]
=== Auf einem Rechner testen, der nicht deiner ist
Dein Rechner ist der schlechteste Ort überhaupt, um eine Veröffentlichung zu testen. Er hat die
Schriftarten, die du installiert hast, den Ordner, an dem du gearbeitet hast, die Speicherdatei
mit deinen Einstellungen darin und die Engine in dem Zustand, in dem ein Monat Arbeit sie
hinterlassen hat.
Der Mindesttest, der Reihe nach:
. Entpack die Veröffentlichung in einen leeren Ordner und lies die Dateiliste.
. Kopier *nur* die `.game`-Datei in einen anderen Ordner, mit einer frischen Kopie von Singe und
sonst nichts, und starte sie aus dem Menü, so wie ein Spieler es tun wird.
. Lösch den Ordner `data` dieses Ordners und starte es noch einmal. Das ist ein erster Start
auf einem neuen Rechner: keine gespeicherten Einstellungen, keine Highscores, kein angemeldetes
Konto. Ein Spiel, das erst beim zweiten Start funktioniert, ist ein häufiger Fehler, und so
findest du ihn.
. Gib es einer Person, die es nie gesehen hat, und sieh zu, ohne zu helfen.
Dann teste den Rechner genauso wie das Spiel. Er wird eine andere Bildschirmform haben, also sieh
dir dein Spiel im Fenster und im Vollbild an. Vielleicht hat er überhaupt kein 3D-Gerät, und
darum hat das Menü einen zweiten Renderer. Die Seite *System
Information* der Service-Tools listet die Engine-Version, das Betriebssystem, den Prozessor, den
Renderer, die Decoder und den Datenordner auf einem Bildschirm auf; ein Foto dieser Seite
beantwortet den größten Teil eines Fehlerberichts, und wenn du das deinen Spielern in deiner
Readme sagst, ersparst du dir eine Woche Raterei.
[#_the_script_2]
=== Das Skript
Ein Bildschirm mit Credits und Version. Er ist schnell geschrieben, und er ist der Teil des
Auslieferns, den die meisten Spiele falsch machen.
[source,lua]
----
dofile("Singe/Framework.singe")
local GAME_NAME = "Rock Dodger"
local GAME_VERSION = "1.0.2"
local CREDITS = {
"Written and drawn by <your name here>",
"",
"Music \"Night Drive\" by A. Composer",
" Creative Commons BY 4.0",
" example.org/night-drive",
"",
"Explosion freesound.org user \"thud\"",
" Creative Commons CC0",
"",
"Font FreeSansBold",
" GPL with the font exception",
"",
"Engine Singe " .. SINGE_VERSION_STRING,
" GPL version 3",
" See Singe/LICENSES for the",
" libraries Singe is built on.",
"",
"Thank you for playing.",
}
local FIRST_ROW = 4
local ROWS = 10
local scroll = 0
local function scrollBy(lines)
local most = #CREDITS - ROWS
if most < 0 then
most = 0
end
scroll = scroll + lines
if scroll < 0 then
scroll = 0
end
if scroll > most then
scroll = most
end
end
function onInputPressed(what)
if what == SWITCH_UP then
scrollBy(-1)
elseif what == SWITCH_DOWN then
scrollBy(1)
elseif what == SWITCH_BUTTON1 then
scroll = 0
end
end
function onOverlayUpdate()
local line = nil
overlayClear()
overlayPrint(2, 0, GAME_NAME .. " version " .. GAME_VERSION)
overlayPrint(2, 1, "Singe " .. SINGE_VERSION_STRING .. " id " .. (singeGetGameId() or "none"))
for row = 0, ROWS - 1 do
line = CREDITS[scroll + row + 1]
if line then
overlayPrint(2, FIRST_ROW + row, line)
end
end
overlayPrint(2, FIRST_ROW + ROWS + 1, "Saves go to:")
overlayPrint(2, FIRST_ROW + ROWS + 2, singeGetDataPath())
overlayPrint(2, 17, "Up and down scroll. Button 1 goes back to the top.")
return OVERLAY_UPDATED
end
----
Ersetze jede Zeile von `CREDITS` durch die Wahrheit über dein eigenes Spiel. Die Platzhalter
sind da, um die Form zu zeigen: was es ist, wer es gemacht hat, welche Lizenz und woher es
kommt.
[#_what_just_happened_25]
=== Was gerade passiert ist
[source,lua]
----
local GAME_VERSION = "1.0.2"
----
Ein Ort, ganz oben, in Großbuchstaben, weil sich der Wert nie ändert, während das Spiel läuft.
Alles, was die Version zeigt, liest diesen Wert, also ist das Erhöhen eine einzige Änderung
statt einer Suche.
[source,lua]
----
"Music \"Night Drive\" by A. Composer",
----
Diese Backslashes sind die Escape-Sequenz aus Lektion eins: ein doppeltes Anführungszeichen, das
zum Text gehört, statt den String zu beenden. Titel haben Anführungszeichen, und darum stößt eine
Credits-Liste gleich in ihrer ersten Zeile darauf.
[source,lua]
----
for row = 0, ROWS - 1 do
line = CREDITS[scroll + row + 1]
if line then
----
Zehn Bildschirmzeilen zeigen zehn Zeilen aus einer Liste, die länger als zehn ist. `scroll`
sagt, wie weit das Fenster in der Liste nach unten gerückt ist. Das `+ 1` gibt es, weil Lua eine
Liste ab eins zählt und die Bildschirmzeilen ab null, und das `if line then` gibt es, weil das
Ende der Liste `nil` ist -- über das Ende einer Tabelle hinaus zu lesen ist in Lua kein Fehler,
es gibt dir einfach nichts, aber nichts auszugeben wäre einer.
[source,lua]
----
overlayPrint(2, 1, "Singe " .. SINGE_VERSION_STRING .. " id " .. (singeGetGameId() or "none"))
----
Drei Fakten, die ein Fehlerbericht braucht und die ein Spieler sonst nicht finden kann: deine
Version in der Zeile darüber, die Version der Engine und die ID des Spiels.
`SINGE_VERSION_STRING` ist eine globale Variable, die die Engine setzt, bevor dein Skript läuft.
`or "none"` deckt den Fall ab, dass das Spiel keinen Eintrag in `games.dat` hat, sodass die
Zeile etwas ausgibt, statt an einem `nil` abzustürzen.
[source,lua]
----
overlayPrint(2, FIRST_ROW + ROWS + 2, singeGetDataPath())
----
Wohin dieses Spiel schreibt. Bei einem gepackten Spiel ist das der einzige Ort, an dem überhaupt
etwas geschrieben wird, und es ist der Ordner, den ein Spieler löscht, um sauber neu anzufangen.
Ihn anzuzeigen macht aus „Wo sind meine Spielstände?“ eine Frage, auf die du nie antworten
musst.
Wenn dieser Pfad rechts über den Bildschirm hinausläuft, ist nichts kaputt: `overlayPrint` lässt
die Zeichen weg, die über den Rand gehen würden. Das Standard-Overlay ist sechzig Zeichenzellen
breit.
[#_try_it_30]
=== Probier es aus
. *Pack es.* Leg dieses Skript in deinen Spielordner, führ `--pack` aus und starte die
`.game`-Datei. Dann entpack sie in einen leeren Ordner und lies die Liste dessen, was
herauskam.
. *Überschreib eine Datei.* Leg neben der gepackten Datei einen Ordner an, der so heißt wie sie
ohne die Endung, und leg eine geänderte Datei hinein. Starte das gepackte Spiel und sieh zu,
wie deine lose Datei gewinnt.
. *Mach einen Patch.* Ändere die Version in `1.0.3`, leg nur das Skript in einen Ordner
`fixes`, pack ihn als `.patch` und spiel ihn in das Spiel ein, das du schon gemacht hast.
Prüf die Version auf dem Bildschirm.
. *Stell den Packer auf die Probe.* Benenn deine `games.dat` kurz um und versuch zu packen.
Dann benenn sie zurück, lass einen ihrer `SCRIPT`-Namen auf eine Datei zeigen, die es nicht
gibt, und versuch es noch einmal. Zwei verschiedene Ablehnungen, beide bevor irgendetwas
geschrieben wird.
. *Schreib die echten Credits.* Geh den Ordner deines Spiels Datei für Datei durch und schreib
eine Zeile für jede, die du nicht selbst gemacht hast. Wenn du dich nicht erinnern kannst,
woher etwas kommt, ist das die Antwort auf die Frage, ob du es ausliefern solltest.
[#_break_it_on_purpose_29]
=== Mach es absichtlich kaputt
Starte Singe einmal *innerhalb* deines Spielordners, sodass die Engine ihre Hilfsdateien dort
entpackt, und pack ihn dann:
----
Singe --pack MyGame MyGame.game
----
----
!!! MyGame has Framework.singe: Singe/Framework.singe
!!! MyGame has sh file: Menu.sh
----
Nichts wird geschrieben. Jede Zeile nennt eine Datei, die er nicht nimmt, und warum, und du
bekommst sie vielleicht in der anderen Reihenfolge oder noch andere dazu. In deinem Spiel liegt
eine Kopie des eigenen Hilfsordners der Engine, und daneben das Startskript, das die Engine
geschrieben hat, weil du Singe irgendwann von dort aus gestartet hast. Ein Spiel darf nie eins
von beiden mitbringen: Die Hilfsdateien gehören zu dem Singe, das der Spieler gerade benutzt,
nicht zu deinem Spiel, und eine gepackte `Framework.singe` würde statt seiner geladen. Unter
Windows nennt die zweite Zeile stattdessen `Menu.bat`.
Lösch den Ordner `Singe` und `Menu.sh` (oder `Menu.bat`) aus deinem Spiel, halte deinen Spielordner als einen Ordner neben dem
Programm Singe statt um es herum, und pack noch einmal. Das ist derselbe Aufbau, den Lektion
dreizehn beschrieben hat, und dieser Fehler ist das, was passiert, wenn er verrutscht.
[#_what_you_learned_30]
=== Was du gelernt hast
* `Singe --pack folder name.game` macht aus einem Spielordner eine einzige Datei, und wer diese
Datei neben das Programm Singe kopiert, hat sie installiert.
* Jeder Dateiname in einem gepackten Spiel wird zuerst in einem losen Ordner gesucht, dann im
Datenordner, dann in der Datenbank.
* Ein loser Ordner, der so heißt wie die gepackte Datei, überschreibt sie Datei für Datei, und
so arbeitest du weiter -- und so lieferst du eine kaputte Veröffentlichung aus, wenn du
vergisst, dass er da ist.
* Lade mit `DIR` und schreib nur unterhalb von `singeGetDataPath()`, sonst funktioniert dein
Spiel lose und scheitert gepackt.
* `--patch` ersetzt Dateien in einem veröffentlichten Spiel in einer einzigen Transaktion;
`--unpack` schreibt alles wieder heraus.
* Der Packer lehnt Engine-Dateien ab, Namenskonflikte und eine `games.dat`, die auf Dateien
zeigt, die er nicht hat.
* Entpack deine eigene Veröffentlichung und lies, was darin ist, bevor du sie irgendwohin
schickst.
* Die Grafiken und Sounds anderer Leute kommen mit Bedingungen, und sie zu erfüllen heißt: ein
Credits-Bildschirm im Spiel.
* Eine Versionsnummer steigt immer nur, und sie gehört auf den Bildschirm.
* Teste auf einem Rechner, der nicht deiner ist, und fang ganz ohne Ordner `data` an.
[#_next_time_30]
=== Beim nächsten Mal
Eine Datei, installiert, mit Credits und nummeriert. Die letzte Lektion steckt sie in eine
Holzkiste mit einem Münzschlitz und ohne Tastatur, und das ändert mehr an deinem Spiel, als du
denkst.