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