singe/docs/de/lessons/31-cabinet.adoc

724 lines
29 KiB
Text

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