E.6 - Generatività

Propagaciones

Obiettivo

Far generare un'animazione continua definendo il comportamento e le modalità di visualizzazione di qualche centinaio di agenti autonomi.

Modalità

Creare uno sketch in cui siano previste le seguenti operazioni:

La risoluzione del canvas dovrà adattarsi a quella della finestra in cui viene eseguito lo sketch. Per l'adattamento dovranno essere usati le variabili windowWidth e windowHeight, il gestore dell'evento windowResized() e l'istruzione resizeCanvas(). Il gestore dell'evento windowResized() non dovrà essere tolto dallo sketch.

Il codice dev'essere strutturato in questo modo:

    // DEFINIZIONE CLASSE/I
    class Agente {
        constructor() {  
            // Imposta le proprietà iniziali 
        }
        display() {                
            // Disegna l'agente 
        }
        update() {                 
            // Aggiorna le proprietà 
        }
    }

    // DATI GLOBALI
    let agenti = [];      // array degli agenti
    // Altre variabili globali

    // INIZIALIZZAZIONI
    function setup() {
        createCanvas( windowWidth, windowHeight );
        // Inizializzazioni variabili globali
        restart();
    }
    function windowResized() {
        resizeCanvas( windowWidth, windowHeight );
        restart();
    }
    function restart() {
        // Modifica variabili legate a dimensione canvas
    }

    // DISEGNO FOTOGRAMMA
    function draw() {
        // Eventuali controlli e creazione di altri agenti
        // Aggiornamento + eliminazione o disegno degli agenti
        for (let i = agenti.length-1; i >= 0; --i) {  // dall'ultimo al primo...
            agenti[i].update();
            if (condizione eliminazione) {  // se agente da eliminare...
                agenti.splice(i, 1);        // elimina
            } else {                        // altrimenti...  
                agenti[i].display();        // disegna
            }
        }
    }

Sul Web Editor di p5.js si può trovare uno sketch di base da cui partire.

Consigli

La parte su cui conviene iniziare a ragionare è il comportamento degli agenti determinato dal codice del metodo update() della classe Agente. In relazione al comportamento può poi essere necessario definire nuove proprietà nel metodo constructor(). Solo alla fine conviene scegliere le istruzioni grafiche più adatte a valorizzare le possibilità grafiche dell'agente intervenendo nel metodo display(). Ovviamente possono esserci aggiustamenti successivi alle parti di codice già definite ma è meglio partire dai metodi della classe.
Le modifiche al setup(), all'eventuale restart() e al draw() sono le meno importanti per le finalità di questa esercitazione e servono solo a gestire la creazione, l'aggiornamento e la visualizzazione di tutti gli "agenti".

Aggiunta di una proprietà

Può essere utile aggiungere una proprietà alla classe per memorizzare una caratteristica geometrica o cromatica con valori specifici per ogni istanza.
Si ricorda che per farlo è necessario:

Ad esempio, può essere aggiunto il colore di riempimento dell'agente:

class Agente {
    constructor() {  
        ...
        this.rosso = random(255);            // definizione
    }
    display() {                
        fill( this.rosso, 128, 64 );         // uso
        circle( this.pos.x, this.pos.y, 5 );
    }
    update() {
        ...                 
        this.rosso = (this.rosso + 1) % 255; // aggiornamento interno
    }
}

La proprietà di ogni singola istanza può essere modificata dall'esterno della classe, solitamente nel draw():

agenti[i].rosso = random(64,192); // aggiornamento esterno 

Visualizzazione dell'agente

Il metodo display() può limitarsi a contenere una sola istruzione di disegno, soprattutto se gli attributi grafici (contorni e colori) sono uguali per tutte le istanze. In alcuni casi, ad esempio se gli attributi grafici sono proprietà della classe, può essere utile inserire sempre almeno le istruzioni fill() e stroke(). In altri casi, ad esempio se devono essere fatte delle trasformazioni geometriche, conviene usare anche le istruzioni push() e pop().
Un agente costituito da un segmento di 50 pixel allineato alla direzione di spostamento potrebbe avere il metodo display() definito in questo modo:

display() {
    push();
    translate( this.pos );
    rotate( this.vel.heading() );  // allinea a direzione spostamento
    stroke( 128 );           
    line( -50, 0, 0, 0 );          // segmento da sinistra verso il centro
    pop();
}

Anche i vertici delle figure chiuse possono essere indicati con le coordinate relative al centro di rotazione:

triangle( -40, -5, 0, 0, -40, 5 );  // triangolo >+ centro

Comportamento dell'agente

Di solito il comportamento viene implementato modificando progressivamente alcune proprietà ma può essere necessario fare delle verifiche per evitare che i valori finiscano per far sparire l'agente. Per far muovere un agente facendogli compiere ogni tanto una leggera rotazione e per farlo rientrare dal lato opposto del canvas quando esce, il metodo update() potrebbe essere strutturato in questo modo:

