[#_lesson_17_light_guns] == Lezione 17: Pistole ottiche image::learn/17-light-guns.png[La lezione finita, 480] Stai per fare un tiro a segno. I bersagli compaiono sopra il video nei fotogrammi che scegli tu, tu miri allo schermo e premi un grilletto, e quelli che colpisci fanno punti. Per scriverlo non ti serve una pistola ottica. Ti serve un mouse, perché per Singe una pistola ottica *è* un mouse: riporta una posizione nelle stesse coordinate, attraverso le stesse chiamate, e il suo grilletto arriva come lo stesso interruttore di un pulsante del mouse. Scrivi il gioco per il mouse sulla tua scrivania e funziona su un cabinato con una pistola imbullonata sopra, ed è l'unico modo sensato di farlo. Una pistola non è un tipo diverso di comando. È un mouse che qualcuno ha puntato. [#_what_you_are_pointing_at] === A che cosa stai mirando Comincia come nella quattordicesima lezione, con il video che il motore ha estratto per te: ---- Singe -R -v Singe/menuBackground.mkv gallery ---- Quel filmato è una griglia viola e un tramonto. Non è una rapina in banca. Ogni numero di fotogramma di questa lezione si riferisce a lui, così il codice funziona il giorno stesso in cui lo scrivi, e tutto lo spezzone è 720 per 480 e lungo circa 420 fotogrammi, che a circa trenta fotogrammi al secondo fanno quattordici secondi. Immagina un filmato migliore. Al codice non importa che cosa c'è nell'immagine. [#_match_the_overlay_to_the_video] === Adatta l'overlay al video Metti questo in cima a `gallery.singe`, sotto la riga del `dofile`: [source,lua] ---- overlaySetResolution(discGetWidth(), discGetHeight()) ---- Senza quella chiamata l'overlay è *metà* del video in ciascuna direzione -- qui 360 per 240 -- e ogni coordinata con cui disegni, e ogni posizione del mouse che ricevi, sta in quello spazio più piccolo. Non c'è niente di sbagliato nell'overlay piccolo, ma è una conversione in più da tenere a mente mentre cerchi di capire dove è finito un colpo. Adattarlo al video vuol dire che un bersaglio a `x = 80` sta a ottanta pixel dal bordo dell'immagine, e lo stesso vale per un colpo a `x = 80`. La voce del Riferimento su `overlaySetResolution` spiega quanto costa. [#_where_the_gun_is_pointing] === Dove punta la pistola Lo dice una sola chiamata: [source,lua] ---- local x, y = mouseGetPosition(0) ---- `mouseGetPosition` restituisce due numeri, cosa che non hai ancora visto fare a una funzione e che a Lua va benissimo: una funzione può restituire tutti i valori che vuole, e tu li prendi elencando altrettanti nomi a sinistra del segno `=`. Lo `0` dice *quale* dispositivo di puntamento ti interessa, contando da zero. Con un mouse solo, o una pistola sola, la risposta è sempre il dispositivo `0`. I numeri sono in coordinate dell'overlay, non in pixel del desktop, ed è per questo che valeva la pena fare la chiamata di prima. La posizione è l'ultima che il dispositivo ha riportato, quindi puoi chiederla quando vuoi, tutte le volte che vuoi. Chiedila in `onOverlayUpdate` e disegna con la risposta. C'è un altro modo di avere gli stessi numeri. `onMouseMoved` è una callback, come `onOverlayUpdate`, e il motore la chiama ogni volta che un puntatore si muove, passandole la nuova posizione. È la scelta giusta quando vuoi *reagire* al movimento. Per disegnare un mirino non lo vuoi: vuoi sapere dov'è la cosa in questo momento, e `mouseGetPosition` te lo dice senza che tu debba tenerne una copia. [#_drawing_a_crosshair] === Disegnare un mirino Il motore non te lo disegna. Un mirino è quattro linee e un cerchio: [source,lua] ---- local function drawCrosshair(x, y) colorForeground(255, 255, 255, 255) overlayCircle(x, y, 10) overlayLine(x - 18, y, x - 4, y) overlayLine(x + 4, y, x + 18, y) overlayLine(x, y - 18, x, y - 4) overlayLine(x, y + 4, x, y + 18) end ---- Gli spazi vuoti contano più delle linee. Fermare i quattro raggi prima del cerchio ti lascia un buco nel mezzo da cui vedere il bersaglio, che è tutto lo scopo di un mirino e la prima cosa che i principianti coprono disegnandoci sopra. Una cosa che dovresti chiedere prima di disegnarlo: [source,lua] ---- local drawAim = singeWantsCrosshairs() ---- Restituisce `false` quando il giocatore ha avviato Singe con `--nocrosshair`. Alcune pistole ottiche mettono sullo schermo un loro puntino, e due mirini a pochi pixel l'uno dall'altro sono peggio di nessuno. La risposta non può cambiare mentre il gioco gira, quindi leggila una volta all'inizio e tienila in una variabile. Non devi nascondere il puntatore di sistema, perché Singe l'ha già fatto. Si prende il mouse all'avvio, il che lo confina nella finestra e nasconde la freccia del desktop. Se mai lo rivuoi -- per un editor di livelli, per dire -- `mouseSetCaptured(false)` lo rilascia, e il tasto assegnato a `INPUT_GRAB`, che nei comandi forniti è `G`, fa la stessa cosa avanti e indietro mentre giochi. [#_the_trigger] === Il grilletto Un grilletto è un pulsante, e i pulsanti arrivano a `onInputPressed`, che hai incontrato per la prima volta nella terza lezione: [source,lua] ---- function onInputPressed(what, device) if what ~= SWITCH_BUTTON3 then return end local x, y = mouseGetPosition(device or 0) -- ... end ---- `SWITCH_BUTTON3` è il pulsante sinistro del mouse, e su una pistola è il grilletto. Il `controls.cfg` fornito assegna il pulsante sinistro a `SWITCH_BUTTON3`, il destro a `SWITCH_BUTTON1` e quello centrale a `SWITCH_BUTTON2`. Quei numeri sembrano mescolati perché lo sono: gli interruttori sono i pulsanti del pannello da sala giochi, e quale comando fisico sia collegato a quale è affare del cabinato, non del gioco. Anche Maiusc sinistro è assegnato a `SWITCH_BUTTON3`, così puoi sparare senza mouse mentre fai le prove. La parte nuova è il secondo argomento. Quando l'interruttore arriva da un mouse o da una pistola ottica, `device` è l'indice di quel dispositivo -- `0` per il primo, `1` per il secondo e così via. Quando arriva da un tasto o da un controller è `nil`, perché non c'era di mezzo nessun dispositivo di puntamento. Ecco perché la chiamata sopra dice `device or 0`: se `device` è `nil`, usa `0` al suo posto. Userai `or` così di continuo, e si legge esattamente come suona -- «device, oppure niente, e in quel caso zero». Quell'unico argomento in più è ciò che rende possibile un cabinato con due pistole. La pistola del primo giocatore è il dispositivo `0` e quella del secondo è il dispositivo `1`, e la stessa `onInputPressed` le distingue senza che tu debba indovinare. [#_did_it_hit_anything] === Ha colpito qualcosa Sai già come rispondere. È l'undicesima lezione: [source,lua] ---- if collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then ---- Un colpo è un punto. Un bersaglio è un rettangolo. `collidePointRect` chiede se il punto sta dentro il rettangolo, e il bordo conta come dentro. Sparare non ha niente di speciale. La verifica dei colpi di un gioco di pistole è la stessa che usa un platform per scoprire se sei atterrato su un blocco, e la famiglia `collide` del Riferimento ha il resto delle forme. [#_targets_that_come_and_go] === Bersagli che vanno e vengono L'unica cosa che il video cambia è *quando* esiste un bersaglio. Nell'undicesima lezione una roccia restava lì finché non le sparavi. Qui un bersaglio è sullo schermo mentre il filmato mostra la parte della scena a cui appartiene, e poi sparisce, che tu gli abbia sparato o no. Sono due campi in più nel record, e la sesta lezione ti ha dato lo strumento: [source,lua] ---- local targets = { { first = 40, last = 110, x = 80, y = 110 }, { first = 120, last = 190, x = 470, y = 140 }, { first = 200, last = 270, x = 290, y = 250 }, { first = 280, last = 360, x = 150, y = 300 }, } ---- Quattro record, ognuno con il fotogramma in cui compare il bersaglio, il fotogramma in cui se ne va, e il punto in cui si trova. Aggiungere un quinto bersaglio vuol dire aggiungere una riga. È la forma a cui ricorrere ogni volta che ti accorgi che stai per scrivere le stesse quattro righe di codice con numeri diversi dentro. Se un bersaglio conta o no è allora una piccola funzione tutta sua: [source,lua] ---- local function targetIsUp(target, frame) return not target.hit and frame >= target.first and frame <= target.last end ---- Glielo chiede il disegno, e glielo chiede lo sparo, ed è proprio questo il motivo per metterlo in un posto solo: un bersaglio che vedi e un bersaglio che puoi colpire non possono mai essere in disaccordo. [#_the_whole_thing_9] === Tutto insieme [source,lua] ---- -- Impara a programmare con Singe -- Lezione 17: Pistole ottiche dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local FIRST_FRAME = 20 local LAST_FRAME = 400 local TARGET_SIZE = 96 local HIT_SCORE = 100 local FLASH_TIME = 12 local targets = { { first = 40, last = 110, x = 80, y = 110 }, { first = 120, last = 190, x = 470, y = 140 }, { first = 200, last = 270, x = 290, y = 250 }, { first = 280, last = 360, x = 150, y = 300 }, } local aimX = overlayGetWidth() / 2 local aimY = overlayGetHeight() / 2 local score = 0 local shots = 0 local hits = 0 local flash = 0 local flashX = 0 local flashY = 0 local drawAim = singeWantsCrosshairs() local function drawCrosshair(x, y) colorForeground(255, 255, 255, 255) overlayCircle(x, y, 10) overlayLine(x - 18, y, x - 4, y) overlayLine(x + 4, y, x + 18, y) overlayLine(x, y - 18, x, y - 4) overlayLine(x, y + 4, x, y + 18) end local function targetIsUp(target, frame) return not target.hit and frame >= target.first and frame <= target.last end local function startRound() score = 0 shots = 0 hits = 0 flash = 0 for _, target in ipairs(targets) do target.hit = false end discSkipToFrame(FIRST_FRAME) end function onInputPressed(what, device) if what == SWITCH_START1 then startRound() return end if what ~= SWITCH_BUTTON3 then return end local x, y = mouseGetPosition(device or 0) local frame = discGetFrame() shots = shots + 1 for _, target in ipairs(targets) do if targetIsUp(target, frame) and collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then target.hit = true hits = hits + 1 score = score + HIT_SCORE flash = FLASH_TIME flashX = x flashY = y return end end end function onOverlayUpdate() local frame = discGetFrame() if frame >= LAST_FRAME then startRound() frame = FIRST_FRAME end aimX, aimY = mouseGetPosition(0) overlayClear() for _, target in ipairs(targets) do if targetIsUp(target, frame) then colorForeground(255, 200, 0, 255) overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1) overlayCircle(target.x + TARGET_SIZE / 2, target.y + TARGET_SIZE / 2, TARGET_SIZE / 3) end end if flash > 0 then colorForeground(255, 255, 255, 255) overlayCircle(flashX, flashY, 30 - flash * 2) flash = flash - 1 end overlayPrint(2, 1, "SCORE " .. score .. " HITS " .. hits .. "/" .. shots .. " FRAME " .. frame) if drawAim then drawCrosshair(aimX, aimY) end return OVERLAY_UPDATED end startRound() ---- Mira a un riquadro giallo e fai clic. Il contatore nell'angolo sale, un anello bianco si apre dove hai colpito, e il riquadro sparisce. Se manchi, sale solo il conto dei colpi sparati, che è il tabellone più economico che ci sia e dice a un giocatore come se la sta cavando più di quanto faccia il punteggio. Il giro ricomincia da solo quando il filmato finisce, e `1` lo fa ricominciare quando vuoi. [#_what_just_happened_13] === Cos'è appena successo [source,lua] ---- local frame = discGetFrame() if frame >= LAST_FRAME then startRound() frame = FIRST_FRAME end ---- Il video è l'orologio, come nella quattordicesima lezione, e `discGetFrame` è il modo in cui lo leggi. Tutto il resto del fotogramma si decide a partire da quel numero. Quando il filmato supera la fine dell'ultimo bersaglio, il giro ricomincia: `startRound` azzera il punteggio, segna tutti i bersagli come non colpiti e rimanda il disco all'inizio con `discSkipToFrame`. La riga sotto imposta anche la copia locale di `frame`, perché è stata letta prima del salto e contiene ancora il numero vecchio. Senza quella riga, il disegno qui sotto passerebbe un fotogramma a lavorare con un numero che non è più vero. [source,lua] ---- shots = shots + 1 for _, target in ipairs(targets) do if targetIsUp(target, frame) and collidePointRect(...) then ---- Conta il colpo prima di cercare un bersaglio colpito, così conta anche un colpo a vuoto. Poi scorri l'elenco. Il `return` dentro il ciclo si ferma al primo bersaglio colpito, il che vuol dire che un proiettile non può prendere due bersagli sovrapposti -- una regola che devi decidere in un senso o nell'altro, e questa è la riga che la decide. [source,lua] ---- overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1) ---- `overlayBox` vuole due angoli opposti, non un angolo e una dimensione, quindi l'angolo lontano è quello vicino più la dimensione. Il `- 1` c'è perché tutti e due gli angoli sono inclusi: un riquadro da `80` a `175` è largo novantasei pixel. `collidePointRect` invece vuole un angolo e una dimensione, ed è per questo che lì `TARGET_SIZE` entra da solo e qui sommato all'angolo. Confondere le due cose è un bug davvero comune, e si vede come bersagli che puoi colpire un po' fuori da dove sono disegnati. [source,lua] ---- if flash > 0 then colorForeground(255, 255, 255, 255) overlayCircle(flashX, flashY, 30 - flash * 2) flash = flash - 1 end ---- `flash` è un conto alla rovescia in fotogrammi, messo a dodici quando un colpo va a segno e diminuito di uno ogni volta che lo schermo viene disegnato. Siccome il raggio è calcolato *a partire* dal conto alla rovescia, l'anello si allarga mentre il numero si rimpicciolisce. Dodici fotogrammi sono meno di mezzo secondo. Una reazione che dura più di così smette di sembrare una conseguenza del grilletto e comincia a sembrare il tempo che fa. [#_two_guns] === Due pistole Sei a due modifiche da un cabinato per due giocatori, e nessuna delle due sta nel ciclo del gioco. La prima è `mouseSetMode(MOUSE_MANY)`, che dice a Singe di leggere ogni dispositivo per conto suo invece di riunirli tutti in un unico cursore. `mouseHowMany()` dice quanti ne ha trovati all'avvio, quindi di solito un gioco lo chiede prima di impegnarsi: [source,lua] ---- if mouseHowMany() >= 2 then mouseSetMode(MOUSE_MANY) end ---- Dopo di che, `mouseGetPosition(1)` è la seconda pistola, e l'argomento `device` di `onInputPressed` dice quale pistola ha sparato. Tieni due mirini, due punteggi e un solo elenco di bersagli, e hai un gioco cooperativo. La seconda modifica non sta affatto nel tuo script. Il `controls.cfg` fornito collega solo i pulsanti del *primo* mouse, quindi il grilletto della seconda pistola non arriva da nessuna parte. Un cabinato con due pistole ha bisogno in quel file di una riga sua che le nomini tutte e due: [source,lua] ---- INPUT_ACTION_3 = { SCANCODE.LSHIFT, GAMEPAD_0.BUTTON_X, MOUSE_0.BUTTON_LEFT, MOUSE_1.BUTTON_LEFT } ---- Scrivilo nelle tue istruzioni. Un giocatore la cui seconda pistola non fa niente darà la colpa al tuo gioco, e avrà ragione a metà. [#_the_other_way_to_test_a_hit] === L'altro modo di verificare un colpo C'è un secondo modo di rispondere a «quel colpo ha preso qualcosa?», e vale la pena conoscerlo anche se oggi non lo userai. `vldpGetPixel(x, y)` legge il colore di un pixel del fotogramma video che è sullo schermo in quel momento. I giochi lo usano fin dalla primissima versione di Singe: dipingi ogni bersaglio nel filmato di un colore che non compare da nessun'altra parte, e quando qualcuno preme il grilletto chiedi di che colore è quello che sta sotto il mirino. Ti fa avere gratis forme di colpo che seguono l'azione alla perfezione, senza rettangoli da preparare. Ti costa un filmato che devi preparare in quel modo, e una verifica dei colpi che non puoi vedere né mettere a punto. La voce del Riferimento su `vldpGetPixel` ha i dettagli. I rettangoli sono la scelta giusta di partenza; questo tienilo in tasca. [#_what_only_a_cabinet_cares_about] === Cose che contano solo per un cabinato Tutto quello che c'è sopra funziona sulla tua scrivania. Diverse cose sulla tua scrivania non esistono proprio, e questo è un buon momento per nominarle, così più avanti non saranno una sorpresa. *Calibrazione.* A una vera pistola ottica bisogna dire dov'è lo schermo, perché è una telecamera che guarda un monitor da dovunque si trovi il giocatore. Gli strumenti di servizio di Singe hanno una schermata Light Gun che mette dei bersagli al centro e in ogni angolo e segna dove ha puntato davvero la pistola, così puoi vedere da che parte è sballata. Il tuo gioco non fa la calibrazione e non dovrebbe provarci. *Il bordo Sinden.* Una famiglia di pistole molto diffusa trova lo schermo cercando una cornice luminosa disegnata intorno all'immagine. `--sindengun` disegna quella cornice, e il motore riporta le posizioni dentro l'immagine più piccola che sta al suo interno, così il tuo gioco continua a ricevere coordinate che vogliono dire quello che volevano dire prima. *Schermi che non hanno la forma del filmato.* `ratioGetX()` e `ratioGetY()` ti danno i numeri che un giocatore ha indicato sulla riga di comando per uno schermo le cui proporzioni non corrispondono a quelle del filmato. Singe non li applica; un gioco che li vuole fa i conti da sé. *Pistole che se ne vanno.* `SWITCH_MOUSE_DISCONNECT` arriva a `onInputPressed` quando una pistola viene scollegata. Nessun pulsante lo produce; lo genera il motore. Un gioco che lo tiene d'occhio può mettere sullo schermo «PLAYER 2 GUN DISCONNECTED» invece di sembrare morto. *Niente mouse.* Un cabinato con un joystick e nessun dispositivo di puntamento può muovere il cursore con la levetta, cosa che si attiva con `joyMouseEnable`. Niente di tutto questo cambia il gioco che hai scritto. Tutto questo è la trentunesima lezione. [#_try_it_17] === Prova tu . *Sposta un bersaglio.* Cambia la `x` del terzo bersaglio in `680` ed eseguilo. Adesso quasi tutto il bersaglio sporge oltre il bordo destro, perché l'overlay è largo 720 e il bersaglio 96. Puoi ancora sparare alla striscia che resta. Decidi che cosa vuoi che succeda ai bordi. . *Rendi i bersagli più facili.* Cambia `TARGET_SIZE` in `140`. Nota che cambiano sia il disegno sia la verifica dei colpi, e che hai modificato un numero solo. È a questo che serve una costante con un nome. . *Tieni il conto della precisione.* Aggiungi una riga che stampa `hits` come percentuale di `shots`. Proteggiti dalla divisione per zero prima del primo colpo, e scopri che cosa stampa Lua se non lo fai. . *Fai in modo che un bersaglio ti punisca.* Aggiungi un campo `penalty = true` a un record e togli punti quando viene colpito. Nota che stai aggiungendo un campo a un record solo, non a tutti e quattro, e che quelli senza hanno lì `nil`. . *Togli il `return`* dal ciclo in `onInputPressed`, poi sovrapponi due bersagli dando loro gli stessi fotogrammi e quasi la stessa posizione. Adesso un colpo li prende tutti e due. Decidi quale comportamento vuole il tuo gioco. [#_break_it_on_purpose_16] === Rompilo apposta Cambia la verifica dello sparo perché usi `SWITCH_BUTTON1` invece di `SWITCH_BUTTON3`, ed eseguilo. Non si rompe niente. Non compare nessun errore. Il pulsante sinistro smette di fare qualcosa, e adesso spara il pulsante *destro*. Questo è il tipo di bug peggiore e il più comune nel codice che gestisce i comandi: il programma non è sbagliato, è solo collegato alla cosa sbagliata. Non c'è nessun messaggio di errore da leggere, perché hai fatto una domanda sensata su un interruttore che esiste e lui ha risposto onestamente. Lo trovano due cose. La prima è `debugPrint`, dell'ottava lezione: [source,lua] ---- function onInputPressed(what, device) debugPrint("switch " .. what .. " from device " .. tostring(device)) ---- Adesso ogni pressione stampa una riga, e puoi vedere che cosa manda davvero il grilletto. Nota `tostring(device)`: `device` è `nil` per la pressione di un tasto, e attaccare `nil` a una stringa con `..` è un errore. `tostring` lo trasforma nella parola `nil` e la riga viene stampata. La seconda è la schermata Input Test del motore, negli strumenti di servizio, che mostra ogni dispositivo che riesce a vedere e l'ultimo interruttore che il gioco avrebbe ricevuto. Quando un pulsante non fa niente, quella schermata ti dice quale metà del problema hai: o l'interruttore arriva e il tuo gioco lo ignora, o non arriva proprio. [#_what_you_learned_17] === Cosa hai imparato * Una pistola ottica si presenta come un mouse, quindi un gioco scritto per il mouse funziona con una pistola. * `mouseGetPosition(device)` restituisce due valori, la posizione del puntatore in coordinate dell'overlay. * In Lua una funzione può restituire più di un valore, e tu li prendi elencando altrettanti nomi. * `singeWantsCrosshairs()` dice se disegnare un mirino tuo. Leggilo una volta. * Singe si prende il puntatore di sistema e lo nasconde all'avvio; tu non devi farlo. * `onInputPressed` riceve un secondo argomento che dice quale mouse o pistola ha sparato, ed è `nil` quando era un tasto o un controller. * La verifica di un colpo contro un rettangolo è `collidePointRect`, la stessa collisione che hai imparato nell'undicesima lezione. * La finestra di fotogrammi di un bersaglio è solo due campi in più nel suo record. * `a or b` ti dà `b` quando `a` è `nil`. * La calibrazione, il bordo Sinden e il collegamento di due pistole stanno fuori dal tuo script. [#_next_time_17] === La prossima volta Il tuo tiro a segno chiede una cosa sola: colpisci quello, lì. L'altra convenzione da sala giochi su cui sono costruiti i giochi video chiede qualcosa di più difficile -- premi *questo*, *adesso*, e hai mezzo secondo. La diciottesima lezione costruisce un quick-time event, con una richiesta a schermo, una finestra misurata in fotogrammi e tre finali diversi a seconda che tu abbia fatto giusto, sbagliato o troppo tardi.