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

765 lines
28 KiB
Text

[#_lesson_31_in_a_cabinet]
== Lezione 31: In un cabinato
image::learn/31-cabinet.png[La lezione finita, 480]
C'è una differenza tra un gioco e una macchina da sala giochi, e non è il
legno. Una macchina si accende da sola la mattina, gioca davanti a una sala
vuota, prende un gettone da qualcuno che non ne ha mai letto una parola, ed è
ancora in funzione a mezzanotte senza che nessuno abbia toccato una tastiera,
perché la tastiera non c'è. Tutto quello che c'è in questa lezione viene da
qui.
Per fare tutto questo non ti serve un cabinato. Una levetta e due pulsanti
sulla scrivania, o un controller, bastano per trovare ogni errore di questa
lezione, e quasi tutto quello che impari qui migliora un gioco anche su un
normale computer.
[#_there_is_no_keyboard]
=== La tastiera non c'è
Comincia da quello che manca, perché è la cosa che rompe i giochi.
Un cabinato ha una levetta, qualche pulsante, uno sportello per i gettoni, un
pulsante Start e, da qualche parte all'interno, un piccolo interruttore che il
gestore può raggiungere. Tutto qui. Niente Esc, niente lettere, niente mouse,
nessun modo di scrivere. Quindi:
* Ogni menu del tuo gioco deve essere raggiungibile con su, giù e un pulsante.
Se una qualunque schermata del tuo gioco dice «premi Y per continuare», in un
cabinato non funziona.
* Inserire le iniziali vuol dire tre pulsanti e una lettera che scorre, non un
campo di testo. È per questo che nei giochi da sala i nomi sono di tre
lettere.
* Una richiesta che nomina un tasto con la sua lettera è sbagliata. Scrivi
«pulsante 1», o disegna il pulsante.
* Non c'è via d'uscita. Un giocatore non può uscire, e non deve poterlo fare.
Ripercorri il tuo gioco una volta, schermata per schermata, e chiediti di
ognuna: qualcuno riuscirebbe a superarla con una levetta e un pulsante? Quel
giro sul tuo gioco vale più di tutto il resto di questa lezione.
[#_controls_cfg]
=== controls.cfg
Dalla terza lezione leggi i comandi come `SWITCH_UP`, `SWITCH_BUTTON1`,
`SWITCH_COIN1` e così via, e non hai mai detto nemmeno una volta a quale tasto
corrispondono. È proprio questo il loro scopo. Da qualche parte tra il
microinterruttore del cabinato e il tuo `onInputPressed` c'è un file che
decide, e il tuo gioco non fa parte di quella conversazione.
Il file è `controls.cfg`. È Lua, come il tuo script, ed è un elenco di tabelle
-- una per ogni interruttore -- che nominano tutto ciò che può produrlo:
[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 }
----
Ogni nome `INPUT_` del file diventa il valore `SWITCH_` corrispondente nel tuo
gioco: `INPUT_ACTION_1` è lo `SWITCH_BUTTON1` che hai sempre controllato, e
`INPUT_1P_COIN` è `SWITCH_COIN1`. Ogni voce di una tabella produce lo stesso
interruttore, quindi a un gioco non importa se il giocatore ha premuto il
tasto, il pulsante del controller o ha spinto la levetta.
Non devi scrivere tutto il file. Singe parte dalle sue impostazioni
predefinite e legge `controls.cfg` da quattro posti, uno dopo l'altro -- la
cartella dello script del gioco, la cartella da cui è stato avviato Singe, la
cartella sopra la cartella dei dati del gioco e la cartella dei dati del gioco
-- e ognuno che trova sostituisce quello che c'era prima, chiave per chiave.
Così un cabinato ha un solo file in cima per tutta la macchina, e un gioco che
ha bisogno di un pulsante in più ha un file tutto suo di quattro righe. Nella
cartella `Singe` c'è un `controls.cfg.example` da cui copiare.
Lì dentro c'è anche `DEAD_ZONE`: di quanto deve muoversi una levetta prima di
contare come premuta, nelle unità grezze che riporta l'hardware, dove
l'inclinazione massima è `32767`. Il valore predefinito è `15000`. Una levetta
consumata che si sposta da sola ha bisogno di un numero più grande, e
`DEAD_ZONES` imposta un asse alla volta per quell'unica levetta stanca. La
sezione del manuale sulla personalizzazione dei comandi spiega tutto.
[#_every_device_is_a_gamepad]
=== Ogni dispositivo è un controller
I pulsanti di un cabinato sono collegati a una scheda encoder, che il computer
vede come una specie di joystick, e che non somiglia per niente a un controller
Xbox. Per un dispositivo del genere Singe scrive da solo un'assegnazione e poi
lo apre come `GAMEPAD_0`, così arriva a `controls.cfg` e al tuo gioco
esattamente come un controller riconosciuto. C'è una sola famiglia di
dispositivi e nessun secondo vocabolario da imparare.
L'assegnazione che inventa è la più diretta: i pulsanti della scheda, in
ordine, diventano `BUTTON_A`, `BUTTON_B`, `BUTTON_X`, `BUTTON_Y` e così via. Se
i pulsanti del tuo cabinato non sono collegati in quell'ordine, finiranno nei
posti sbagliati, e la correzione non sta nel tuo gioco: `--program` scrive
nella traccia l'assegnazione creata da Singe, in una forma che puoi modificare
e mettere in un `gamecontrollerdb.txt` accanto al gioco.
Quando un pulsante «non fa niente», lo strumento *Input Test* del menu risponde
alla domanda in cinque secondi. Mostra ogni dispositivo che il motore riesce a
vedere, e l'ultimo interruttore che sarebbe stato consegnato a un gioco. O
l'interruttore arriva e il tuo gioco lo ignora, o non arriva mai e il problema
è l'assegnazione o il filo.
[#_four_switches_are_not_yours]
=== Quattro interruttori non sono tuoi
Quasi tutti gli interruttori vanno dritti al tuo gioco. Quattro no: della
pausa, dell'uscita, dello screenshot e del comando che cattura e rilascia il
mouse si occupa il motore stesso.
La pausa è davvero utile, e gratis. Finché è del motore, il tasto assegnato a
`INPUT_PAUSE` congela tutto -- il disco, ogni video, ogni suono e il tuo script
-- disegna un suo indicatore, e scongela tutto alla pressione successiva. Tutto
ciò che il tuo gioco credeva tenuto premuto viene rilasciato come si deve e
premuto di nuovo dopo, così una levetta tenuta durante una pausa non resta mai
bloccata.
L'uscita è quella da togliere, ed è quello che fa una sezione più avanti.
[#_coins_credits_and_start]
=== Gettoni, crediti e Start
Una macchina da sala giochi ha una forma più vecchia di tutti noi, e i
giocatori la conoscono senza che nessuno gliela spieghi:
. *Modalità attract.* Non c'è nessuno. La macchina mostra che cos'è, fa
lampeggiare `INSERT COIN` e non chiede niente.
. *Credito.* Un gettone arriva come `SWITCH_COIN1`. I gettoni diventano crediti
al cambio deciso dal gestore -- non sempre uno a uno.
. *Partita.* `SWITCH_START1` spende un credito e fa partire il gioco.
. *Game over.* Il punteggio, per un attimo, e poi di nuovo in modalità attract
da sola. Nessuno preme niente per uscire: il giocatore se n'è andato.
Due dettagli che si sbagliano spesso. I gettoni si contano, non si spendono: a
due gettoni per credito, un gettone deve restare lì ad aspettare il suo
compagno, e un giocatore che ne ha messo uno e se n'è andato l'ha lasciato alla
persona successiva. E la macchina deve trovare da sola la strada per tornare
alla modalità attract, perché l'ultima cosa che fa un giocatore è smettere di
giocare.
[#_behind_the_service_switch]
=== Dietro l'interruttore di servizio
Dentro lo sportello dei gettoni c'è un pulsante che il giocatore non vede mai e
che il gestore usa di continuo. È `SWITCH_SERVICE`, e il motore lo passa
direttamente a te.
Un gestore che apre il tuo menu di servizio si aspetta di trovare, più o meno
in quest'ordine:
* *il prezzo* -- i gettoni per credito, e un'impostazione di free play per una
macchina in casa o a una fiera;
* *la difficoltà*, perché una macchina in un bar e una in una sala giochi
vogliono risposte diverse;
* *un modo per azzerare la tabella dei punteggi record*, che è in assoluto la
cosa più richiesta di questo elenco;
* *che macchina è* -- la versione del motore, lo schermo, dove stanno i dati --
così una segnalazione di guasto contiene dei fatti;
* *un modo per uscire*, senza spegnere niente.
Tutto ciò che un gestore cambia deve sopravvivere a un'interruzione di
corrente, che in una sala giochi vuol dire un'interruzione nel mezzo di una
partita, senza preavviso, da parte di qualcuno che stacca una spina. `saveSet`
scrive alla fine del fotogramma, che è abbastanza presto per un punteggio e non
abbastanza per un'impostazione che qualcuno ha appena regolato. `saveFlush()`
scrive subito, e un menu di servizio è esattamente ciò a cui serve.
Gli strumenti di servizio del menu incluso, dietro lo stesso interruttore, vale
la pena conoscerli, perché sono su ogni macchina su cui girerà il tuo gioco: il
test dei comandi, i test del suono e delle pistole ottiche, la geometria dello
schermo, il ritardo dell'audio, l'account online e una pagina che cancella i
dati salvati di un singolo gioco. Il tuo non deve ripeterli.
[#_a_machine_that_starts_itself_and_never_quits]
=== Una macchina che si avvia da sola e non esce mai
Il cabinato si accende dalla presa a muro. Nessuno accede al computer. Un gioco
deve comparire sullo schermo nel giro di un minuto o giù di lì, e restarci.
Far partire Singe all'accensione è compito del sistema operativo, non di Singe
-- la macchina avvia il `Menu.sh` o il `Menu.bat` che il motore scrive accanto
ai tuoi giochi, oppure direttamente il tuo gioco. Compito tuo è quello che
succede per il resto della giornata.
Togli di mezzo il tasto per uscire:
[source,lua]
----
singeSetQuitKeyEnabled(false)
----
A quel punto il motore ignora l'interruttore assegnato a `INPUT_QUIT` e lo
consegna a te come `SWITCH_QUIT`, come tutti gli altri, così puoi prima
chiedere, prima salvare, o non fare proprio niente. Il pulsante di chiusura
della finestra fa sempre uscire, qualunque cosa tu imposti, e tra un attimo
questo conterà.
Poi ci sono alcune opzioni che appartengono alla macchina invece che al tuo
script, e vanno in un `settings.cfg` accanto ai giochi -- con gli stessi nomi
delle opzioni da riga di comando, senza i trattini:
[source,lua]
----
fullscreen = true
screen = 2
idleexit = 900
startsilent = true
----
`idleexit` esce dopo quel numero di secondi senza nessun comando di nessun
tipo, per un cabinato che deve restituire lo schermo a un menu o a un
front-end. `startsilent` parte senza audio finché qualcuno non tocca qualcosa,
ed è così che una sala piena di macchine diventa sopportabile. Ce ne sono
altre; le sezioni del manuale sulle impostazioni e sulla riga di comando le
elencano tutte.
[#_artwork_round_the_picture]
=== Grafica attorno all'immagine
Un vero cabinato ha un bezel (una cornice decorativa) stampato attorno allo
schermo: una grafica con un buco attraverso cui si vede il gioco. Singe ne
disegna uno per te.
----
Singe --bezel cabinet.png MyGame.game
----
Il file viene cercato in una cartella `bezels` dentro il gioco -- un gioco
impacchettato può portarsi dietro la sua -- e poi in una cartella `bezels`
nella cartella dei dati, così un giocatore può mettere la sua grafica su un
gioco senza toccarlo. Accanto al file della grafica, con il suo stesso nome, un
piccolo file Lua dice dove va l'immagine del gioco, nei pixel della grafica
stessa:
[source,lua]
----
-- bezels/cabinet.cfg
CUTOUT = { x = 200, y = 120, width = 800, height = 500 }
----
Siccome quei numeri sono della grafica stessa, restano giusti con qualunque
dimensione della finestra: l'immagine e il buco si ridimensionano insieme.
Senza quel file l'immagine del gioco occupa tutta la finestra, come ha sempre
fatto.
Di base la grafica sta dietro a tutto e il tuo gioco copre il buco.
`--bezelflip` invece la mette davanti, così solo le sue parti trasparenti
lasciano passare il gioco, che è quello che vuoi per bordi sfumati o sagomati
-- e a quel punto anche il tuo overlay finisce sotto, a meno che tu non dica
altrimenti:
[source,lua]
----
if mainBezelLoaded() then
setOverlayOnTop(true)
end
----
`mainBezelLoaded()` ti dice anche se una grafica c'è o no, ed è così che un
gioco decide di disegnare una cornice sua quando non ce n'è.
[#_a_monitor_on_its_side]
=== Un monitor su un fianco
Molti monitor da sala giochi sono montati in verticale. Ruota tutta la
presentazione:
----
Singe --rotate=90 MyGame.game
----
L'immagine, l'overlay, l'eventuale GUI, una scena 3D e le particelle ruotano
tutti insieme, e le posizioni del mouse e della pistola ottica vengono ruotate
all'indietro, così un clic finisce ancora dove lo vede il giocatore. Un quarto
di giro scambia anche la forma dell'area, così una finestra alta viene riempita
invece di ridursi a una striscia tra due bande nere.
Sono accettati solo `0`, `90`, `180` e `270`. Uno script può cambiare la
rotazione mentre gira con `vldpSetRotate`, che per qualsiasi altro valore
risponde `false` invece di terminare il gioco, e `vldpGetRotate()` la rilegge
-- e vale la pena metterla in una schermata di servizio, perché «l'immagine è
girata» e «il monitor è girato» sono guasti diversi.
Quello che la rotazione non fa è riscrivere il tuo gioco. Un gioco disegnato
per uno schermo largo, ruotato di novanta gradi, è un gioco largo messo su un
fianco. Progettare per uno schermo alto vuol dire progettare per uno schermo
alto.
[#_the_script_3]
=== Lo script
Tutta la forma: modalità attract, gettone, credito, partita, game over,
servizio, e un'uscita che chiede conferma. Il gioco in mezzo è un pulsante e un
timer di quindici secondi, perché stavolta il punto non è il gioco -- è la
macchina che gli sta attorno.
[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
----
Avvialo e premi `5` per un gettone, `1` per Start e `9` per l'interruttore di
servizio -- sono le impostazioni predefinite del `controls.cfg` incluso. Poi
premi Esc, e leggi che cosa dice prima di premerlo di nuovo.
[#_what_just_happened_26]
=== Cos'è appena successo
[source,lua]
----
local coinsPerCredit = saveGet("coinsPerCredit", 1)
local difficulty = saveGet("difficulty", 2)
local freePlay = saveGet("freePlay", false)
----
Le impostazioni del gestore vengono lette una volta, in cima, con un valore
sensato per una macchina che non è mai stata configurata. `saveGet` con un
secondo argomento restituisce quel valore quando non è stato salvato niente, ed
è così che si distingue un primo avvio da uno successivo.
[source,lua]
----
local function insertCoin()
coins = coins + 1
while coins >= coinsPerCredit do
coins = coins - coinsPerCredit
credits = credits + 1
end
end
----
Entrano gettoni, escono crediti, al cambio deciso dal gestore. Il `while` al
posto di un `if` non è prudenza contro un input impossibile -- è il gestore che
abbassa il prezzo da quattro gettoni a uno mentre lì dentro ci sono tre gettoni
in attesa.
[source,lua]
----
if what == SWITCH_QUIT then
if state == "confirm" then
singeQuit()
else
goBackTo = state
state = "confirm"
end
return
end
----
L'interruttore per uscire arriva al tuo gioco solo grazie al
`singeSetQuitKeyEnabled(false)` in fondo allo script. Premerlo una volta fa la
domanda; premerlo di nuovo, dalla schermata della domanda, fa sì che sia tu a
chiamare `singeQuit()`. `goBackTo` ricorda dov'era il giocatore, così
rispondere di no lo rimette nella sua partita invece che sulla schermata
attract.
Questo blocco è il primo della funzione e finisce con `return`, così nient'altro può
inghiottire l'interruttore. Ogni altro stato del gioco viene gestito sotto.
[source,lua]
----
if state == "service" then
serviceInput(what)
return
end
----
Di nuovo la stessa forma. Finché il menu di servizio è aperto, ogni
interruttore appartiene a lui -- la levetta non guida il gioco che c'è sotto,
un gettone non arriva nel mezzo di una regolazione. Un `if`, presto, con un
`return`. Ogni schermata che sta sopra un'altra ne ha bisogno, e un gioco che
ne accumula tre senza averlo produce bug quasi impossibili da descrivere.
[source,lua]
----
saveSet("coinsPerCredit", coinsPerCredit)
...
saveFlush()
----
Imposta, poi forza la scrittura. Senza forzarla, la scrittura avviene alla fine
del fotogramma, il che è presto ma non subito. Un gestore regola
un'impostazione e stacca la spina dieci secondi dopo, e questo è l'unico posto
in un gioco in cui succede davvero.
[source,lua]
----
elseif (singeGetTicks() // 400) % 2 == 0 then
overlayPrint(2, 4, "INSERT COIN")
end
----
Il lampeggio. `singeGetTicks()` sono i millisecondi da quando è partito il
motore, `//` divide e butta via il resto, quindi il numero cambia ogni
quattrocento millisecondi, e `% 2` lo fa alternare tra zero e uno. Nessuna
variabile, nessun timer, niente da azzerare: l'orologio stava già contando.
[source,lua]
----
overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate())
----
La metà della pagina di servizio che riguarda il gestore. Una versione, una
rotazione, se c'è una grafica e dove stanno i dati. Niente di tutto ciò è per
il giocatore, ed è tutto quello che vorresti avere quando qualcuno ti telefona
per una macchina a cinquecento chilometri di distanza.
[source,lua]
----
singeSetQuitKeyEnabled(false)
hasBezel = mainBezelLoaded()
----
In fondo, fuori da ogni funzione, così viene eseguito una volta quando lo
script si carica. Nessuna delle due cose va fatta a ogni fotogramma, e
`mainBezelLoaded` non può cambiare mentre il gioco gira.
[#_try_it_31]
=== Prova tu
. *Cambia il prezzo.* Apri il menu di servizio, imposta i gettoni per credito a
due e inserisci un gettone. Guarda il credito non comparire, e guarda il
contatore dei gettoni sulla riga in fondo. Poi esci, riavvialo e verifica che
il prezzo sia rimasto.
. *Gioca gratis.* Attiva il free play e controlla che Start funzioni senza
gettoni e che i crediti non vadano sotto zero.
. *Aggiungi una pagina.* Metti una seconda pagina di informazioni di servizio
dietro il pulsante 3 -- i campi di `singeGetSystemInfo()` sono un buon
inizio, e la voce del riferimento li elenca tutti.
. *Ruota lo schermo.* Avvialo con `--rotate=90` e guarda la tua disposizione.
Poi rendi la pagina di servizio leggibile con quella rotazione, il che è più
difficile di quanto sembri ed è esattamente il lavoro che chiede un cabinato
verticale.
. *Togli di mezzo la tastiera.* Scollegala, o prometti a te stesso di non
toccarla, e gioca tutte le schermate con un controller. Qualunque cosa tu non
riesca a raggiungere è un bug.
[#_break_it_on_purpose_30]
=== Rompilo apposta
Cancella il blocco di `SWITCH_QUIT` da `onInputPressed`, lasciando
`singeSetQuitKeyEnabled(false)` dov'è, e avvialo.
Ora niente fa uscire. Esc non fa niente, `Q` non fa niente, e il tuo gioco non
ha via d'uscita, che è esattamente quello che hai chiesto. Non è un crash e non
c'è nessun messaggio: il motore ha fatto esattamente quello che gli è stato
detto.
Chiudi la finestra con il suo pulsante di chiusura. Quello funziona sempre,
qualunque cosa dica uno script, ed è la porta che ti sei lasciato aperta. A
schermo intero, prima `Alt-Enter` ti riporta in una finestra.
Vale la pena provarlo sulla tua pelle. Disattivare il tasto per uscire è
l'unica cosa di questo libro che può lasciarti con un programma da cui non
riesci a uscire, e l'abitudine che lo evita è semplice: scrivi la via d'uscita
nella stessa seduta in cui disattivi il tasto per uscire, mai dopo.
[#_what_you_learned_31]
=== Cosa hai imparato
* Un cabinato non ha la tastiera, quindi ogni schermata del gioco deve
funzionare con una levetta e un pulsante.
* `controls.cfg` assegna tasti, pulsanti del controller e direzioni della
levetta ai nomi `SWITCH_` che il tuo gioco usa già, e il tuo gioco non viene
mai a sapere che cosa è stato premuto.
* Vengono letti, uno dopo l'altro, quattro file di assegnazioni, e ognuno
sostituisce il precedente, così possono esistere sia un file per tutta la
macchina sia un file per ogni gioco.
* Ogni dispositivo che Singe apre è un controller, compresa una scheda encoder
da sala giochi, e lo strumento Input Test dice che cosa sta arrivando.
* Il motore tiene per sé pausa, uscita, screenshot e cattura del mouse; il
resto è tuo.
* I gettoni si contano e diventano crediti al cambio deciso dal gestore, e una
macchina torna da sola alla modalità attract.
* `SWITCH_SERVICE` è l'interruttore del gestore, e dietro ci sono il prezzo, la
difficoltà, un modo per azzerare i punteggi e che macchina è.
* `saveFlush()` scrive un'impostazione subito, perché un cabinato perde la
corrente senza preavviso.
* `singeSetQuitKeyEnabled(false)` consegna l'interruttore per uscire al tuo
gioco. Scrivi la via d'uscita nello stesso momento.
* Un bezel è una grafica con un buco, e `--rotate` ruota tutta la
presentazione per un monitor verticale.
[#_the_end]
=== Fine
Sono trentuno lezioni, e sai scrivere un gioco.
Non un gioco perfetto, e non ancora un gioco grande, ma quello che ti separa da
entrambi è tempo e pratica, non un altro libro. Hai cominciato con cinque righe
che mettevano una parola su uno schermo nero. Ora sai che cos'è una variabile,
a che cosa serve una funzione, come si disegna, come si fa un rumore, come si
capisce quando due cose si sono toccate, come si tiene un punteggio in un posto
dove sopravvive, come si usa un filmato come mondo, come si mette un modello in
una stanza con una luce accesa, e come si consegna il tutto a uno sconosciuto.
Quello che ti serve adesso è il _Riferimento di Singe_, non questo libro. È il
riferimento per ogni funzione, ogni argomento e ogni valore restituito, ed è
quello che terrai davvero aperto mentre lavori. Viene con questo libro, come
`Reference.pdf` e `Reference.html`. Leggi la voce prima di usare la chiamata; nella
maggior parte delle voci c'è una nota sull'errore che si fa di solito con
quella chiamata.
Leggi anche gli script che vengono con Singe. `Singe/Menu.singe`,
`Singe/Tools.singe` e `Singe/Framework.singe` non sono dimostrazioni scritte per
essere lette -- sono il menu e gli strumenti di servizio che hai usato fin
dall'inizio, e fanno un lavoro vero con le stesse funzioni che hai tu. Capire
come il programma di qualcun altro fa una certa cosa è un'abilità a sé, e
quelli sono un posto gentile in cui esercitarla.
E poi c'è Forge, uno strumento separato con un libro tutto suo, per descrivere
un gioco invece di scriverlo. È una strada diversa. Non ti serve, ma c'è.
Il prossimo gioco è tuo. Fai prima quello piccolo, finiscilo, e mettilo davanti
a qualcuno. Finire è la parte che nessuno ti dice che è quella difficile, ed è
l'unica parte che trasforma una cartella piena di script in un gioco a cui
qualcuno ha giocato.
Vai a costruirlo.