update() {
    // MODIFICA LE PROPRIETÀ
    if (random() < 0.01) {                  // con una probabilità dell'1%...
		this.vel.rotate(random(-0.1, 0.1)); // cambia orientamento spostamento
	}
	this.pos.add(this.vel);                 // sposta l'agente

    // VERIFICA E "CORREGGI"
	if (this.pos.x < 0) {                   // se esce a sinistra...
	    this.pos.x += width;                // rientra da destra
	} else if (this.pos.x > width) {        // se esce a destra...
	    this.pos.x -= width;                // rientra da sinistra
	}
	if (this.pos.y < 0) {                   // se esce in alto...
	    this.pos.y += height;               // rientra dal basso
	} else if (this.pos.y > height) {       // se esce in basso...
	    this.pos.y -= height;               // rientra dall'alto
	}
}

Sul Web Editor si può trovare una variante "pittorica" che implementa un comportamento un po' diverso.

Nascita degli agenti

Le istanze degli agenti possono essere create tutte insieme nel setup(). Ad esempio, se se ne vogliono creare 300 posizionate a caso nel canvas, si può usare un for come questo:

for (let i=0;  i < 300;  i++) {     // per 300 volte...
    let pos = createVector(random(width),random(height)); 
    agenti[i] = new Agente( pos );  // crea un'istanza e memorizzala nell'array
}

Il riempimento del'array può essere anche progressivo e le istanze possono essere create nel draw() o in una delle funzioni di gestione degli eventi della tastiera, del mouse, ecc. Ad esempio, se si vogliono far creare le istanze mentre si tiene premuto il tasto del mouse, si può inserire un codice come questo nel draw():

if (mouseIsPressed) {
    let mousePos = createVector(mouseX, mouseY);
    agenti.unshift(new Agente(mousePos)); // aggiungi in cima all'array
}

Sempre nella variante "pittorica" si può trovare un esempio pratico di questo codice.

Morte degli agenti

L'eventuale "morte" delle istanze degli agenti è gestita dall'if del for finale nel draw(). La condizione può riguardare il numero massimo di istanze ma anche una proprietà interna alla classe. Ad esempio, potrebbe essere previsto un contatore per tenere traccia dei fotogrammi passati dalla creazione dell'istanza:

class Agente {
    constructor() {  
        ...
        this.vita = 200;  // vita iniziale, in fotogrammi
    }
    ...
    update() {
        ...                 
        this.vita--;  // riduzione vita
    }
}

Nel draw() si potrebbe quindi controllare se il contatore di ogni singola istanza si è azzerato ed è quindi necessario toglierla dall'array agenti:

function draw() {
    ...
  	// Aggiornamento + eliminazione o disegno degli agenti
  	for (let i = agenti.length-1; i >= 0; --i) {  
  	    agenti[i].update();
        if (agenti[i].vita < 0) {   // se "vita" esaurita...
            agenti.splice(i, 1);    // elimina istanza
        } else {                    // altrimenti...  
            agenti[i].display();    // disegna istanza
        }
    }
}

Ancora nella variante "pittorica" si può vedere un esempio con agenti dalla "vita" limitata.

Relazioni fra agenti

Per prevedere azioni dipendenti dalle relazioni fra gli agenti, è possibile usare due for annidati nel draw() che confrontino ogni agente con tutti gli altri. Utilizzando le proprietà pos, ad esempio, può essere calcolata la distanza fra i due agenti e con un if si possono definire le azioni da eseguire se la distanza e superiore o inferiore a un determinato valore. Le proprietà possono essere utilizzate anche per disegnare elementi esterni come, ad esempio, una linea che unisce i due agenti. Il codice di base potrebbe essere questo:

function draw() {
	// ...
    // CONFRONTA AGENTI ED ESEGUI EVENTUALI ISTRUZIONI
  	for (let a = 0; a < agenti.length-1; ++a) {           
		let agenteA = agenti[a];
	  	for (let b = a+1; b < agenti.length; ++b) {
			let agenteB = agenti[b];
            let distanza = agenteA.pos.dist( agenteB.pos );
            // modifica proprietà di agenteA e agenteB e/o
            // disegna elementi esterni agli agenti
	  	}
	}  
    for (let agente of agenti) {
        agente.display();
        agente.update();
    }
}

Sul Web Editor si può trovare uno sketch d'esempio.

Altre varianti dello sketch di base

Sempre sul Web Editor sono presenti anche tre versioni dello sketch di base che mostrano alcune tecniche utilizzabili per l'esercitazione:

variante B

variante C

variante D

Consegna

Lo sketch andrà consegnato, dopo l'aggiunta dell'anteprima (obbligatoria), seguendo le modalità indicate nel "compito" della Classroom del corso.

Prima di consegnare l'esercitazione si consiglia di verificare il caricamento dell'anteprima attraverso la pagina di verifica dello sketch e l'eventuale adattamento del canvas alla modifica delle dimensioni della finestra del browser.