[#_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 ", "", "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.