428 lines
17 KiB
Text
428 lines
17 KiB
Text
[#_lesson_4_doing_it_again]
|
||
== Lezione 4: Ancora una volta
|
||
|
||
image::learn/04-repeating.png[La lezione finita, 480]
|
||
|
||
Hai disegnato una parola e un riquadro. A un gioco le cose servono a centinaia:
|
||
stelle, mattoni, proiettili, le righe di un menu. Scrivere cento righe non è la
|
||
risposta, e se lo fosse, dovresti scriverne altre cento nel momento in cui ne
|
||
volessi centouno.
|
||
|
||
Questa lezione è la risposta. Alla fine un campo di stelle scenderà lentamente
|
||
lungo la tua finestra, e ognuna sarà disegnata dalle stesse cinque righe di codice.
|
||
|
||
[#_start_a_new_folder_3]
|
||
=== Crea una nuova cartella
|
||
|
||
Una cartella chiamata `repeating`, dentro un file chiamato `repeating.singe`, e
|
||
`Singe -R repeating` in esecuzione in un terminale.
|
||
|
||
[#_forty_dots]
|
||
=== Quaranta punti
|
||
|
||
Scrivi questo ed eseguilo.
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
colorForeground(255, 255, 255)
|
||
|
||
for star = 1, 40 do
|
||
overlayPlot(star * 8, 100)
|
||
end
|
||
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Una fila di quaranta punti bianchi attraverso il centro della finestra.
|
||
|
||
`overlayPlot` è la chiamata per disegnare più semplice che ci sia: colora un
|
||
pixel con il colore di primo piano attuale, ed è esattamente quello che serve a
|
||
una stella. Tutto ciò che c'è di interessante qui sono le tre righe intorno.
|
||
|
||
[source,lua]
|
||
----
|
||
for star = 1, 40 do
|
||
----
|
||
|
||
Questo dice: fai quello che segue quaranta volte, e a ogni giro `star` contiene
|
||
un numero diverso -- uno la prima volta, due la seconda, quaranta l'ultima.
|
||
Entrambi gli estremi sono compresi, quindi `1, 40` sono davvero quaranta giri e
|
||
non trentanove. Questa forma è un *ciclo*, e questo in particolare è un *ciclo
|
||
for numerico*.
|
||
|
||
`star` è un nome che inventi tu, come hai inventato `frames` nella seconda
|
||
lezione, e il ciclo lo riempie per te. Non sei tu a impostarlo, e non dovresti
|
||
cambiarlo dentro il ciclo. Esiste solo tra il `for` e il suo `end`; se lo chiedi
|
||
dopo, ottieni `nil`.
|
||
|
||
Il `do` è obbligatorio ed è facile dimenticarlo. Se lo togli Lua dice
|
||
`'do' expected near`, seguito da quello che ha trovato al suo posto, che sarà la
|
||
prima cosa della riga successiva.
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPlot(star * 8, 100)
|
||
----
|
||
|
||
Il corpo. È questo che viene eseguito quaranta volte, ed è sempre la stessa riga
|
||
-- quello che cambia è `star`, quindi il primo punto cade a x 8, il secondo a 16
|
||
e l'ultimo a 320. Un ciclo serve solo se il corpo fa qualcosa con il numero del
|
||
ciclo, e la prima cosa a cui ricorrerai ogni volta è farci dei calcoli.
|
||
|
||
Il corpo è rientrato di un passo in più rispetto al `for`, per lo stesso motivo
|
||
per cui finora è stato rientrato tutto il resto.
|
||
|
||
[source,lua]
|
||
----
|
||
end
|
||
----
|
||
|
||
Chiude il ciclo. Ora hai incontrato tre cose che hanno bisogno di un `end`:
|
||
`function`, `if` e `for`. Funzionano tutte allo stesso modo, si annidano una
|
||
dentro l'altra, e il rientro è il modo in cui tieni traccia di quale `end`
|
||
appartiene a che cosa.
|
||
|
||
[#_counting_by_more_than_one]
|
||
=== Contare a passi più grandi di uno
|
||
|
||
Il ciclo conta in avanti di uno alla volta, a meno che tu non gli dica
|
||
altrimenti. Aggiungi un terzo numero e conterà invece a passi di quel numero.
|
||
|
||
[source,lua]
|
||
----
|
||
for x = 0, overlayGetWidth() - 1, 10 do
|
||
overlayPlot(x, 100)
|
||
end
|
||
----
|
||
|
||
Leggilo come *da, fino a, a passi di*. Questo va 0, 10, 20 e così via,
|
||
fermandosi sull'ultimo pixel della superficie o prima, e disegna una linea
|
||
punteggiata da un lato all'altro, qualunque sia la dimensione della finestra. È
|
||
la stessa fila di punti di prima, descritta in termini di dove vanno invece che
|
||
di quanti sono.
|
||
|
||
Il passo può essere negativo, e allora conta all'indietro.
|
||
|
||
[source,lua]
|
||
----
|
||
for countdown = 10, 1, -1 do
|
||
----
|
||
|
||
Dieci, nove, otto, giù fino a uno. Con un passo negativo il secondo numero è il
|
||
pavimento invece del soffitto, come ti aspetteresti, e sbagliare il segno dà un
|
||
ciclo che non viene mai eseguito: `for i = 10, 1 do` senza passo conta in avanti
|
||
da dieci verso uno, è già oltre la fine prima di cominciare e non fa niente.
|
||
Nessun errore, nessun risultato. Se un tuo ciclo sembra non partire mai,
|
||
controlla prima la direzione.
|
||
|
||
Il passo può anche essere una frazione, anche se ti servirà meno spesso di
|
||
quanto pensi.
|
||
|
||
[#_one_loop_inside_another]
|
||
=== Un ciclo dentro un altro
|
||
|
||
Il corpo di un ciclo è codice normale, e il codice normale può contenere un
|
||
ciclo.
|
||
|
||
[source,lua]
|
||
----
|
||
for row = 0, overlayGetHeight() - 1, 20 do
|
||
for column = 0, overlayGetWidth() - 1, 20 do
|
||
overlayPlot(column, row)
|
||
end
|
||
end
|
||
----
|
||
|
||
Questo disegna una griglia di punti su tutta la finestra. Mettilo nella tua
|
||
`onOverlayUpdate` al posto della fila e guardalo, perché conviene chiarirti in
|
||
testa adesso come viene eseguito, invece che nell'undicesima lezione, quando
|
||
qualcosa dipenderà da questo.
|
||
|
||
Il ciclo esterno viene eseguito una volta con `row` a 0. Lì dentro, tutto il
|
||
ciclo interno viene eseguito dall'inizio alla fine -- diciotto colonne, diciotto
|
||
punti, una riga della griglia. Solo quando il ciclo interno ha finito il ciclo
|
||
esterno passa a `row` a 20 ed esegue di nuovo tutto il ciclo interno.
|
||
|
||
Il corpo del ciclo interno viene quindi eseguito diciotto per dodici volte,
|
||
cioè duecentosedici volte, per dodici giri del ciclo esterno. Quella
|
||
moltiplicazione è il motivo per cui i cicli annidati sono il modo di riempire un
|
||
rettangolo con qualsiasi cosa, e anche il motivo per cui dovresti dare
|
||
un'occhiata ai numeri prima di annidarne tre.
|
||
|
||
Nota che la posizione di ogni punto ha bisogno di entrambe le variabili dei
|
||
cicli: `column` per l'orizzontale e `row` per il verticale. Lo schema è questo.
|
||
Se il corpo interno usa solo la variabile interna, il ciclo interno disegna la
|
||
stessa cosa più e più volte nello stesso posto, e hai scritto un modo lento per
|
||
farla una volta sola.
|
||
|
||
[#_when_you_do_not_know_how_many]
|
||
=== Quando non sai quante volte
|
||
|
||
Un ciclo `for` ha bisogno di sapere quante volte prima di cominciare. A volte
|
||
non lo sai, e allora ti serve l'altro tipo.
|
||
|
||
[source,lua]
|
||
----
|
||
x = 0
|
||
while x < overlayGetWidth() do
|
||
overlayPlot(x, 100)
|
||
x = x + 10
|
||
end
|
||
----
|
||
|
||
Un ciclo `while` controlla una domanda prima di ogni giro, esattamente la
|
||
domanda che farebbe un `if`, e continua finché la risposta è vera. Quando la
|
||
risposta è falsa si ferma e prosegue con la riga che segue `end`. Se la risposta è
|
||
falsa già la prima volta, il corpo non viene mai eseguito.
|
||
|
||
Quell'esempio disegna la stessa linea punteggiata del `for` con passo dieci, e
|
||
ci mette tre righe per fare quello che `for` faceva in una, quindi per quello
|
||
usa `for`. Ecco la differenza che decide:
|
||
|
||
* Usa `for` quando il numero di giri è noto prima di cominciare. Quaranta
|
||
stelle. Un pixel ogni dieci da un lato all'altro. Ogni riga di una griglia.
|
||
* Usa `while` quando aspetti che qualcosa diventi vero e non puoi dire in
|
||
anticipo quanto ci vorrà. Distribuisci carte finché il mazzo non finisce.
|
||
Continua a chiedere finché il giocatore non scrive qualcosa di valido. Scorri
|
||
un elenco finché non trovi quello che cercavi.
|
||
|
||
Un `while` ha tre parti e devi scriverle tutte e tre tu: preparare la variabile
|
||
prima del ciclo, controllarla nel `while` e cambiarla dentro il corpo. Il ciclo
|
||
`for` le fa tutte e tre per te, ed è per questo che è quello da scegliere quando
|
||
va bene. Dimentica la terza parte in un `while` e la domanda non smette mai di
|
||
essere vera, cosa che farai apposta alla fine di questa lezione.
|
||
|
||
[#_a_star_made_out_of_its_own_number]
|
||
=== Una stella fatta del suo numero
|
||
|
||
Ora il campo di stelle, e un problema che dà forma al resto della prima parte.
|
||
|
||
Ogni stella ha bisogno di una x e di una y tutte sue, e non hai modo di tenere
|
||
cento variabili separate -- scrivere `star1X`, `star2X` e così via fino a cento
|
||
è peggio che scrivere le cento righe che stavi evitando. Quindi non
|
||
memorizzarle. Calcolale una per una a partire dall'unica cosa che rende una
|
||
stella diversa dalle sue vicine: il suo numero.
|
||
|
||
[source,lua]
|
||
----
|
||
starX = (star * 37) % width
|
||
----
|
||
|
||
Il `%` è l'ultimo degli operatori aritmetici ed è il *resto*: quello che avanza
|
||
dopo una divisione. Quindici diviso quattro fa tre con il resto di tre, quindi
|
||
`15 % 4` fa 3. La sua proprietà utile è che il risultato non può mai essere
|
||
grande quanto il numero a destra, quindi il resto della divisione per `width` è
|
||
sempre una posizione da qualche parte sullo schermo, per quanto grande diventi
|
||
il lato sinistro.
|
||
|
||
La stella 1 va a 37, la stella 5 a 185, la stella 10 a 10 -- 370 ha fatto il
|
||
giro oltre 360 ed è ricomparso a sinistra. Trentasette è scelto perché non ha
|
||
fattori in comune con 360, ed è questo che impedisce alle stelle di disporsi in
|
||
strisce ordinate; prova invece 36 e vedrai subito il problema.
|
||
|
||
La discesa lenta è lo stesso trucco con un numero che cresce col tempo.
|
||
|
||
[source,lua]
|
||
----
|
||
drift = 0
|
||
|
||
|
||
function onOverlayUpdate()
|
||
drift = drift + 1
|
||
...
|
||
starY = (star * 61 + math.floor(drift * layer * 0.5)) % height
|
||
----
|
||
|
||
`drift` vive fuori dalla funzione e sale a ogni fotogramma, esattamente come
|
||
`frames` nella seconda lezione. Aggiungerlo alla y di ogni stella sposta in
|
||
basso tutto il campo insieme, e il `%` riporta in cima una stella che cade fuori
|
||
dal fondo, per sempre, gratis.
|
||
|
||
`math.floor` butta via la parte frazionaria di un numero e restituisce la parte
|
||
intera sotto: `math.floor(7.9)` fa 7. La seconda lezione faceva notare che Singe
|
||
lo fa per te quando disegna, ed è vero, ma farlo tu vuol dire che il numero con
|
||
cui lavori è il numero che compare sullo schermo. Questo conterà nella settima
|
||
lezione, quando due cose dovranno essere d'accordo sul fatto di toccarsi o no.
|
||
|
||
Nota che lì dentro `layer` è una seconda variabile di ciclo. Le stelle sono
|
||
disegnate da un ciclo dentro un ciclo, e quello esterno non è una riga di una
|
||
griglia ma una *distanza*: tre strati di stelle, ognuno che scende alla sua
|
||
velocità e con la sua luminosità, ed è questo che fa sembrare profondo un campo
|
||
piatto di punti.
|
||
|
||
[#_chance]
|
||
=== Il caso
|
||
|
||
Le stelle scintillano. Scintillare vuol dire una luminosità un po' diversa a
|
||
ogni fotogramma, e «un po' diversa» è `math.random`.
|
||
|
||
[source,lua]
|
||
----
|
||
shade = math.random(60, 90) + layer * 55
|
||
colorForeground(shade, shade, shade)
|
||
----
|
||
|
||
`math.random(60, 90)` restituisce un numero intero da 60 a 90, entrambi
|
||
compresi, scelto di nuovo ogni volta che la chiami. Chiamala senza nessun
|
||
argomento e ottieni invece una frazione tra 0 e 1, che è la forma da usare
|
||
quando vuoi che qualcosa succeda una volta su cinque. Non devi preparare il
|
||
generatore: lo fa Lua quando Singe parte.
|
||
|
||
Aggiungere `layer * 55` solleva gli strati vicini fuori dalla fascia scura, così
|
||
le stelle lontane restano fioche e quelle vicine sono quasi bianche. Rosso,
|
||
verde e blu uguali, come spiegava la terza lezione, danno una sfumatura di
|
||
grigio.
|
||
|
||
Siccome `shade` viene scelto di nuovo a ogni fotogramma per ogni stella, ogni
|
||
stella tremola per conto suo, e a sessanta fotogrammi al secondo sembra uno
|
||
scintillio e non un difetto.
|
||
|
||
[#_the_whole_thing_3]
|
||
=== Tutto insieme
|
||
|
||
[source,lua]
|
||
----
|
||
LAYERS = 3
|
||
STARS_PER_LAYER = 40
|
||
ACROSS_STEP = 37
|
||
DOWN_STEP = 61
|
||
LAYER_SHIFT = 13
|
||
LAYER_SPEED = 0.5
|
||
LAYER_BRIGHT = 55
|
||
|
||
width = overlayGetWidth()
|
||
height = overlayGetHeight()
|
||
|
||
drift = 0
|
||
|
||
|
||
function onOverlayUpdate()
|
||
drift = drift + 1
|
||
overlayClear()
|
||
|
||
for layer = 1, LAYERS do
|
||
for star = 1, STARS_PER_LAYER do
|
||
starX = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
||
starY = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
||
shade = math.random(60, 90) + layer * LAYER_BRIGHT
|
||
colorForeground(shade, shade, shade)
|
||
overlayPlot(starX, starY)
|
||
end
|
||
end
|
||
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Centoventi stelle, disegnate da cinque righe, e il file è più corto di quello
|
||
della terza lezione.
|
||
|
||
È anche il limite di questo approccio, e vale la pena essere onesti sul perché
|
||
mentre lo hai sotto gli occhi. Ogni stella lì dentro è una formula. Di una
|
||
stella non viene *ricordato* niente: non puoi renderne una più luminosa della
|
||
vicina e farla restare così, non puoi abbatterne una dal cielo e non puoi far
|
||
sì che il giocatore ne raccolga una, perché non c'è nessun posto dove scrivere
|
||
che le è successo qualcosa. La stella diciassette non è una cosa. È un numero
|
||
che passa per un attimo in una variabile chiamata `star`.
|
||
|
||
Nel momento in cui vuoi cento cose che si ricordino ognuna qualcosa di suo, ti
|
||
serve un posto dove mettere cento gruppi di informazioni. Quella è la sesta
|
||
lezione, e il campo di stelle è il motivo per cui esiste.
|
||
|
||
[#_try_it_4]
|
||
=== Prova tu
|
||
|
||
. *Di più e di meno.* Prova `STARS_PER_LAYER = 200`, poi `4`. Poi
|
||
`LAYERS = 8` e guarda che cosa fa `layer * LAYER_BRIGHT` a `shade` quando il
|
||
numero dello strato diventa grande. I numeri che accetta `colorForeground` si
|
||
fermano a 255.
|
||
. *Cambia la spaziatura.* Metti `ACROSS_STEP` a 36 ed eseguilo. Le stelle si
|
||
mettono in strisce, perché 36 sta in 360 esattamente dieci volte. Rimettilo a
|
||
37 e prova 71 e 90.
|
||
. *Ferma lo scintillio.* Sostituisci la riga con `math.random` con un semplice
|
||
`shade = layer * 70` e confronta. Decidi quale preferisci; non c'è una
|
||
risposta giusta, e accorgerti di avere una preferenza fa parte del lavoro.
|
||
. *Falle cadere verso l'alto.* Un carattere.
|
||
. *Disegna anche la griglia.* Rimetti il ciclo annidato della griglia di prima,
|
||
dopo le stelle, in un colore scuro. Due cicli annidati in una sola funzione,
|
||
che disegnano due cose diverse, e il secondo non disturba il primo.
|
||
|
||
[#_break_it_on_purpose_4]
|
||
=== Rompilo apposta
|
||
|
||
Finora ogni errore è stato un messaggio. Questo no, ed è proprio per questo che
|
||
dovresti incontrarlo mentre te lo aspetti.
|
||
|
||
Metti questo nella tua `onOverlayUpdate`, subito sotto `overlayClear()`,
|
||
eseguilo e leggi il paragrafo successivo prima di fare qualsiasi altra cosa.
|
||
|
||
[source,lua]
|
||
----
|
||
x = 0
|
||
while x < width do
|
||
overlayPlot(x, 100)
|
||
end
|
||
----
|
||
|
||
Manca la riga che fa avanzare `x`, quindi `x` resta a zero, quindi `x < width`
|
||
non smette mai di essere vero. Il ciclo gira per sempre, disegnando lo stesso
|
||
pixel, ed è dentro `onOverlayUpdate`, quindi `onOverlayUpdate` non ritorna mai.
|
||
Singe sta aspettando la tua funzione. Non può disegnare, non può leggere la
|
||
tastiera e non può uscire.
|
||
|
||
Quello che vedi è una finestra che smette di muoversi. Esc non fa niente. Q non
|
||
fa niente. E non viene stampato niente.
|
||
|
||
Vai nel terminale da cui hai avviato Singe e premi *Ctrl* e *C* insieme. Singe
|
||
ferma il tuo ciclo e ti dice dov'era:
|
||
|
||
----
|
||
Stopped: the script did not return, and Singe was asked to quit.
|
||
stack traceback:
|
||
repeating/repeating.singe:20: in function 'onOverlayUpdate'
|
||
----
|
||
|
||
Può dirlo due volte, a qualche secondo di distanza, prima di uscire. Una volta
|
||
basta e avanza. La riga 20 è l’`end` del tuo `while`, che è dove il ciclo stava
|
||
girando quando l'hai fermato. Togli il ciclo, salva e avvia di nuovo Singe.
|
||
|
||
Questo è un resoconto, non un errore. Niente si è lamentato finché non l'hai
|
||
chiesto, perché, per quanto riguarda il computer, non è andato storto niente.
|
||
Hai chiesto un ciclo che gira finché `x` è minore della
|
||
larghezza, ed è esattamente quello che hai ottenuto. Questo è l'unico tipo di
|
||
errore con cui i messaggi di errore non possono aiutarti, e la cura è
|
||
un'abitudine: ogni volta che scrivi un `while`, scrivi la riga che cambia la
|
||
variabile prima di scrivere qualsiasi altra cosa nel corpo.
|
||
|
||
[#_what_you_learned_4]
|
||
=== Cosa hai imparato
|
||
|
||
* `for name = first, last do ... end` esegue il suo corpo una volta per ogni
|
||
numero dal primo all'ultimo, entrambi compresi.
|
||
* Un terzo numero è il passo, e può essere negativo per contare all'indietro.
|
||
* La variabile del ciclo esiste solo dentro il ciclo, e il corpo dovrebbe
|
||
usarla.
|
||
* `for` ha bisogno di un `do`, e ogni ciclo ha bisogno di un `end`.
|
||
* Un ciclo dentro un ciclo esegue tutto il ciclo interno per ogni giro di quello
|
||
esterno, ed è così che riempi un rettangolo.
|
||
* `while question do ... end` ripete finché la domanda è vera, e qualcosa nel
|
||
corpo devi cambiarlo tu.
|
||
* Usa `for` quando sai quante volte, e `while` quando aspetti che qualcosa
|
||
diventi vero.
|
||
* `%` è il resto, e tiene dentro un intervallo un numero che cresce.
|
||
* `math.random(a, b)` dà un numero intero da a a b; `math.floor` butta via la
|
||
parte frazionaria.
|
||
* Un ciclo che non finisce mai blocca tutto il gioco senza nessun messaggio di
|
||
errore, e *Ctrl* e *C* nel terminale sono il modo per uscirne.
|
||
|
||
[#_next_time_4]
|
||
=== La prossima volta
|
||
|
||
Guarda di nuovo quella `onOverlayUpdate`. Disegna stelle, calcola posizioni,
|
||
sceglie colori, e sta cominciando a diventare una funzione che fa quattro lavori
|
||
invece di uno. Nella quinta lezione scrivi funzioni tutte tue, passi loro
|
||
argomenti come li ricevono le funzioni di Singe e ne ottieni delle risposte -- e
|
||
il campo di stelle diventa una riga che dice `drawStars()`.
|