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