592 lines
22 KiB
Text
592 lines
22 KiB
Text
[#_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.
|