[#_lesson_31_in_a_cabinet] == Lektion 31: Im Automaten image::learn/31-cabinet.png[Die fertige Lektion, 480] Zwischen einem Spiel und einem Arcade-Automaten gibt es einen Unterschied, und es ist nicht das Holz. Ein Automat schaltet sich morgens selbst ein, spielt vor einem leeren Raum, nimmt eine Münze von jemandem, der nie ein Wort darüber gelesen hat, und läuft um Mitternacht immer noch, ohne dass jemand eine Tastatur angefasst hat, weil es keine Tastatur gibt. Alles in dieser Lektion kommt daher. Du brauchst keinen Automaten, um irgendetwas davon zu machen. Ein Stick und zwei Knöpfe auf einem Schreibtisch oder ein Gamepad reichen, um jeden Fehler in dieser Lektion zu finden, und das meiste, was du hier lernst, macht ein Spiel auch auf einem Desktop besser. [#_there_is_no_keyboard] === Es gibt keine Tastatur Fang mit dem an, was fehlt, denn genau das macht Spiele kaputt. Ein Automat hat einen Stick, ein paar Knöpfe, eine Münztür, einen Startknopf und irgendwo im Inneren einen kleinen Schalter, an den ein Betreiber herankommt. Das ist alles. Kein Esc, keine Buchstaben, keine Maus, keine Möglichkeit zu tippen. Also: * Jedes Menü in deinem Spiel muss mit hoch, runter und einem Knopf erreichbar sein. Wenn irgendein Bildschirm in deinem Spiel „Drück Y, um weiterzumachen“ sagt, funktioniert er in einem Automaten nicht. * Initialen eingeben heißt drei Knöpfe und ein Buchstabe, der durchläuft, kein Textfeld. Darum haben Arcade-Spiele Namen aus drei Buchstaben. * Eine Aufforderung, die eine Taste über ihren Buchstaben nennt, ist falsch. Sag „Knopf 1“ oder zeichne den Knopf. * Es gibt keinen Ausweg. Ein Spieler kann nicht beenden, und er sollte es auch nicht können. Geh dein Spiel einmal durch, Bildschirm für Bildschirm, und frag bei jedem: Käme jemand mit einem Stick und einem Knopf hier durch? Dieser Durchgang durch dein eigenes Spiel ist mehr wert als der Rest dieser Lektion. [#_controls_cfg] === controls.cfg Seit Lektion drei liest du Eingaben als `SWITCH_UP`, `SWITCH_BUTTON1`, `SWITCH_COIN1` und den Rest, und du hast nicht ein einziges Mal gesagt, welche Taste das ist. Genau darum geht es bei ihnen. Irgendwo zwischen dem Mikroschalter im Automaten und deinem `onInputPressed` liegt eine Datei, die das entscheidet, und dein Spiel ist an diesem Gespräch nicht beteiligt. Die Datei ist `controls.cfg`. Sie ist Lua, wie dein Skript, und eine Liste von Tabellen -- eine pro Schalter -- mit allem, was ihn auslösen kann: [source,lua] ---- INPUT_1P_START = { SCANCODE.MAIN_1, GAMEPAD_0.BUTTON_START } INPUT_1P_COIN = { SCANCODE.MAIN_5, GAMEPAD_0.BUTTON_LEFT_BUMPER } INPUT_ACTION_1 = { SCANCODE.LCTRL, GAMEPAD_0.BUTTON_A } INPUT_UP = { SCANCODE.UP, GAMEPAD_0.DPAD_UP, GAMEPAD_0.AXIS_LEFT_Y_U } ---- Jeder `INPUT_`-Name in der Datei wird zum passenden `SWITCH_`-Wert in deinem Spiel: `INPUT_ACTION_1` ist der `SWITCH_BUTTON1`, auf den du die ganze Zeit prüfst, und `INPUT_1P_COIN` ist `SWITCH_COIN1`. Jeder Eintrag in einer Tabelle löst denselben Schalter aus, also ist es einem Spiel egal, ob der Spieler die Taste gedrückt hat, den Knopf am Pad oder den Stick bewegt hat. Du musst nicht die ganze Datei schreiben. Singe fängt mit seinen eigenen Standardwerten an und liest `controls.cfg` der Reihe nach an vier Orten -- dem Skriptordner des Spiels, dem Ordner, in dem Singe gestartet wurde, dem Ordner über dem Datenordner des Spiels und dem Datenordner des Spiels -- und jede Datei, die es findet, überschreibt, was vorher kam, Eintrag für Eintrag. So hat ein Automat ganz oben eine Datei für den ganzen Rechner, und ein Spiel, das einen zusätzlichen Knopf braucht, hat eine eigene Datei mit vier Zeilen. Im Ordner `Singe` liegt eine `controls.cfg.example`, die du kopieren kannst. `DEAD_ZONE` steht auch darin: wie weit sich ein Stick bewegen muss, bevor er als gedrückt zählt, in den rohen Einheiten, die die Hardware meldet, wobei voller Ausschlag `32767` ist. Der Standard ist `15000`. Ein abgenutzter Stick, der driftet, braucht eine größere Zahl, und `DEAD_ZONES` stellt eine Achse nach der anderen ein, für den einen Stick, der müde ist. Der Abschnitt des Handbuchs über das Anpassen der Steuerung deckt das alles ab. [#_every_device_is_a_gamepad] === Jedes Gerät ist ein Gamepad Die Knöpfe eines Arcade-Automaten sind an eine Encoder-Platine angeschlossen, die der Computer als eine Art Joystick sieht, und die hat nichts mit einem Xbox-Pad gemein. Singe schreibt für so ein Gerät selbst eine Belegung und öffnet es dann als `GAMEPAD_0`, sodass es `controls.cfg` und dein Spiel genau so erreicht wie ein erkanntes Pad. Es gibt eine einzige Gerätefamilie und kein zweites Vokabular, das du lernen musst. Die Belegung, die es erfindet, ist eine direkte: Die Knöpfe der Platine werden der Reihe nach zu `BUTTON_A`, `BUTTON_B`, `BUTTON_X`, `BUTTON_Y` und so weiter. Wenn die Knöpfe deines Automaten nicht in dieser Reihenfolge verdrahtet sind, kommen sie an den falschen Stellen heraus, und die Korrektur liegt nicht in deinem Spiel: `--program` schreibt die Belegung, die Singe geschrieben hat, in einer Form in den Trace, die du bearbeiten und als `gamecontrollerdb.txt` neben das Spiel legen kannst. Wenn ein Knopf „nichts tut“, beantwortet das Tool *Input Test* des Menüs die Frage in fünf Sekunden. Es zeigt jedes Gerät, das die Engine sehen kann, und den letzten Schalter, den ein Spiel bekommen hätte. Entweder kommt der Schalter an, und dein Spiel ignoriert ihn, oder er kommt nie an, und das Problem ist die Belegung oder das Kabel. [#_four_switches_are_not_yours] === Vier Schalter gehören nicht dir Die meisten Schalter gehen direkt an dein Spiel. Vier nicht: Um Pause, Beenden, Screenshot und das Umschalten des Mausfangs kümmert sich die Engine selbst. Pause ist wirklich nützlich, und zwar umsonst. Solange die Engine sie in der Hand hat, friert die Taste, die `INPUT_PAUSE` zugeordnet ist, alles ein -- die Disc, jedes Video, jeden Sound und dein Skript. Sie zeichnet ihre eigene Anzeige und taut beim nächsten Druck alles wieder auf. Alles, wovon dein Spiel dachte, es werde gehalten, wird ordentlich losgelassen und danach wieder gedrückt, sodass ein Stick, der während einer Pause gehalten wird, nie hängen bleibt. Beenden ist der Schalter, den du wegnimmst, und das macht ein späterer Abschnitt. [#_coins_credits_and_start] === Münzen, Credits und Start Ein Arcade-Automat hat eine Form, die älter ist als jeder von uns, und Spieler kennen sie, ohne dass man es ihnen sagt: . *Attract-Modus.* Niemand ist da. Der Automat zeigt, was er ist, lässt `INSERT COIN` blinken und verlangt nichts. . *Credit.* Eine Münze kommt als `SWITCH_COIN1` an. Münzen werden zu Credits, zu dem Kurs, den der Betreiber eingestellt hat -- nicht immer eins zu eins. . *Spiel.* `SWITCH_START1` gibt einen Credit aus und startet. . *Game Over.* Kurz die Punktzahl und dann von selbst zurück in den Attract-Modus. Niemand drückt etwas, um hinauszukommen; der Spieler ist schon gegangen. Zwei Details, die Leute falsch machen. Münzen werden gezählt, nicht ausgegeben: Bei zwei Münzen pro Credit muss eine Münze dasitzen und auf ihren Partner warten, und ein Spieler, der eine eingeworfen hat und gegangen ist, hat sie der nächsten Person hinterlassen. Und der Automat muss selbst den Weg zurück in den Attract-Modus finden, denn das Letzte, was ein Spieler tut, ist, mit dem Spielen aufzuhören. [#_behind_the_service_switch] === Hinter dem Service-Schalter Hinter der Münztür gibt es einen Knopf, den der Spieler nie sieht und den der Betreiber ständig benutzt. Er ist `SWITCH_SERVICE`, und die Engine reicht ihn direkt an dich weiter. Ein Betreiber, der dein Service-Menü öffnet, erwartet, ungefähr in dieser Reihenfolge, Folgendes vorzufinden: * *den Preis* -- Münzen pro Credit und eine Freispiel-Einstellung für einen Automaten zu Hause oder auf einer Messe; * *die Schwierigkeit*, weil ein Automat in einer Bar und ein Automat in einer Spielhalle verschiedene Antworten brauchen; * *eine Möglichkeit, die Highscore-Tabelle zu löschen*, das mit Abstand meistgewünschte Ding auf dieser Liste; * *was der Automat ist*, damit eine Störungsmeldung Fakten enthält -- die Engine-Version, der Bildschirm, wo die Daten liegen; * *einen Ausweg*, ohne irgendetwas auszuschalten. Alles, was ein Betreiber ändert, muss überstehen, dass der Strom wegfällt, und in einer Spielhalle heißt das: mitten in einem Spiel, ohne Vorwarnung, weil jemand einen Stecker zieht. `saveSet` schreibt am Ende des Frames, und das ist früh genug für eine Punktzahl und nicht früh genug für eine Einstellung, die jemand gerade verstellt hat. `saveFlush()` schreibt sofort, und genau für ein Service-Menü ist es gedacht. Die eigenen Service-Tools des mitgelieferten Menüs, hinter demselben Schalter, solltest du kennen, denn sie sind auf jedem Automaten, auf dem dein Spiel laufen wird: der Input Test, die Tests für Sound und Lightgun, die Bildgeometrie, die Audioverzögerung, das Online-Konto und eine Seite, die die gespeicherten Daten eines einzelnen Spiels löscht. Dein Service-Menü muss sie nicht nachbauen. [#_a_machine_that_starts_itself_and_never_quits] === Ein Automat, der sich selbst startet und nie beendet Der Automat wird an der Steckdose eingeschaltet. Niemand meldet sich an. Nach einer Minute oder so muss ein Spiel auf dem Bildschirm sein und dort bleiben. Dass Singe beim Einschalten startet, ist die Aufgabe deines Betriebssystems und nicht die von Singe -- der Rechner startet die `Menu.sh` oder `Menu.bat`, die die Engine neben deine Spiele schreibt, oder direkt dein Spiel. Deine Aufgabe ist, was den Rest des Tages passiert. Nimm die Beenden-Taste weg: [source,lua] ---- singeSetQuitKeyEnabled(false) ---- Die Engine ignoriert dann den Schalter, der `INPUT_QUIT` zugeordnet ist, und liefert ihn dir als `SWITCH_QUIT` wie jeden anderen, sodass du erst fragen, erst speichern oder gar nichts tun kannst. Der Schließen-Knopf des Fensters beendet immer, egal was du einstellst, und das wird gleich wichtig. Dann gehört eine Handvoll Optionen dem Automaten statt deinem Skript, und die kommen in eine `settings.cfg` neben den Spielen -- dieselben Namen wie die Befehlszeilenoptionen, nur ohne die Bindestriche: [source,lua] ---- fullscreen = true screen = 2 idleexit = 900 startsilent = true ---- `idleexit` beendet nach so vielen Sekunden ohne jede Eingabe, für einen Automaten, der den Bildschirm an ein Menü oder ein Frontend zurückgeben soll. `startsilent` startet stumm, bis jemand etwas anfasst, und so ist ein Raum voller Automaten erträglich. Es gibt noch mehr; die Abschnitte des Handbuchs über Einstellungen und die Befehlszeile listen jede einzelne auf. [#_artwork_round_the_picture] === Artwork um das Bild Ein echter Automat hat ein gedrucktes Bezel (einen Bildschirmrahmen) um den Bildschirm: Artwork mit einem Loch darin, durch das das Spiel zu sehen ist. Singe zeichnet eines für dich. ---- Singe --bezel cabinet.png MyGame.game ---- Die Datei wird in einem Ordner `bezels` im Spiel gesucht -- ein gepacktes Spiel kann sein eigenes mitbringen -- und dann in einem Ordner `bezels` im Datenordner, sodass ein Spieler sein eigenes Artwork auf ein Spiel legen kann, ohne es anzufassen. Neben der Grafik, nach ihr benannt, sagt eine kleine Lua-Datei, wohin das Bild kommt, in den eigenen Pixeln des Artworks: [source,lua] ---- -- bezels/cabinet.cfg CUTOUT = { x = 200, y = 120, width = 800, height = 500 } ---- Weil diese Zahlen die eigenen des Artworks sind, bleiben sie bei jeder Fenstergröße richtig: Grafik und Loch skalieren zusammen. Ohne eine solche Datei behält das Bild das ganze Fenster, so wie es das immer getan hat. Standardmäßig liegt das Artwork hinter allem, und dein Spiel verdeckt das Loch. `--bezelflip` setzt es stattdessen nach vorn, sodass nur seine Transparenz das Spiel durchlässt, und das willst du für weiche oder geformte Ränder -- und dann liegt auch dein Overlay darunter, wenn du nichts anderes sagst: [source,lua] ---- if mainBezelLoaded() then setOverlayOnTop(true) end ---- `mainBezelLoaded()` sagt dir außerdem, ob es überhaupt Artwork gibt, und so entscheidet ein Spiel, seinen eigenen Rahmen zu zeichnen, wenn keins da ist. [#_a_monitor_on_its_side] === Ein Monitor auf der Seite Viele Arcade-Monitore sind hochkant eingebaut. Dreh die ganze Darstellung: ---- Singe --rotate=90 MyGame.game ---- Das Bild, das Overlay, jede GUI, eine 3D-Szene und die Partikel drehen sich alle zusammen, und die Positionen von Maus und Lightgun werden zurückgedreht, sodass ein Klick immer noch dort landet, wo der Spieler ihn sieht. Eine Vierteldrehung vertauscht außerdem die Form der Fläche, sodass ein hohes Fenster gefüllt wird, statt mit Letterbox zu einem Streifen zu schrumpfen. Nur `0`, `90`, `180` und `270` werden angenommen. Ein Skript kann es während des Laufs mit `vldpSetRotate` ändern, das für alles andere `false` antwortet, statt dein Spiel zu beenden, und `vldpGetRotate()` liest es zurück -- und das gehört auf einen Service-Bildschirm, denn „das Bild liegt auf der Seite“ und „der Monitor liegt auf der Seite“ sind verschiedene Störungen. Was die Drehung nicht tut, ist, dein Spiel umzuschreiben. Ein Spiel, das für einen breiten Bildschirm gezeichnet und um neunzig Grad gedreht wurde, ist ein breites Spiel auf der Seite. Für einen hohen Bildschirm zu entwerfen heißt, für einen hohen Bildschirm zu entwerfen. [#_the_script_3] === Das Skript Die ganze Form: Attract-Modus, Münze, Credit, Spiel, Game Over, Service und ein Beenden, das nachfragt. Das Spiel in der Mitte ist ein Knopf und ein Timer von fünfzehn Sekunden, denn diesmal geht es nicht um das Spiel -- sondern um den Automaten drumherum. [source,lua] ---- dofile("Singe/Framework.singe") local ROUND_MS = 15000 local MENU = { "Coins per credit", "Difficulty", "Free play", "Clear high score", "Exit service" } local HARDNESS = { "Easy", "Normal", "Hard" } local coinsPerCredit = saveGet("coinsPerCredit", 1) local difficulty = saveGet("difficulty", 2) local freePlay = saveGet("freePlay", false) local best = saveGet("best", 0) local state = "attract" local goBackTo = "attract" local coins = 0 local credits = 0 local score = 0 local endsAt = 0 local item = 1 local saying = "" local hasBezel = false local function wrap(value, low, high) if value < low then return high end if value > high then return low end return value end local function canStart() return freePlay or credits > 0 end local function insertCoin() coins = coins + 1 while coins >= coinsPerCredit do coins = coins - coinsPerCredit credits = credits + 1 end end local function startGame() if not freePlay then credits = credits - 1 end score = 0 endsAt = singeGetTicks() + ROUND_MS state = "play" end local function endGame() if score > best then best = score saveSet("best", best) end saveFlush() endsAt = singeGetTicks() + 5000 state = "over" end local function settingOf(which) if which == 1 then return tostring(coinsPerCredit) elseif which == 2 then return HARDNESS[difficulty] elseif which == 3 then if freePlay then return "on" end return "off" elseif which == 4 then return tostring(best) end return "" end local function adjust(step) if item == 1 then coinsPerCredit = wrap(coinsPerCredit + step, 1, 4) saveSet("coinsPerCredit", coinsPerCredit) elseif item == 2 then difficulty = wrap(difficulty + step, 1, #HARDNESS) saveSet("difficulty", difficulty) elseif item == 3 then freePlay = not freePlay saveSet("freePlay", freePlay) end saveFlush() end local function serviceInput(what) if what == SWITCH_UP then item = wrap(item - 1, 1, #MENU) elseif what == SWITCH_DOWN then item = wrap(item + 1, 1, #MENU) elseif what == SWITCH_LEFT then adjust(-1) elseif what == SWITCH_RIGHT then adjust(1) elseif what == SWITCH_BUTTON1 then if item == 4 then best = 0 saveSet("best", 0) saveFlush() saying = "High score cleared." elseif item == 5 then state = goBackTo end elseif what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then state = goBackTo end end function onInputPressed(what) if what == SWITCH_QUIT then if state == "confirm" then singeQuit() else goBackTo = state state = "confirm" end return end if state == "confirm" then if what == SWITCH_BUTTON2 then state = goBackTo end return end if state == "service" then serviceInput(what) return end if what == SWITCH_SERVICE then goBackTo = state saying = "" state = "service" return end if what == SWITCH_COIN1 then insertCoin() return end if state == "play" then if what == SWITCH_BUTTON1 then score = score + 10 * difficulty end return end if what == SWITCH_START1 and canStart() then startGame() end end local function drawAttract() overlayPrint(2, 2, "ROCK DODGER") if canStart() then overlayPrint(2, 4, "PRESS START") elseif (singeGetTicks() // 400) % 2 == 0 then overlayPrint(2, 4, "INSERT COIN") end overlayPrint(2, 6, "High score " .. best) end local function drawService() overlayPrint(2, 0, "SERVICE MENU") for row, name in ipairs(MENU) do if row == item then overlayPrint(1, row + 1, ">") end overlayPrint(3, row + 1, name) overlayPrint(24, row + 1, settingOf(row)) end overlayPrint(2, 8, saying) overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate()) if hasBezel then overlayPrint(2, 11, "Bezel artwork loaded.") else overlayPrint(2, 11, "No bezel artwork.") end overlayPrint(2, 12, singeGetDataPath()) overlayPrint(2, 16, "Up/Down choose. Left/Right change. Button 1 does it.") overlayPrint(2, 17, "Button 2 or the service switch leaves.") end function onOverlayUpdate() overlayClear() if state == "play" and singeGetTicks() >= endsAt then endGame() elseif state == "over" and singeGetTicks() >= endsAt then state = "attract" end if state == "service" then drawService() elseif state == "confirm" then overlayPrint(2, 2, "QUIT THIS GAME?") overlayPrint(2, 4, "Quit again to quit. Button 2 stays.") elseif state == "play" then overlayPrint(2, 2, "SCORE " .. score) overlayPrint(2, 3, "TIME " .. ((endsAt - singeGetTicks()) // 1000)) overlayPrint(2, 5, "Button 1 scores. " .. HARDNESS[difficulty] .. ".") elseif state == "over" then overlayPrint(2, 2, "GAME OVER") overlayPrint(2, 4, "You scored " .. score .. ".") overlayPrint(2, 6, "High score " .. best) else drawAttract() end if state ~= "service" then overlayPrint(2, 17, "CREDITS " .. credits .. " COINS " .. coins .. "/" .. coinsPerCredit) end return OVERLAY_UPDATED end singeSetQuitKeyEnabled(false) hasBezel = mainBezelLoaded() if hasBezel then setOverlayOnTop(true) end ---- Starte es und drück `5` für eine Münze, `1` für Start und `9` für den Service-Schalter -- das sind die Standardwerte in der mitgelieferten `controls.cfg`. Dann drück Esc und lies, was da steht, bevor du sie noch einmal drückst. [#_what_just_happened_26] === Was gerade passiert ist [source,lua] ---- local coinsPerCredit = saveGet("coinsPerCredit", 1) local difficulty = saveGet("difficulty", 2) local freePlay = saveGet("freePlay", false) ---- Die Einstellungen des Betreibers werden einmal gelesen, ganz oben, mit einem vernünftigen Wert für einen Automaten, der noch nie eingerichtet wurde. `saveGet` mit einem zweiten Argument gibt diesen Wert zurück, wenn nichts gespeichert wurde, und so unterscheidet man einen ersten Start von einem späteren. [source,lua] ---- local function insertCoin() coins = coins + 1 while coins >= coinsPerCredit do coins = coins - coinsPerCredit credits = credits + 1 end end ---- Münzen rein, Credits raus, zu dem Kurs, den der Betreiber eingestellt hat. Das `while` statt eines `if` ist keine Vorsicht vor unmöglichen Eingaben -- es ist der Betreiber, der den Preis von vier Münzen auf eine senkt, während drei Münzen darin sitzen. [source,lua] ---- if what == SWITCH_QUIT then if state == "confirm" then singeQuit() else goBackTo = state state = "confirm" end return end ---- Der Beenden-Schalter erreicht dein Spiel nur wegen des `singeSetQuitKeyEnabled(false)` ganz unten im Skript. Beim ersten Drücken wird gefragt; beim zweiten Drücken, vom Fragebildschirm aus, rufst du selbst `singeQuit()` auf. `goBackTo` merkt sich, wo der Spieler war, sodass ein Nein ihn zurück in sein Spiel bringt statt auf den Attract-Bildschirm. Dieser Block steht als Erstes in der Funktion und endet mit `return`, sodass nichts anderes den Schalter verschlucken kann. Jeder andere Zustand des Spiels wird darunter behandelt. [source,lua] ---- if state == "service" then serviceInput(what) return end ---- Wieder dieselbe Form. Solange das Service-Menü offen ist, gehört ihm jeder Schalter -- der Stick steuert nicht das Spiel darunter, eine Münze landet nicht mitten in einer Einstellung. Ein `if`, früh, mit einem `return`. Jeder Bildschirm, der über einem anderen liegt, braucht das, und ein Spiel, das drei davon bekommt und das nicht hat, erzeugt Fehler, die man kaum beschreiben kann. [source,lua] ---- saveSet("coinsPerCredit", coinsPerCredit) ... saveFlush() ---- Setzen, dann flushen. Ohne den Flush passiert das Schreiben am Ende des Frames, und das ist schnell, aber nicht sofort. Ein Betreiber verstellt eine Einstellung und zieht zehn Sekunden später den Stecker, und das ist die eine Stelle in einem Spiel, an der das wirklich passiert. [source,lua] ---- elseif (singeGetTicks() // 400) % 2 == 0 then overlayPrint(2, 4, "INSERT COIN") end ---- Das Blinken. `singeGetTicks()` sind die Millisekunden seit dem Start der Engine, `//` teilt und wirft den Rest weg, also ändert sich die Zahl alle vierhundert Millisekunden, und `% 2` lässt sie zwischen null und eins wechseln. Keine Variable, kein Timer, nichts zurückzusetzen: Die Uhr hat schon gezählt. [source,lua] ---- overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate()) ---- Die Hälfte der Service-Seite, die dem Betreiber gehört. Eine Version, eine Drehung, ob es Artwork gibt und wo die Daten liegen. Nichts davon ist für den Spieler, und all das wirst du dir wünschen, wenn dich jemand wegen eines Automaten anruft, der dreihundert Meilen entfernt steht. [source,lua] ---- singeSetQuitKeyEnabled(false) hasBezel = mainBezelLoaded() ---- Ganz unten, außerhalb jeder Funktion, sodass es einmal läuft, wenn das Skript geladen wird. Keins von beiden muss in jedem Frame passieren, und `mainBezelLoaded` kann sich nicht ändern, während das Spiel läuft. [#_try_it_31] === Probier es aus . *Ändere den Preis.* Öffne das Service-Menü, stell Münzen pro Credit auf zwei und wirf eine Münze ein. Sieh zu, wie der Credit nicht erscheint, und behalte den Münzzähler in der untersten Zeile im Auge. Dann beende das Spiel, starte es noch einmal und sieh, dass der Preis geblieben ist. . *Spiel umsonst.* Schalte Freispiel ein und prüf, dass Start ohne Münzen funktioniert und dass die Credits nicht negativ werden. . *Füg eine Seite hinzu.* Leg eine zweite Seite mit Service-Informationen hinter Knopf 3 -- die Felder von `singeGetSystemInfo()` sind ein guter Anfang, und der Eintrag in der Referenz listet sie alle auf. . *Dreh den Bildschirm.* Starte es mit `--rotate=90` und sieh dir dein Layout an. Dann mach die Service-Seite in dieser Drehung lesbar, was schwerer ist, als es klingt, und genau die Arbeit, die ein hochkant stehender Automat verlangt. . *Nimm die Tastatur weg.* Steck sie aus oder versprich dir, sie nicht anzufassen, und spiel jeden Bildschirm mit einem Pad durch. Alles, was du nicht erreichst, ist ein Fehler. [#_break_it_on_purpose_30] === Mach es absichtlich kaputt Lösch den Block mit `SWITCH_QUIT` aus `onInputPressed`, lass `singeSetQuitKeyEnabled(false)`, wo es ist, und starte es. Jetzt beendet nichts mehr. Esc tut nichts, `Q` tut nichts, und dein Spiel hat keinen Ausweg, und das ist genau das, worum du gebeten hast. Das ist kein Absturz, und es gibt keine Meldung; die Engine hat genau das getan, was man ihr gesagt hat. Schließ das Fenster mit seinem Schließen-Knopf. Das funktioniert immer, egal was ein Skript sagt, und es ist die Tür, die du dir offen gelassen hast. Im Vollbild bringt dich `Alt-Enter` zuerst zurück in ein Fenster. Das hier lohnt es sich zu spüren. Die Beenden-Taste abzuschalten ist das Einzige in diesem Buch, das dich mit einem Programm zurücklassen kann, aus dem du nicht herauskommst, und die Gewohnheit, die das verhindert, ist einfach: Schreib den Ausweg in derselben Sitzung, in der du die Beenden-Taste abschaltest, nie später. [#_what_you_learned_31] === Was du gelernt hast * Ein Automat hat keine Tastatur, also muss jeder Bildschirm im Spiel mit einem Stick und einem Knopf funktionieren. * `controls.cfg` legt Tasten, Pad-Knöpfe und Stick-Richtungen auf die `SWITCH_`-Namen, die dein Spiel schon benutzt, und dein Spiel erfährt nie, was davon gedrückt wurde. * Vier Dateien mit Belegungen werden der Reihe nach gelesen, und jede überschreibt die vorige, sodass es eine Datei für den ganzen Automaten und eine pro Spiel geben kann. * Jedes Gerät, das Singe öffnet, ist ein Gamepad, auch eine Arcade-Encoder-Platine, und das Tool Input Test sagt, was ankommt. * Die Engine behält Pause, Beenden, Screenshot und Mausfang für sich; der Rest gehört dir. * Münzen werden zum Kurs des Betreibers in Credits umgezählt, und ein Automat kehrt von selbst in den Attract-Modus zurück. * `SWITCH_SERVICE` ist der Schalter des Betreibers, und dahinter liegen der Preis, die Schwierigkeit, eine Möglichkeit, die Punkte zu löschen, und was der Automat ist. * `saveFlush()` schreibt eine Einstellung sofort, denn ein Automat verliert ohne Vorwarnung den Strom. * `singeSetQuitKeyEnabled(false)` gibt den Beenden-Schalter an dein Spiel. Schreib den Ausweg im selben Moment. * Ein Bezel ist Artwork mit einem Loch darin, und `--rotate` dreht die ganze Darstellung für einen hochkant stehenden Monitor. [#_the_end] === Das Ende Das sind einunddreißig Lektionen, und du kannst ein Spiel schreiben. Kein perfektes und noch kein großes, aber der Abstand zwischen hier und einem von beiden ist Zeit und Übung, nicht noch ein Buch. Du hast mit fünf Zeilen angefangen, die ein Wort auf einen schwarzen Bildschirm gesetzt haben. Jetzt weißt du, was eine Variable ist, wofür eine Funktion da ist, wie man zeichnet, wie man ein Geräusch macht, wie man merkt, dass sich zwei Dinge berührt haben, wie man eine Punktzahl irgendwo aufbewahrt, wo sie überlebt, wie man Film als Welt laufen lässt, wie man ein Modell in einen Raum mit einem Licht darauf stellt und wie man das Ganze einem Fremden in die Hand gibt. Was du jetzt brauchst, ist die _Singe-Referenz_, nicht dieses Buch. Sie ist das Nachschlagewerk zu jeder Funktion, jedem Argument und jedem Rückgabewert, und sie ist das, was du beim Arbeiten tatsächlich offen haben wirst. Sie liegt diesem Buch bei, als `Reference.pdf` und `Reference.html`. Lies den Eintrag, bevor du den Aufruf benutzt; in den meisten steht ein Hinweis auf den Fehler, den Leute damit machen. Lies auch die Skripte, die mit Singe kommen. `Singe/Menu.singe`, `Singe/Tools.singe` und `Singe/Framework.singe` sind keine Vorführungen, die zum Lesen geschrieben wurden -- sie sind das Menü und die Service-Tools, die du die ganze Zeit benutzt hast, und sie tun echte Arbeit mit denselben Funktionen, die du auch hast. Herauszufinden, wie das Programm von jemand anderem etwas macht, ist eine eigene Fähigkeit, und diese Skripte sind ein freundlicher Ort, sie zu üben. Und dann gibt es Forge, ein eigenes Werkzeug mit einem eigenen Buch, um ein Spiel zu beschreiben, statt es zu schreiben. Das ist ein anderer Weg. Du brauchst es nicht, aber es ist da. Das nächste Spiel gehört dir. Mach zuerst das kleine, mach es fertig und stell es vor jemanden hin. Fertigmachen ist der Teil, von dem dir niemand sagt, dass er der schwere ist, und es ist der einzige Teil, der aus einem Ordner voller Skripte ein Spiel macht, das jemand gespielt hat. Geh und bau es.