724 lines
29 KiB
Text
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.
|