singe/docs/it/lessons/11-hitting-things.adoc

534 lines
21 KiB
Text

[#_lesson_11_hitting_things]
== Lezione 11: Colpire le cose
image::learn/11-hitting-things.png[La lezione finita, 480]
Il tuo gioco della decima lezione funziona. I colpi spezzano le rocce, le rocce
costano vite e le esplosioni arrivano nei punti giusti. Guardalo bene, però, e
vedrai che bara, in tutte e due le direzioni. Una roccia che scivola oltre la
punta di un'ala ti toglie una vita. Un colpo che attraversa il cielo vuoto
accanto a una roccia la spezza lo stesso.
In un gioco due cose si toccano quando lo dicono i numeri. Nessuno guarda le
immagini. Questa lezione mette d'accordo i numeri con quello che vede il
giocatore: sostituisce il controllo che hai scritto a mano con quello del
motore, rimpicciolisce i riquadri controllati perché calzino sulle cose che
contengono, e ti dà un interruttore che ti mostra quei riquadri. L'immagine qui
sopra è il gioco con quell'interruttore acceso.
[#_where_you_are]
=== Dove sei arrivato
Il gioco chiede «queste due cose si toccano?» esattamente in due punti, e tutti
e due usano `overlapping`, la funzione che hai scritto nella nona lezione. Uno è
in `updateRocks`, dove una roccia incontra la nave:
[source,lua]
----
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
----
L'altro è in `updateShots`, dove un colpo incontra una roccia:
[source,lua]
----
if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
----
Tutto ciò che c'è in questa lezione cambia quelle due righe e aggiunge qualche
piccola funzione che le alimenti. Il resto del gioco non cambia. Lo script
completo e finito è in `learn/11-hitting-things.singe`, se ti perdi.
[#_read_it_once_more]
=== Rileggila ancora una volta
Questa parte l'ha già fatta la nona lezione: `overlapping` è la collisione tra
rettangoli scritta a mano. Rileggila ancora una volta prima di buttarla via,
perché è l'idea tutta intera:
[source,lua]
----
function overlapping(ax, ay, aw, ah, bx, by, bw, bh)
if ax + aw <= bx then
return false
end
if bx + bw <= ax then
return false
end
if ay + ah <= by then
return false
end
if by + bh <= ay then
return false
end
return true
end
----
Alla domanda «queste due cose si toccano?» è difficile rispondere direttamente
e facile rispondere al contrario. Due rettangoli *non* si toccano quando uno dei
due sta tutto a sinistra dell'altro, o tutto a destra, o tutto sopra, o tutto
sotto. Non c'è un quinto modo di mancarsi. Se nessuno di questi quattro è vero,
si toccano.
Ogni `if` è uno dei quattro modi di mancarsi. `ax + aw` è il bordo destro del
primo rettangolo, quindi `ax + aw <= bx` si legge «il primo finisce prima che
cominci il secondo», che è quello che «tutto a sinistra» vuol dire in numeri.
La stessa forma altre tre volte, per destra, sopra e sotto, con `y` e le
altezze al posto di `x` e delle larghezze, e l'unica cosa che resta è `true`.
Questi sono gli otto numeri che servono a qualunque controllo di collisione: un
angolo e una dimensione per ciascuna delle due cose. Tieni a mente la forma,
perché stai per cancellarla.
[#_the_engine_already_has_it]
=== Il motore ce l'ha già
Hai incontrato `collideRects` nella settima lezione, dove acchiappava i blocchi
che cadevano in Dodge. La nona lezione ha scritto lo stesso controllo a mano
perché tu potessi vederci dentro. Ci hai guardato dentro. Cancella
`overlapping` e cambia le due righe di *Dove sei arrivato* perché chiamino
invece il motore:
[source,lua]
----
if collideRects(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
----
[source,lua]
----
if not gone and collideRects(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
----
Gli stessi otto numeri, nello stesso ordine: l'angolo e la dimensione del primo
rettangolo, poi quelli del secondo. Eseguilo, e il gioco si comporta
esattamente come prima, ed è questo il punto. L'unica differenza sta proprio
sul bordo. Il tuo `<=` diceva che due rettangoli che si incontrano lungo un
bordo non si toccano. `collideRects` dice che si toccano, e un rettangolo senza
larghezza o altezza non tocca proprio niente. Questi casi scomodi sono il punto
in cui le versioni scritte a mano sbagliano in silenzio, e sono uno dei motivi
per usare quella del motore.
`collideRects` fa parte di una famiglia di controlli di collisione, e la
famiglia è una delle parti di quello che questo libro chiama il *kit di
gioco*: una manciata di semplici funzioni per le cose che quasi ogni gioco un
tempo si scriveva da sé. Controllare se due cose si toccano è una. Salvare un
numero per la volta successiva è un'altra, ed è la dodicesima lezione.
Aspettare un po', far scorrere un numero dolcemente da un valore a un altro e
mostrare quanto è durato ogni fotogramma sono le altre. Il Riferimento dà a
ogni parte una sezione tutta sua: Collisioni, Salvataggio, Timer, Tween e
Statistiche.
I controlli di collisione sono i più semplici del mucchio: domande e basta,
senza handle da conservare e niente da preparare o liberare. E nessuno di loro
sposta qualcosa. `collideRects` ti dice che la nave e la roccia sono nello
stesso posto; che cosa farne lo decidi tu, e tutto ciò che c'è di interessante
in un gioco succede in quella decisione.
[#_the_picture_is_not_the_hit_box]
=== L'immagine non è la hitbox
La decima lezione ha promesso di spiegare perché il rettangolo attorno a una
roccia rotonda ha la forma sbagliata. Anche il rettangolo attorno alla nave è
sbagliato, e per lo stesso motivo.
Disegna su carta il rettangolo 32 per 24 della nave e colora la nave. È un
triangolo con due pinne, quindi c'è cielo vuoto in tutti e due gli angoli in
alto e tra le pinne. La roccia è rotonda, quindi c'è cielo vuoto in tutti e
quattro gli angoli del suo quadrato. Una roccia che sfiora l'angolo in alto del
rettangolo della nave non ha colpito la nave. È passata oltre l'ala, e un
giocatore che ci perde una vita dirà che il gioco ha barato -- e avrà ragione.
Quindi il riquadro che controlli non è l'immagine che disegni. È più piccolo, e
sta dentro l'immagine. Aggiungi tre numeri alle costanti in cima al tuo file:
[source,lua]
----
local SHIP_PAD_X = 6
local SHIP_PAD_Y = 5
local ROCK_PAD = 4
----
Il margine è quanto più all'interno di ogni bordo comincia la cosa vera. Sei
pixel tolti da ogni lato della nave e cinque da sopra e da sotto lasciano un
riquadro di 20 per 14 in mezzo a un'immagine di 32 per 24, che è più o meno
dove sta lo scafo. Quattro tolti da ogni lato della roccia lasciano 16 per 16,
ed è quasi tutto roccia.
Tutti e due i riquadri sono un po' più piccoli delle cose che rappresentano, ed
è voluto. Un riquadro appena troppo piccolo regala al giocatore dei passaggi per
un pelo che sembrano bravura. Un riquadro appena troppo grande gli dà dei colpi
subiti che sembrano un imbroglio. I giocatori perdonano i primi e mai i
secondi.
Invece di scrivere quei calcoli a ogni controllo, scrivili una volta per ogni
tipo di cosa:
[source,lua]
----
function rockBox(rock)
return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2
end
function shipBox()
return shipX + SHIP_PAD_X, shipY + SHIP_PAD_Y, shipWidth - SHIP_PAD_X * 2, shipHeight - SHIP_PAD_Y * 2
end
----
Le dimensioni vengono da `rockWidth`, `shipWidth` e compagnia, le misure che la
nona lezione ha preso dalle immagini, quindi nel codice di gioco non c'è ancora
una sola dimensione in pixel scritta a mano. Ridisegna la nave più grande e il
riquadro cresce con lei.
Ognuna di queste funzioni restituisce quattro numeri insieme, separati da
virgole. Una funzione può restituire tutti i valori che vuole, e il modo per
raccoglierli è un elenco di nomi a sinistra del segno `=`:
[source,lua]
----
function shipHitsRock(rock)
local sx, sy, sw, sh = shipBox()
local rx, ry, rw, rh = rockBox(rock)
return collideRects(sx, sy, sw, sh, rx, ry, rw, rh)
end
----
Ora il controllo in `updateRocks` si legge come la domanda che sta facendo:
[source,lua]
----
if shipHitsRock(rock) then
----
I calcoli del margine stanno esattamente in due posti, e i numeri che usano
sono in cima al file, quindi puoi regolare quanto il gioco sembra onesto
cambiando un numero invece di andare a caccia tra i tuoi controlli.
Metti le nuove funzioni al loro posto in ordine alfabetico, come chiedeva la
nona lezione: `rockBox` dopo `onSoundCompleted`, poi `shipBox` e
`shipHitsRock`.
[#_firing_back]
=== Rispondere al fuoco
Il lato della nave è sistemato. L'altra collisione del gioco è quella con cui
rispondi al fuoco: ogni colpo contro ogni roccia, in `updateShots`. Ha lo
stesso problema che aveva la nave, in formato ridotto. Un colpo è un
rettangolo di 4 per 10 controllato contro l'intero quadrato di 24 per 24 della
roccia, quindi un colpo che passa soltanto per l'angolo vuoto accanto a una
roccia la spezza lo stesso.
Potresti dare anche al colpo un riquadro con il suo margine. Prima però guarda
i numeri. Togli un margine a un colpo largo quattro pixel e non resta quasi
niente, e controllare un intero rettangolo per scoprire se un filo di luce ha
toccato qualcosa è più macchinario di quanto la domanda meriti.
[#_a_shot_is_almost_a_point]
=== Un colpo è quasi un punto
Controlla invece la punta. `collidePointRect` chiede se un punto cade dentro un
rettangolo -- due numeri per il punto, poi l'angolo e la dimensione del
rettangolo:
[source,lua]
----
function shotHitsRock(shot, rock)
local rx, ry, rw, rh = rockBox(rock)
return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh)
end
----
`shot.y` è la cima del colpo e `shot.x + shotWidth / 2` è a metà della sua
larghezza, quindi il punto controllato è il centro della punta del colpo. È lì
che un giocatore pensa che sia un colpo. Dalla parte della roccia, la domanda
usa lo stesso `rockBox` che usa la nave, quindi una roccia ha la stessa
dimensione qualunque cosa la colpisca.
Va dopo `shipHitsRock`, e il controllo in `updateShots` diventa:
[source,lua]
----
if not gone and shotHitsRock(shot, rock) then
----
[#_deciding_what_happened]
=== Decidere che cosa è successo
Nessuno dei controlli di collisione fa niente. Rispondono, e decide il tuo
codice. Ecco i due punti che decidono, con dentro le loro nuove domande. Prima
`updateRocks`:
[source,lua]
----
if shipHitsRock(rock) then
local channel = soundPlay(boomClip)
newRock(rock)
lives = lives - 1
if lives <= 0 then
dying = true
deathChannel = channel
if channel < 0 then
endGame()
end
end
end
----
E il ciclo dentro `updateShots`:
[source,lua]
----
for _, rock in ipairs(rocks) do
if not gone and shotHitsRock(shot, rock) then
newRock(rock)
score = score + HIT_SCORE
gone = true
soundPlay(boomClip, 0, 45)
end
end
----
Tutto quello che sta sotto le domande è esattamente ciò che ci ha lasciato la
decima lezione. Una roccia che colpisce la nave fa un botto, torna in cima e
costa una vita. Una roccia centrata da un colpo fa un botto, torna in cima e
dà punti. Se ne va anche il colpo, e il `not gone` impedisce a un colpo solo di
spezzare due rocce. Cambia una qualunque di queste cose e cambi il gioco, senza
toccare un solo controllo di collisione, ed è per questo che le domande e le
risposte stanno separate.
Eseguilo. Passa vicinissimo a una roccia e lasciale sfiorare una pinna: la vita
resta tua. Spara appena accanto a una roccia e il colpo tira dritto. Spara nel
mezzo di una roccia e si spezza, come sempre.
[#_seeing_the_boxes]
=== Vedere i riquadri
Le hitbox sono invisibili, e questo rende invisibile anche un errore al loro
interno. Disegnale mentre ci lavori. Un'altra costante in cima:
[source,lua]
----
local SHOW_BOXES = false
----
Altre due funzioni, che vanno proprio in cima all'ordine alfabetico:
[source,lua]
----
function drawBox(x, y, width, height)
overlayBox(x, y, x + width - 1, y + height - 1)
end
function drawBoxes()
colorForeground(0, 255, 0)
drawBox(shipBox())
for _, rock in ipairs(rocks) do
drawBox(rockBox(rock))
end
end
----
E in `onOverlayUpdate`, dopo che è stata disegnata la nave e prima che ci vada
sopra il punteggio:
[source,lua]
----
spriteDraw(shipSprite, shipX, shipY)
if SHOW_BOXES then
drawBoxes()
end
drawHud()
----
Cambia `SHOW_BOXES` in `true` e sopra la grafica compaiono dei contorni verdi,
come nell'immagine all'inizio di questa lezione. Rimettilo com'era quando hai
finito. Un interruttore come questo, una parola in cima a un file che accende
un'immagine di quello che il programma crede, vale più di tutte le occhiate del
mondo.
[#_what_just_happened_7]
=== Cos'è appena successo
[source,lua]
----
function rockBox(rock)
return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2
end
----
Quattro valori da un solo `return`, separati da virgole. Lua ti restituisce
volentieri tutti quelli che vuoi, e chi chiama decide quanti tenerne. Quando
una chiamata come questa è l'*ultima* cosa dentro le parentesi di un'altra
chiamata, passano tutti e quattro, ed è per questo che `drawBox(rockBox(rock))`
funziona e dà a `drawBox` i suoi quattro argomenti. In qualunque altro punto di
un elenco di argomenti sopravvive solo il primo valore. Quella regola morde
esattamente una volta, e morde alla fine di questa lezione.
[source,lua]
----
return collideRects(sx, sy, sw, sh, rx, ry, rw, rh)
----
`collideRects` vuole otto numeri: angolo e dimensione, angolo e dimensione.
Restituisce `true` o `false` e non cambia niente. Siccome la risposta esce
dritta dalla funzione con `return`, `shipHitsRock(rock)` si legge come una
domanda dovunque venga usata.
[source,lua]
----
return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh)
----
`collidePointRect` ne vuole sei: un punto, poi un rettangolo. Il bordo conta
come interno, quindi un colpo esattamente sul confine colpisce. Usala ogni
volta che uno dei due lati della domanda è abbastanza piccolo da rendere
irrilevante la sua dimensione.
[source,lua]
----
colorForeground(0, 255, 0)
overlayBox(x, y, x + width - 1, y + height - 1)
----
Vengono tutte e due dalla settima lezione. `overlayBox` prende due *angoli*,
non un angolo e una dimensione -- l'unico punto di questa lezione in cui i
numeri cambiano forma -- e il `- 1` c'è perché entrambi gli angoli sono
inclusi. `colorForeground` è un interruttore che resta dove l'hai messo, quindi
tutto ciò che disegna `drawBoxes` viene verde. Gli sprite e `overlayPrint` lo
ignorano, quindi il resto del gioco ha lo stesso aspetto di prima.
[#_the_rest_of_the_family]
=== Il resto della famiglia
`collideRects` e `collidePointRect` bastano per la maggior parte dei giochi 2D.
Le altre ci sono per le forme che i rettangoli descrivono male, e ognuna è lo
stesso tipo di domanda semplice:
* `collideCircles` e `collidePointCircle`, per qualunque cosa rotonda. Un
cerchio è un centro e un raggio.
* `collideRectCircle`, per una cosa rotonda contro una quadrata.
* `collidePointPolygon`, per una forma con angoli tutti suoi -- un drago, un
continente, un pulsante dalla forma strana. Le passi un elenco piatto di
numeri `x`, `y`, `x`, `y`.
* `collideSegments`, per due segmenti che si incrociano.
A essere onesti, la tua roccia è rotonda, e i controlli con i cerchi le calzano
meglio di qualunque riquadro. Il margine è la risposta economica, e basta per
la maggior parte dei giochi, ed è per questo che viene prima. La risposta
esatta è una delle cose da provare qui sotto.
`collideSegments` conta più di quanto sembri. Il tuo colpo si muove di sei
pixel per fotogramma e le rocce hanno sedici pixel di hitbox, quindi un colpo
non può superarne una senza essere controllato mentre ci sta dentro. Rendi il
colpo abbastanza veloce -- quaranta pixel per fotogramma -- e salta da sotto
una roccia a sopra, senza mai trovarsi nello stesso posto della roccia, e non
colpisce mai niente. La cura è tracciare la linea da dove era il colpo a dove
si trova adesso, e chiedere a `collideSegments` se quella linea attraversa un
bordo. Alle cose lente non serve.
In Singe c'è una sola callback di collisione, `onCollision`, e non è per
questo. Appartiene al motore fisico 3D: corpi con una massa, che cadono e
rimbalzano uno contro l'altro in tre dimensioni, e ti dicono dove si sono
toccati e con quanta forza. La incontrerai nella ventiquattresima lezione.
Niente sull'overlay si segnala da solo, e niente ne ha bisogno. Le tue rocce
sono in un elenco, sai quante sono, e chiedere costa poco.
E quando vuoi sapere non solo *se* due cose si sono incontrate ma dove dovrebbe
finire quella che si muove -- scivolando lungo un muro invece di fermarsi di
colpo dentro -- le chiamate collide di proposito non rispondono. Per
esattamente quel lavoro con Singe viene fornita una libreria chiamata `bump`, e
la sezione Librerie incluse del Riferimento spiega come raggiungerla.
[#_try_it_11]
=== Prova tu
. *Togli il margine.* Metti `ROCK_PAD` a `0` e gioca per un minuto. Poi mettilo
a `8` e gioca di nuovo. In uno dei due casi sembra che il gioco ti stia
mentendo.
. *Guarda che cosa stai controllando.* Metti `SHOW_BOXES` a `true` e osserva i
contorni verdi mentre una roccia passa accanto alla nave. Ora metti
`SHIP_PAD_X` a `16`, gioca, e nota che niente riesce più a colpirti. Scopri
perché dai calcoli in `shipBox`.
. *Rendi il colpo troppo veloce.* Metti `SHOT_SPEED` a `40`. Conta quante rocce
colpisci su dieci. Prima di rimetterlo com'era, ricava dai numeri perché
succede.
. *Usa tutto il colpo.* Riscrivi `shotHitsRock` in modo che usi `collideRects`
con il rettangolo intero del colpo invece di `collidePointRect` con la sua
punta. Provale tutte e due e decidi quale preferisci; non c'è una risposta
giusta, ed è una cosa che vale la pena sapere.
. *Rendi rotonda la roccia.* Dai alla roccia un cerchio invece di un riquadro:
il suo centro è a `rock.x + rockWidth / 2` in orizzontale e a
`rock.y + rockHeight / 2` in verticale, e un raggio di `10` va più o meno
bene. Usa `collideRectCircle` per la nave e `collidePointCircle` per il
colpo, e disegna il cerchio in `drawBoxes` con `overlayCircle`, che prende un
centro e un raggio.
[#_break_it_on_purpose_10]
=== Rompilo apposta
I due riquadri sono già quattro valori ciascuno, quindi viene la tentazione di
passarli direttamente e saltare gli otto nomi locali:
[source,lua]
----
function shipHitsRock(rock)
return collideRects(shipBox(), rockBox(rock))
end
----
Sembra giusto ed è sbagliato. Eseguilo e il gioco si ferma prima di disegnare
qualunque cosa:
----
186:collideRects: Expected 8 argument(s), got 5.
----
La riga è quella che hai appena scritto, quindi la tua potrebbe non
essere la 186. Cinque, però. `rockBox(rock)` è l'ultima cosa tra le parentesi,
quindi sono entrati tutti e quattro i suoi valori. `shipBox()` no, quindi è stato
ridotto a uno. Uno più quattro fa cinque, e `collideRects` non tira a
indovinare gli altri tre.
Questo errore viene dal motore e non da Lua, quindi ha la stessa forma di
quelli della nona e della decima lezione: il numero davanti è la riga, poi il
nome della funzione che si è rifiutata, poi la lamentela. Rimetti gli otto nomi
locali.
[#_what_you_learned_11]
=== Cosa hai imparato
* La collisione è aritmetica sui rettangoli, e puoi scriverla da solo.
* Due rettangoli si mancano esattamente in quattro modi; se nessuno di questi è
vero, si colpiscono.
* `collideRects` prende un angolo e una dimensione per ciascuno di due
rettangoli e risponde `true` o `false`. Due bordi che si toccano appena
contano come contatto.
* I controlli di collisione fanno parte del kit di gioco: semplici funzioni,
senza niente da preparare. Salvataggio, Timer, Tween e Statistiche sono il
resto.
* `collidePointRect` chiede se un punto sta dentro un rettangolo, che è la
domanda giusta per qualunque cosa piccola.
* Il riquadro che controlli è più piccolo dell'immagine che disegni, altrimenti
il gioco sembra ingiusto.
* Una funzione può restituire più valori, e solo l'ultima chiamata in un elenco
di argomenti li passa tutti.
* I controlli di collisione rispondono e basta. Quello che succede dopo è il
tuo codice, e tenere separate le due cose ti permette di cambiarne una sola.
* Disegnare le hitbox sullo schermo trasforma un bug invisibile in uno visibile.
* `onCollision` è per i corpi della fisica 3D, non per gli sprite sull'overlay.
[#_next_time_11]
=== La prossima volta
Il gioco conta il tuo punteggio e ti toglie le vite, e nel momento in cui esci
il punteggio migliore che qualcuno abbia mai fatto è sparito. Comincia nel
mezzo dell'azione nell'istante in cui si carica. E tiene traccia di quello che
sta facendo con due variabili vero-o-falso, `over` e `dying`, che è esattamente
la trappola da cui ti metteva in guardia la settima lezione. La dodicesima
lezione gli dà una schermata del titolo, una variabile sola al posto di quelle
due e un punteggio record che domani c'è ancora.