765 lines
28 KiB
Text
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.
|