Esplosioni autunnali
Dato che la lezione scorsa non è stata certo una passeggiata, questa volta ci divertiamo. La volta scorsa abbiamo introdotto le classi e abbiamo visto come possano rappresentare oggetti concreti di cui il computer ignorava l’esistenza, tipo gatti e particelle. Ma siccome questa sarà una lezione relativamente leggera dal punto di vista teorico, ho pensato che fosse un buon momento per calare il carico da undici, di quelli che si vince facile con poca fatica, tranne se l’altro ha una briscola ma vabbè, non divaghiamo.
Il problema col codice che abbiamo visto fin’ora è che era abbastanza semplice, compatto e comprensibile con poco sforzo, ma quando si inizia a crescere in complessità è importante saperci calare in qualsiasi parte del codice e capire al volo cosa sta succedendo. Per questo abbiamo inventato i commenti che altro non sono se non delle annotazioni che il programmatore scrive tra le righe di codice e che vengono ignorate dal compilatore. La domanda ora è: come si fa a dire al compilatore che deve ignorare queste righe?
Java, e quindi Processing, adotta due stili di commento: la riga e il blocco. La riga inizia con un doppio slash (//
) e tutto ciò che segue fino al termine della riga viene ignorato; il blocco inizia con la sequenza /*
e termina con la sequenza */
e perciò può estendersi su più righe dato che tutto quello che è compreso tra la sequenza iniziale e quella finale verrà ignorato. Esempio:
// Questo è un commento in riga. // Nessuno mi vieta di scrivere più di una riga di commento di fila. /* Certo, a volte può essere più comodo usare un blocco per svariate ragioni, tipo estetiche. */
A questo punto possiamo dare uno sguardo al codice della lezione, che tra l’altro contiene una copiosa dose di commenti così li vedete in azione. Data la sua estensione, ho preferito caricarlo su github e riportarne qui i frammenti più notevoli. Il file pController.pde
è quello che chiamavamo il programma principale. C’è da dire che non è molto commentato ma, considerando di quanto si è ridotto e che è il codice che vediamo più spesso in assoluto, ormai dovremmo riconoscerlo ad occhi chiusi. Tranne per quel ParticleController
nella seconda riga.
Abbiamo visto che le classi rappresentano tipi di dati complessi con proprietà e metodi, ma se la storia finisse lì sarebbero uno strumento ben gramo. Con le classi possiamo anche rappresentare componenti operativi, cioè suddividere i nostri programmi in blocchi che si occupano di poche cose ciascuno in modo da semplificare la gestione del codice e dargli una struttura logica di un certo livello. In questo caso il nostro blocco operativo si occupa di tutto ciò che ha a che fare con le particelle, cioè è sua responsabilità far sì che vengano aggiornate e disegnate ad ogni fotogramma. Un oggetto di questo tipo ha una posizione privilegiata perché vede tutte le particelle che controlla attraverso il suo ArrayList particles
e quindi può operare su ciascuna vedendola nel contesto di tutte le altre. L’obiezione adesso sarebbe che tutto ciò lo facevamo anche la volta scorsa, ma la volta scorsa lo facevamo nel posto sbagliato perché se per esempio avessimo voluto due pool di particelle non ci sarebbe stato troppo difficile creare un altro ArrayList e ripetere tutte le operazioni che facevamo già sul primo, ma mettiamo che ne avessimo voluti dieci o cento? Certo, un array di ArrayList e via, ma resta il fatto che la responsabilità del programma principale non è quella di gestire le particelle ma di far sì che l’esecuzione di tutti i componenti fili liscia. Se vogliamo possiamo chiamarlo un application controller, cioè una specie di direttore d’orchestra che fa funzionare tutti i vari controller che via via andremo ad aggiungere.
Con tutti questi discorsi e queste complicazioni strutturali non stiamo né perdendo tempo, né ci stiamo arrovellando inutilmente il cervello, ma stiamo gettando le basi per uno dei fondamenti dell’ingegneria del software: i design pattern. Niente paura, non ho intenzione di affrontarli in questa lezione dato che è un argomento talmente vasto che è impossibile da coprire in così poco spazio: lo affronteremo un po’ alla volta, man mano che ci capiterà di usare i diversi pattern. Una cosa tuttavia mi preme ricordare: i design pattern sono a buon titolo uno dei fondamenti dell’ingegneria del software, senza di loro difficilmente saremmo in grado di scrivere buoni programmi di elevata complessità mantenendo una certa dose di sanità mentale e quindi non fate l’errore di bollarli come teoria noiosa perché non si può mai sapere quando un design pattern può risparmiarci ore di lavoro e notevoli mal di testa.
Tornando alle particelle, il ParticleController di questa lezione ha una novità, cioè il suo costruttore accetta un parametro. Ok, non è una grande novità, il costruttore è essenzialmente una funzione e quindi sembrava abbastanza scontato che potesse avere parametri. In questo caso prende la quantità di particelle da generare e poi fa più o meno tutte cose che avevamo già visto la scorsa volta. Prima di passare alla parte interessante, facciamo un salto in fondo alla classe dove troviamo il metodo draw()
. Il commento dice già tutto: la funzione meno sorprendente di tutta la lezione, se abbiamo seguito tutte le precedenti.
A questo punto facciamo una piccola digressione. La classe Particle che abbiamo usato la volta scorsa funzionava bene ma, per svariati motivi, rendeva più complicate le operazioni che volevo far svolgere al ParticleController e quindi l’ho un po’ modificata. In particolare, velocità e accelerazione sono diventate vettori, e ho aggiunto la proprietà decay
che simula una specie di attrito per cui la velocità con cui la particella si muove andrà riducendosi progressivamente. Vorrei dire che il contenuto della funzione update()
rasenta la banalità ma non sarebbe del tutto vero:
void update() { velocity.add(acceleration); float maxVelocity = radius + 0.0025f; float speed = velocity.mag()*velocity.mag() + 0.1; if(speed > maxVelocity*maxVelocity) { velocity.normalize(); velocity.mult(maxVelocity); } position.add(velocity); velocity.mult(decay); acceleration.set(new PVector(0, 0)); }
alcune operazioni non sono del tutto intuitive ma sono frutto di un pomeriggio di prove ed errori, nonché una certa dose di copiancolla, quindi non vi crucciate troppo se non capite perché l’accelerazione viene sparata a zero dopo un solo ciclo. Il motivo c’è ma non ho trovato un modo più chiaro per esprimerlo, magari se qualcuno di voi dotato di buona volontà volesse darmi una mano sarà il benvenuto. Comunque.
Tornando al ParticleController, il suo metodo update()
contiene una chicca interessante che ci servirà più avanti quando ci addentreremo negli oscuri reami degli algoritmi. Restando nelle particelle, la volta scorsa non facevamo che scorrere l’ArrayList che le conteneva e chiamare la loro update()
su ciascuna, e anche questa volta non facciamo diversamente (righe 44 e 45). Il fatto è che il risultato dell’altra volta era abbastanza noioso e quindi ho pensato di aggiungere un po’ di claustrofobia alle particelle, cioè ho fatto in modo che si tengano ad una certa distanza le une dalle altre. Il problema è che per ogni particella che esaminiamo col primo ciclo for
, dovremmo in teoria esaminare tutte le altre particelle nel pool con un altro ciclo for
, e questo significa che se abbiamo dieci particelle, il secondo ciclo dovrà passarne altre dieci, quindi eseguendo cento iterazioni in totale, che non sembra tanto finché non immaginiamo di avere cento particelle, il che totalizza diecimila iterazioni. Il fatto è però che la spinta repulsiva tra le particelle, così come l’ho implementata, è un’operazione simmetrica, quindi è la stessa cosa sia che la eseguiamo tra la particella i = 0
e la particella j = 8
, sia che la eseguiamo tra la particella i = 8
e la particella j = 0
. Quindi il trucco per non fare operazioni inutili è non selezionare coppie che abbiamo già esaminato, cioè se stiamo esaminando la particella i
ci basterà cominciare ad esaminare la seconda particella j
partendo da quella dopo i
e fino alla fine dell’array, il che tra l’altro ha il pregio di non considerare la coppia formata dalla stessa particella presa due volte. Un esempio ci chiarirà le idee, immaginando di avere un totale di cinque particelle elenchiamo le coppie (i, j)
:
(0, 1) (0, 2) (0, 3) (0, 4) (1, 2) (1, 3) (1, 4) (2, 3) (2, 4) (3, 4)
Abbiamo effettivamente risparmiato più di metà delle iterazioni! Mal di testa? Niente paura, per questa volta la smetto qui, anche perché il resto del codice è composto da conti che troviamo in qualsiasi libro di fisica, quindi non è molto interessante, e poi con tutti quei commenti che ho inserito non può essere troppo difficile capire cosa succede, no? Per rendere le cose ancora un po’ più interessanti, ho inserito una specie di centro di gravità al centro della finestra: se provate ad eseguire il tutto l’effetto è notevole ma lasciatelo andare per un paio di minuti per vedere tutta l’evoluzione della cosa. I più acuti tra voi avranno notato la riga 51 in cui chiamo un metodo della classe PVector ma usando il nome della classe invece di un oggetto concreto. Esistono dei metodi speciali, di cui parleremo più avanti, che possono essere chiamati senza prima istanziare un oggetto, e in questo caso mi veniva più comodo e comprensibile fare la sottrazione tra due vettori in questo modo invece che prendere il primo vettore e sottrargli il secondo. Ci torneremo.
Un’ultima nota: se guardate attentamente i commenti che ho messo prima delle definizioni delle classi e dei loro membri, hanno una forma particolare: iniziano con la sequenza /**
e vanno subito a capo (gli asterischi all’inizio delle righe successive sono puramente estetici). Esistono programmi che ignorano il codice e considerano i commenti, e il formato che ho usato in questo caso è uno dei molteplici accettati da Doxygen che è un software che analizza il codice, estrae i commenti, li associa alle varie definizioni e produce automaticamente la documentazione ai nostri programmi. Certo, il contenuto della documentazione sta a noi, e documentare le classi spesso non è sufficiente ma è comunque un notevole passo avanti rispetto al nulla. Non entrerò nel dettaglio di Doxygen perché Internet è piena di esempi esaustivi. Quello che mi interessa farvi capire è l’importanza cruciale che i commenti possono avere, in tutti i momenti dello sviluppo, e una documentazione di questo tipo aiuta non solo noi stessi a ritrovarci nel codice dopo molto tempo che non lo guardiamo, ma anche altri che vi si addentrano per la prima volta o che volessero riutilizzarlo per i loro scopi — come nel caso di una libreria o di una classe particolarmente interessante e ben sviluppata.
Con questa lezione non abbiamo visto molto codice nuovo ma introdurre il ParticleController ci ha permesso di avere una prospettiva più ampia sui nostri oggetti e su quello che possiamo farci, e da qui in avanti sarà quasi solo questione di fantasia, il che ci porterà a porci quel genere di problemi di cui parlavo nella prima lezione, quelli che prima bisogna risolvere con la testa e poi col codice.