Fondamenti di programmazione

A.S. 2018-2019

Istituto di Istruzione Superiore G. Marconi

Tecnologie informatiche

Benvenuti ragazzi!

Questo materiale riepiloga quanto detto a lezione.

Le risposte alle domande dei compiti in classe sono tutte incluse in questo materiale.

I riferimenti al libro di testo sono tra parentesi quadre, come ad esempio [pag. 1].

Il testo in questi box sono per approfondimento. La lettura di questo materiale non è strettamente necessaria per le verifiche, ma è consigliata per vostra cultura personale e in quanto sapere indispensabile per chi vuole diventare un informatico.

Buon studio e buon lavoro.

Principi di programmazione

Qual'è il nostro scopo in quanto studenti e futuri informatici?

Questa è una domanda che dobbiamo porci continuamente, dal momento in cui cominciamo a studiare informatica e per tutta la nostra carriera lavorativa.

Probabilmente la risposta più esatta a questa domanda è: risolvere problemi.

Non tutti i problemi possono essere risolti con l'informatica ed i computer, o almeno non in maniera semplice e diretta. Noi ci concentreremo su quei problemi che possiamo risolvere, attraverso gli strumenti che piano piano negli anni impareremo ad usare e padroneggiare.

È importante però chiarire fin da subito che noi, in quanto esseri umani, per poter risolvere un problema dobbiamo prima comprenderlo ed essere in grado di condividere il nostro punto di vista con le altre persone coinvolte nel progetto. Nel corso degli ultimi 50 anni in cui si è sviluppata l'ingegneria del software, si è visto statisticamente che il modo più efficace per raggiungere questi obiettivi è attraverso la narrativizzazione dei problemi, in altre parole rappresentare i problemi attraverso delle storie. Noi useremo questo approccio durante questo corso e per tutti gli anni a seguire.

Tradurre i progetti in storie è stato formalizzato in particolare da una metodologia chiamata Agile/Scrum. Per maggiori approfondimenti, potete consultare:

Lightbot

Per spiegare in maniera pratica cosa intendiamo per risolvere i problemi attraverso storie useremo un'applicazione mobile chiamata LightBot. Potete scaricare l'applicazione sul vostro smartphone attraverso i seguenti link.

Lightbot è pubblicata da code.org, un'organizzazione nonprofit basata a Seattle (USA) per facilitare l'accesso a scuola della computer science ed in particolare per incrementare la partecipazione delle donne e minoranze sotto-rappresentate.

Avvio del programma

Appena aperta l'applicazione di Lightbot, vi comparirà la seguente schermata.

Potete scegliere il bot con sembianze femminili cliccando in alto a sinistra e cambiare lingua dall'icona in alto a destra.

Già da questi particolari potete notare l'attenzione per l'inclusione e la partecipazione di tutti e tutte alla programmazione. Anche noi dobbiamo cercare di mantenere lo stesso livello di inclusione a scuola ed in tutto quello che facciamo!

Cliccando sull'immagine al centro, cominciamo il nostro viaggio nella programmazione!

Il problema e la storia

Appena avviato il primo livello, subito il nostro bot si presenta e dichiara i suoi obiettivi.

Possiamo identificare degli elementi fondamentali che caratterizzano tutte le storie:

  1. chi ha il problema?
  2. qual'è il problema?
  3. perché lo vuole risolvere?

Nel caso di Lightbot, le risposte a queste domande sono:

  1. il bot
  2. accendere tutte le mattonelle
  3. per passare al livello successivo

Per descrivere un problema si possono mettere questi tre elementi in un unica frase, creando così una storia utente. Nelle storie generalmente si preferisce mettere il verbo in prima persona, ponendo chi ha il problema come soggetto. La nostra storia utente assume la seguente forma:

Storia 1: come bot, voglio accendere tutte le mattonelle per passare al livello successivo

Bene, abbiamo creato la nostra prima storia 😎.

Ovviamente possiamo individuare vari livelli di problemi. Ad esempio ci possiamo chiedere perché il bot vuole passare di livello, oppure potremmo dire che siamo noi che vogliamo passare di livello per divertirci o per imparare a programmare.

Tutte queste considerazioni sono corrette e possono essere prese in considerazione, creando altre storie. Saremo noi programmatori, di volta in volta, a decidere su quale livello di problema lavorare e quali storie prendere in considerazione.

Vincoli di progetto

Quando vogliamo risolvere un qualsiasi problema, abbiamo dei limiti che non possiamo superare. Ad esempio potremmo avere denaro o tempo limitati, oppure poca esperienza nel campo o strumenti insufficienti. In ogni caso, per risolvere un problema dobbiamo fare i conti con la nostra realtà, qui e ora. Questi limiti in termine tecnico vengono chiamati vincoli di progetto.

I vincoli tra di loro sono generalmente collegati: ad esempio con i soldi posso comprare strumenti migliori, oppure con il tempo posso studiare per migliorare le mie capacità. L'importante, quando si affronta un problema, è avere ben chiaro quali sono i vincoli.

Continuando il tutorial del primo livello, il bot ci presenta quali sono i nostri strumenti di lavoro.

Gli strumenti di lavoro compaiono nella parte bassa dello schermo. Possiamo usare ogni strumento quante volte vogliamo, senza che questi finiscano o si consumino.

Arrivati al secondo livello, avremo la seguente schermata.

Nella parte destra dello schermo abbiamo lo spazio per inserire i nostri comandi. Nel gioco non possiamo inserire più comandi di quelli che entrano in queste caselle. Questo spazio possiamo considerarlo il nostro limite di spesa, detto anche budget, oltre il quale non possiamo andare per completare il progetto.

Ricapitolando, finora possiamo individuare due vincoli principali:

  • gli strumenti che abbiamo a disposizione
  • il budget che ci è stato assegnato

All'inizio è facile riuscire ad accendere tutte le mattonelle, ma andando avanti le cose diventano più complicate. Ad esempio, uno degli ultimi livelli si presenta così:

Facendo varie prove, si vede presto che il problema è di difficile soluzione perché abbiamo poche caselle a disposizione, in altre parole abbiamo un budget limitato, oppure perché degli strumenti che abbiamo sono insufficienti (sarebbe utile ad esempio un "salta e gira" in un unico comando!). Ma non abbiamo potere su questi vincoli.

A questo punto diventa chiaro l'ultimo vincolo da cui dipende il successo del nostro progetto:

  • le capacità del nostro team

Per riuscire ad aiutare il bot ad accendere tutte le mattonelle, dobbiamo riflettere, ingegnarci, studiare e chiedere aiuto finché non troviamo la soluzione adatta.

Risolvere un problema

Come abbiamo detto all'inizio, non tutti i problemi sono risolvibili. In particolare, quando un problema comincia ad essere difficile ci scontreremo prima o poi con uno dei tre vincoli:

  • non avere abbastanza budget
  • non avere gli strumenti adatti
  • non avere capacità sufficienti.

Come essere umani, tendiamo spesso a dare troppa importanza al primo vincolo: non sono riuscito a completare il progetto perché non avevo abbastanza tempo, o soldi, o persone nel team. La realtà è però che il budget, per sua natura, è sempre limitato: non potremo mai aumentare all'infinito i nostri soldi, o il tempo, o le persone che lavorano con noi.

Gli altri due vincoli possiamo aumentarli molto di più: trovare strumenti migliori, studiare, aggiornarsi. La storia della tecnologia in fondo è proprio questa: l'essere umano, mantenendo più o meno costante il budget, è riuscito a risolvere straordinari problemi migliorando continuamente gli strumenti e le proprie capacità: procurarsi il cibo, condividere informazioni, arrivare sempre più lontano. Il nostro percorso di studi si concentrerà su questo: trovare gli strumenti più adatti e migliorare le nostre capacità per risolvere i problemi che dovremo affrontare.

Processing

Passiamo ora ad un vero ambiente di programmazione che possiamo usare per risolvere problemi e realizzare progetti di vario tipo: Processing.

Da Wikipedia: "Processing è un linguaggio di programmazione che consente di sviluppare diverse applicazioni come giochi, animazioni, contenuti interattivi e opere d'arte generativa."

Per avere un'idea di cosa può essere realizzato con Processing, potete visitare il tutorial ufficiale che contiene video e un editor online.

Nel caso di Processing, gli strumenti che abbiamo sono le funzioni predefinite, i costrutti del linguaggio ed i paradigmi di programmazione che mette a disposizione.

La documentazione: un sito da tenere sempre a portata di mano

  • Come si disegna un quadrato?
  • Come si disegna un cerchio?
  • Come si disegna un triangolo?
  • Quali parametri devo mettere?
  • È possibile disegnare delle linee?
  • Si può generare un numero casuale?
  • Si può caricare un'immagine? Quale formato devo usare?

La risposta a queste e moltissime altre domande è sempre la stessa: consultare la documentazione, detta anche "Language Reference". Questo sito che descrive nel dettaglio i particolari sull'uso di tutte le caratteristiche del linguaggio. È fondamentale che prendiate confidenza con questo sito, non fatevi spaventare dall'inglese: le parole usate sono più o meno sempre le stesse e la costruzione delle frasi molto semplice. Vedrete che farete pratica in poco tempo, e questa conoscenza vi tornerà utile per tutto il resto della vostra vita.

Quando si impara un nuovo linguaggio, la prima cosa che un buon programmatore deve avere sempre a portata di mano e con cui deve prendere confidenza è il language reference.

Per esercizio, provate a trovare nel language reference di Processing la risposta alle domande all'inizio di questo paragrafo.

Scelta del problema: Cappucetto Rosso

cappuccetto-rosso Come abbiamo detto, tutto comincia da un problema. Per poter imparare ad usare Processing, dobbiamo prima trovare un nostro problema da risolvere. Per questo corso useremo il racconto di Cappuccetto Rosso come ispirazione.

In questa favola, tutto comincia con Cappuccetto Rosso che deve andare a consegnare delle focacce alla nonna malata. Possiamo così scrivere la nostra prima storia utente, come abbiamo visto nel capitolo precedente:

Storia 1: come Cappuccetto Rosso, voglio raggiungere la casa della nonna al di là del bosco per portarle delle focaccine, perché è malata e non può uscire da sola.

Cominciamo ora a realizzare passo dopo passo la nostra applicazione, partendo da zero ed arrivando a creare un programma che realizzi la storia appena descritta.

Il progetto completo di Cappuccetto Rosso svolto in classe lo trovate qui: https://github.com/wbigger/cappuccettorosso

Design

Il passo successivo alla storia è disegnare come dovrà apparire la nostra applicazione. Possiamo fare un disegno su carta o usando un programma di disegno (es. Gimp, Photoshop), ma la cosa importante è che sia chiaro come deve apparire visivamente l'applicazione e che cosa deve fare. Nel nostro caso, possiamo rappresentare l'applicazione con il seguente schema.

design

Immagini e forme

Analizziamo gli elementi della nostra storia che dovremo andare a rappresentare sul nostro schermo:

  • Cappuccetto Rosso
  • la casa della nonna

Come rappresentiamo questi elementi? Abbiamo fondamentalmente due possibilità:

  • attraverso un'immagine (in inglese image), caricate ad esempio da un file .png o .jpg (in termine tecnico sono immagini rasterizzate)
  • attraverso forme (in inglese shape), come ad esempio un insieme di ellissi, rettangoli e triangoli (in termine tecnico sono immagini vettoriali)

Possiamo scegliere sia l'una che l'altra strada. In generale, una volta presa una scelta, meglio rimanere su quella strada e rendere tutto coerente, mischiare immagini rasterizzate e forme e mantenere un aspetto gradevole può essere molto difficile.

Per il nostro progetto, scegliamo di usare le forme, perché in questo momento sono più semplici da creare e manipolare, e in futuro possiamo usare facilmente delle forme tridimensionali.

Andando a consultare la documentazione per vedere come si disegnano le forme in Processing. Troviamo che quello di cui abbiamo bisogno si chiama PShape (abbreviazione di Processing Shape). Fortunatamente Processing mette a disposizione un tutorial completo per il suo utilizzo.

Per rappresentare immagini si usa invece il tipo PImage

Variabili

Per rappresentare Cappuccetto Rosso, abbiamo bisogno di riservare un pezzettino della nostra memoria RAM che conterrà tutte le informazioni necessarie per disegnarla, come ad esempio dimensione, colore, etc. per riservare un'area di memoria RAM di questo genere ci serve una variabile.

Apriamo l'IDE di Processing, e salviamo il progetto vuoto che ci si presenta con il nome cappuccettorosso.

Nella prima riga, creiamo la nostra prima variabile PShape.

variable-declaration

Analizziamo nel dettaglio quello che abbiamo appena scritto:

variable-declaration

La creazione di una variabile in termine tecnico si chiama dichiarazione. La dichiarazione ha due componenti:

  • il tipo della variabile, che determina la quantità di spazio occupata in memoria e le caratteristiche della variabile
  • l'identificativo, che è il nome con cui nel resto del programma possiamo riferirci alla variabile.

È di estrema importanza che l'identificativo sia chiaro ed autoesplicativo: evitate di usare nomi come a, b o simili e preferite i nomi che hanno un senso all'interno della storia in cui vi trovate.

La favola di Cappuccetto Rosso non sarebbe stata la stessa se la bambina si fosse chiamata a o stivaletti blu!

Appena dichiarata, la variabile non ha alcun valore significativo. Spesso usare una variabile solo dichiarata porta ad un errore in esecuzione con relativo crash dell'applicazione. Dopo aver dichiarato una variabile bisogna quindi dargli un valore, questa operazione si chiama assegnazione.

Assegniamo quindi il valore della nostra variabile nella funzione setup() del nostro programma. La mettiamo in setup perché, una volta assegnata la forma, questa non cambierà per tutto il resto del programma.

variable-assignment

Come vediamo, l'assegnazione si fa usando il segno = e mettendo a sinistra l'identificativo della nostra variabile e a destra il valore che vogliamo assegnare. In questo caso facciamo creare la forma alla funzione createShape(), ed in particolare ci facciamo creare un rettangolo che ha inizialmente posizione 0,0, altezza 30 e larghezza 30.

Procedendo in maniera simile per la casa, otteniamo il seguente codice:

PShape cappuccetto;
PShape house;

void setup() {
  fullScreen(); // usa tutto lo schermo
  cappuccetto = createShape(RECT, 0, 0, 30, 30);
  house = createShape(RECT, 0, 0, 100, 100);
}

Ora, nella funzione draw(), vogliamo disegnare cappuccetto rosso a sinistra e la casetta della nonna a destra. Sempre consultando la documentazione, scopriamo che per disegnare una forma possiamo usare la funzione shape(). Esistono diversi modi di usare shape, a noi fa comodo la versione shape(shape, x, y), in cui possiamo specificare le coordinate x,y in cui andremo a disegnare la forma.

void draw() {
  background(#00FF00); // siamo nella foresta, lo sfondo è verde

  //disegniamo la casa a destra, a metà altezza dello schermo
  shape(house, width*0.8, height*0.5);

  //disegniamo cappuccetto rosso a sinistra, a metà altezza dello schermo
  shape(cappuccetto, 10, height*0.5);
}

Se provate ad eseguire questo codice, avrete una schermata simile alla seguente.

static-1

Va quasi bene! Dobbiamo però cambiare il colore della bambina in rosso. Come facciamo? Come al solito, andiamo sulla documentazione di PShape, scorriamo un po' e scopriamo che esiste un metodo chiamato setFill() che serve proprio per questo.

Ci sono altre soluzioni per colorare la forma, ad esempio quella che già conosciamo di usare la funzione fill() subito prima di disegnare la forma. Però noi useremo setFill() perché ci permette di colorare la nostra forma senza influenzare il resto del disegno.

Avendo trovato setFill() dentro la documentazione di PShape, dobbiamo usare una notazione particolare per poterla chiamare:

void setup() {
  fullScreen(); // usa tutto lo schermo
  cappuccetto = createShape(RECT, 0, 0, 30, 30);
  cappuccetto.setFill(color(255,0,0)); // riempimento rosso
  house = createShape(RECT, 0, 0, 100, 100);
}

static-2

OK! Ora ci rimane solo da far muovere cappuccetto rosso da sinistra a destra. Per fare questo, creiamo una nuova variabile che conterrà la posizione del personaggio. Il tipo di questa variabile è un numero intero, che in Processing si chiama int; come identificativo possiamo usare xCappuccetto, per far capire che è la coordinata x della variabile cappuccetto. Assegniamo anche il valore iniziale 10.

dich-assign

Questa volta, per comodità, abbiamo dichiarato la variabile ed assegnato il valore sulla stessa riga, ma ricordiamoci che sono comunque due operazioni differenti.

Per far muovere cappuccetto, sostituamo la coordinata x nella funzione shape() con questa nuova variabile, e ricordiamoci di incrementarla ad ogni ciclo. Il risultato finale è il seguente.

PShape cappuccetto;
PShape house;
int xCappuccetto = 10;

void setup() {
  fullScreen(); // usa tutto lo schermo
  cappuccetto = createShape(RECT, 0, 0, 30, 30);
  cappuccetto.setFill(color(255,0,0));
  house = createShape(RECT, 0, 0, 100, 100);
}

void draw() {
  background(#00FF00); // siamo nella foresta, lo sfondo è verde

  //disegniamo la casa a destra, a metà altezza dello schermo
  shape(house, width*0.8, height*0.5);

  //disegniamo cappuccetto a metà altezza dello schermo
  shape(cappuccetto, xCappuccetto, height*0.5);

  // incremento la coordinata x di cappuccetto
  xCappuccetto = xCappuccetto + 5;
}

dynamic

Mh... Cappuccetto Rosso ora si sposta ma continua anche oltre la casa della nonna...🤔 Dobbiamo aggiungere una condizione che faccia in modo tale che la bambina avanzi solo se ancora non è arrivata alla casa.

Controllo di flusso: condizioni

Per fermare Cappuccetto Rosso solo quando arriva dentro la casa, ci serve di aggiungere una condizione, qualcosa del tipo: se accade questo allora fai questo. In inglese se si traduce con if e allora si traduce con then. La struttura di controllo in programmazione si chiama infatti if-then.

Subito dopo l'if dobbiamo mettere una condizione, ovvero qualcosa che possa essere vero o falso. Nel nostro caso vogliamo che l'incremento della posizione di Cappuccetto Rosso avvenga solo se non è ancora arrivata alla casa. Vediamo graficamente le variabili rappresentate sullo schermo.

conditional

In codice possiamo tradurre il concetto "solo se xCappuccetto è minore di width*0.8, fai avanzare cappuccetto" come segue:

conditional

Vediamo nel dettaglio queste istruzioni:

conditional

Tutto inizia con if, che è una parola speciale nel linguaggio di Processing, e per questo viene chiamata keyword. Subito dopo, tra parentesi tonde, c'è la condizione: in questo caso la posizione x di Cappuccetto Rosso deve essere minore di width*0.8, che è esattamente la posizione della casa. Come possiamo vedere, questa condizione può essere solo vera o falsa, non ci sono altre possibilità nel mezzo. Dopo le parentesi tonde ci sono delle parentesi graffe che racchiudono le istruzioni da eseguire, solo se la condizione è vera.

Il risultato finale è quello che segue.

final

E questo è il codice relativo.

PShape cappuccetto;
PShape house;
int xCappuccetto = 10;

void setup() {
  fullScreen(); // usa tutto lo schermo

  cappuccetto = createShape(RECT, 0, 0, 30, 30);
  cappuccetto.setFill(color(255,0,0));

  house = createShape(RECT, 0, 0, 100, 100);
}

void draw() {
  background(#00FF00); // siamo nella foresta, lo sfondo è verde

  //disegniamo la casa a destra, a metà altezza dello schermo
  shape(house, width*0.8, height*0.5);

  //disegniamo cappuccetto a metà altezza dello schermo
  shape(cappuccetto, xCappuccetto, height*0.5);

  // incremento la coordinata x di cappuccetto
  // solo se non è ancora nella casa della nonna
  if (xCappuccetto < width*0.8) {
    xCappuccetto = xCappuccetto + 5;
  }
}

Fondamenti di programmazione

Ora che abbiamo visto alcune basi teoriche della programmazione, andiamo avanti e vediamo quali sono i mattoncini fondamentali che ci serviranno per costruire in pratica le nostre applicazioni.

Un paio di elementi li abbiamo già visti:

  • le variabili: un'area di memoria RAM su cui possiamo operare
  • i controllori di flusso condizionali (if): ci permettono di eseguire un'operazione oppure un'altra in base ad una condizione.

In questo capitolo affronteremo altri concetti fondamentali, come quello di classe, attributi e metodi.

Le classi

Abbiamo detto che un linguaggio di programmazione ci serve per risolvere problemi nel mondo reale. Per poter svolgere correttamente il suo lavoro, il linguaggio deve permetterci di rappresentare facilmente i concetti del mondo reale a cui facciamo riferimento.

Ad esempio, nel mio progetto ora ho le variabili cappuccetto rosso e la casa della nonna. Guardando semplicemente il codice, le variabili cappuccetto e house sono create ed usate in modo molto simile, non sono legate ad un concetto particolare distinto. L'unica cosa che differenzia le due variabili è il nome, a cui il programmatore umano sa dare un senso, ma per il computer (in questo caso il compilatore) i nomi delle variabili sono semplicemente un insieme di caratteri senza un particolare significato.

Il compilatore è il software che interpreta il codice che abbiamo scritto e lo traduce in linguaggio macchina. Se ci sono degli errori di sintassi nel nostro codice, il compilatore non riuscirà ad interpretarlo correttamente e ci restituirà un errore.

Si possono dare maggiori indizi al compilatore (e ai programmatori del mio team) per poter gestire meglio i diversi tipi di variabili?

La risposta è , e il modo in cui si fa è tramite la creazione di nuovi assegnare tipi da assegnare alle variabili. Ad esempio possiamo dire che cappuccetto è una Bambina, o un Personaggio della mia storia, o quello che vogliamo.

Analogamente, house potrebbe essere di tipo Edificio.

Come facciamo in pratica a fare una cosa del genere? Per creare nuovi tipi in Processing il modo più semplice è dichiarare una classe.

Classi

Le classi in informatica sono come degli stampini che servono per creare degli oggetti. Rivediamo un attimo l'attività che abbiamo fatto in classe.

classes

Gli stampini che avete usato avevano una forma ben definita: aereo, squalo, numero, etc. Potevate usare ogni stampino per creare tanti oggetti dello stesso tipo, ad esempio tanti aerei. Gli oggetti così creati saranno tutti molto simili fra loro, ma non perfettamente uguali: nel momento della creazione o dopo averlo stampato, possiamo modificarli un pochino.

Un vantaggio di usare questi stampini/classi è che possiamo creare velocemente degli oggetti simili tra di loro. Lo svantaggio è, che se dobbiamo modificare molto l'oggetto dopo la crezione, le cose cominciano a diventare complicate. Non c'è una regola assoluta per la scelta: in generale, cercate di mantenere il senso di quello che state facendo: non cercate di trasformare la formina di un aereo in un pesce, o viceversa.

Dichiarazione di una classe

Ipotizziamo che, nel nostro progetto, vogliamo che cappuccetto rosso sia un Personaggio. Per prima cosa, definiamo un nuovo tipo.

class Personaggio {}

Vediamo bene la sintassi, in ordine da sinistra a destra:

  • la keyword class, che dichiara una nuova classe
  • l'identificativo della classe, in questo caso la Personaggio
  • le parentesi graffe, che rappresentano il corpo (in inglese body) della classe

Proviamo ad usare questo nuovo tipo.

class Personaggio {};
Personaggio cappuccetto;

OK, abbiamo fatto in modo di esplicitare che cappuccetto rosso non è una forma qualsiasi, ma un Personaggio!

A questo punto però, se proviamo a compilare il programma, ci restituisce un errore quando proviamo ad assegnare cappuccetto con createShape(...). L'errore è il seguente:

Type mismatch, "PShape" does not match with "Personaggio".

Perché questa cosa? Riflettiamo: stiamo provando ad assegnare alla nostra variabile cappuccetto una forma, ma adesso il tipo è cambiato, e il compilatore non sa come assegnare una forma ad un personaggio. È giunto il momento di scrivere qualcosa nel corpo della classe.

Costruttore

Per prima cosa, dobbiamo dire che il nostro personaggio ha una forma. Inseriamo quindi all'interno della classe la dichiarazione di una variabile forma:

class Personaggio {
  PShape forma;
};

Guardate bene: abbiamo dichiarato una variabile di tipo forma dentro la classe personaggio. Bisogna quindi tenere in considerazione che:

  • si può usare questa variabile solo se si sta utilizzando un personaggio
  • ogni personaggio avrà una forma diversa

Finora abbiamo dichiarato la variabile forma, ma quando la assegniamo? L'assegnazione delle variabili della classe di solito avviene all'interno di una funzione speciale, chiamata costruttore. Questa funzione ha lo stesso nome della classe, e non bisogna specificare il valore di ritorno.

class Personaggio {
  PShape forma;
  Personaggio() { // questo è il costruttore!
    // qui dentro ci mettiamo quello che ci serve per inizializzare il nostro oggetto
    forma = createShape(RECT, 0, 0, 30, 30);
  }
};

Ora dobbiamo andare ad assegnare il valore corretto alla variabile cappuccetto. Questo si fa tramite una nuova keyword: new.

void setup() {
  // ...
  // Utilizziamo la keyword new per "stampare" un nuovo oggetto dalla classe
  cappuccetto = new Personaggio();
}

OK, ora non abbiamo più l'errore di prima ma ne è comparso un altro per riga dopo:

cappuccetto.setFill(color(255,0,0));
The function "setFill(int)" does not exist.

Perché questo? Cerchiamo di capire bene: abbiamo utilizzato il simbolo punto (.) subito dopo la variabile cappuccetto. Cosa significa questo punto? Risposta: significa che stiamo andando a richiamare variabili e funzioni all'interno della classe di cui fa parte cappuccetto, in questo caso Personaggio. In effetti, se andiamo a vedere dentro la classe c'è solo il costruttore, non esiste un metodo setFill().

Ci sono varie soluzioni possibili: una potrebbe essere creare il metodo che ci serve. Per ora però usiamo una strategia diversa: visto che il setFill() fa parte della creazione del personaggio, spostiamo questo metodo dentro il costruttore della classe:

class Personaggio {
  PShape forma;
  Personaggio() {
    forma = createShape(RECT, 0, 0, 30, 30);
    forma.setFill(color(255,0,0));
  }
};

Fate attenzione: abbiamo richiamato setFill() sulla variabile forma, perché all'interno della classe, è questa variabile a dover cambiare colore.

OK, anche questo errore è risolto. Ma ne abbiamo ancora uno (l'ultimo, per fortuna):

shape(cappuccetto, xCappuccetto, height*0.5);
The function "shape()" expects parameters like: "shape(PShape,  float,  float)"

Questo perché la funzione shape() si aspetta come primo parametro una variabile di tipo PShape, ma noi gli stiamo passando una variabile di tipo Personaggio.

Per risolvere questo problema, possiamo creare una funzione disegna() dentro la classe Personaggio, che, come dice il nome, disegna il nostro personaggio. Proviamo.

class Personaggio {
  //...
  void disegna() {
    shape(forma, xCappuccetto, height*0.5);
  }
};

void draw() {}
  // ...
  // chiamiamo il nuovo metodo disegna()
  cappuccetto.disegna();
}

OK, ora funziona tutto!

Scriviamo di seguito il codice completo, per riferimento.

Personaggio cappuccetto;
PShape house;
int xCappuccetto = 10;

class Personaggio {
  PShape forma;
  Personaggio() {
    forma = createShape(RECT, 0, 0, 30, 30);
    forma.setFill(color(255,0,0));
  }
  void disegna() {
    shape(forma, xCappuccetto, height*0.5);
  }
};

void setup() {
  fullScreen(); // usa tutto lo schermo

  cappuccetto = new Personaggio();

  house = createShape(RECT, 0, 0, 100, 100);
}

void draw() {
  background(#00FF00); // siamo nella foresta, lo sfondo è verde

  //disegniamo la casa a destra, a metà altezza dello schermo
  shape(house, width*0.8, height*0.5);

  //disegniamo cappuccetto a metà altezza dello schermo
  cappuccetto.disegna();
  // incremento la coordinata x di cappuccetto
  // solo se non è ancora nella casa della nonna
  if (xCappuccetto < width*0.8) {
    xCappuccetto = xCappuccetto + 5;
  }
}

Miglioramenti

Ci sono dei miglioramenti che possiamo fare al codice scritto finora, per renderlo più generale e facilmente utilizzabile.

Parametri del costruttore

Guardiamo da vicino il costruttore della nostra classe:

Personaggio() {
  forma = createShape(RECT, 0, 0, 30, 30);
  forma.setFill(color(255,0,0));

}

La classe Personaggio in questo momento crea tutti oggetti con la forma di un quadrato 30x30 di colore rosso. Se volessimo creare dei personaggi di forma e colore diverso? Voler generalizzare una funzione è un problema comune e la soluzione è semplice: possiamo parametrizzare la funzione, rendendola più generica.

Questa tecnica la applicheremo al costruttore ma si può utilizzare per qualsiasi funzione.

Dobbiamo fare i seguenti passi:

  1. creare dei parametri all'interno delle parentesi tonde nella dichiarazione della funzione, specificando tipo e nome del parametro
  2. sostituire i valori costanti all'interno della funzione con il nuovo parametro
  3. aggiungere i parametri necessari quando viene chiamata la funzione

Nel nostro caso, ipotizziamo di voler avere la possibilità cambiare la dimensione ed il colore del personaggio:

  1. aggiungo i parametri nella dichiarazione del costruttore:
Personaggio(float siz, color col) {
   //...
}
  1. sostituisco le costanti con i nuovi all'interno della funzione:
Personaggio(float siz, color col) {
    forma = createShape(RECT, 0, 0, siz, siz);
    forma.setFill(col);
}
  1. aggiungo i parametri necessari quando creo l'oggetto:
cappuccetto = new Personaggio(30, color(255,0,0));

Ora posso creare nuovi personaggi con forma e colore diverso, semplicemente cambiando i parametri quando creo l'oggetto.

Variabili di posizione

C'è ancora qualcosa che posso migliorare. Ragionandoci, la variabile xCappuccetto rappresenta la posizione del personaggio di cappuccetto rosso, e quindi fa parte delle caratteristiche del personaggio. Se in seguito dovessi creare il personaggio della nonna, vorrei che anche lei avesse una propria posizione.

Sposto quindi la variabile xCappuccetto da fuori a dentro la classe Personaggio, cambiandogli nome nel più generico x.

class Personaggio {
  //...
  int x = 10; // tutti i personaggi iniziano dalla posizione 10
  // ...
}

Quando disegno il personaggio, sostituisco la variabile di posizione con questa nuova:

class Personaggio {
  //...
  void disegna() {
      shape(forma, x, height*0.5);
  }
}

Infine, quando cambio la posizione del personaggio, devo ricordarmi di sostituire la vecchia variabile con la nuova:

if (cappuccetto.x < width*0.8) {
    cappuccetto.x = cappuccetto.x + 5;
  }

Come potete vedere, anche in questo caso ho usato la notazione con il punto per accedere alla variabile interna alla classe.

Codice finale

Di seguito il codice completo finora.

Personaggio cappuccetto;
PShape house;


class Personaggio {
  PShape forma;
  int x = 10;
  Personaggio(float siz, color col) {
    forma = createShape(RECT, 0, 0, siz, siz);
    forma.setFill(col);
  }
  void disegna() {
    shape(forma, x, height*0.5);
  }
};

void setup() {
  fullScreen(); // usa tutto lo schermo

  cappuccetto = new Personaggio(30, color(255,0,0));

  house = createShape(RECT, 0, 0, 100, 100);
}

void draw() {
  background(#00FF00); // siamo nella foresta, lo sfondo è verde

  //disegniamo la casa a destra, a metà altezza dello schermo
  shape(house, width*0.8, height*0.5);

  cappuccetto.disegna();
  // incremento la coordinata x di cappuccetto
  // solo se non è ancora nella casa della nonna
  if (cappuccetto.x < width*0.8) {
    cappuccetto.x = cappuccetto.x + 5;
  }
}

Passiamo al 3D

Finora abbiamo usato Processing per disegnare in due dimensioni: rettangoli, quadrati, ellissi, etc. E se volessimo disegnare in tre dimensioni, come ad esempio cubi, sfere o forme più complesse?

Passare al 3D con Processing è molto semplice, in quanto questo linguaggio è nato in realtà proprio per rendere semplice la creazione di applicazioni in tre dimensioni. Seguiremo ora passo passo le operazioni da fare per passare dal 2D al 3D.

Dichiariamo di voler utilizzare le funzionalità 3D

Prima di tutto dobbiamo dichiarare esplicitamente che vogliamo utilizzare le funzionalità 3D di Processing. Questo va fatto aggiungendo il parametro P3D alla funzione fullScreen() o alla funzionesize().

void setup() {
  fullScreen(P3D);
  //...
}

Carichiamo un modello 3D

Ora che siamo in 3D, possiamo caricare modelli tridimensionali. Processing attualmente supporta unicamente il formato .obj, quindi dobbiamo fare attenzione a cercare dei modelli in questo formato o a convertirlo opportunamente, ad esempio con Blender.

Per convertire un file con Blender, aprire un nuovo progetto, eliminare il cubo di default, importare il file dal formato di origine (es. .fbx, .stl, .gltf), eventualmente modificarlo come necessario (rotazione, scalatura, etc.), quindi esportarlo in formato .obj.

Per il nostro esempio, useremo un modello di Cappuccetto Rosso disponibile qui: cappuccetto.obj. Scaricate il file e salvatelo all'interno della vostra cartella di progetto, nella sottocartella data/.

L'organizzazione dei file e delle cartelle deve essere il seguente:

folder structure

Adesso, per importare il modello, dobbiamo usare la funzione loadShape():

forma = loadShape("cappuccetto.obj");

Attenzione: anche in questo caso, parametrizziamo il costruttore per fare in modo di scegliere nel momento in cui viene creato l'oggetto, quale modello si vuole importare. In questo caso, "cappuccetto.obj" è di tipo stringa, quindi il nostro costruttore diventerà:

Personaggio(String filename, color col) {
    forma = loadShape(filename);
    forma.setFill(col);
}

e per creare l'oggetto scriveremo:

cappuccetto = new Personaggio("cappuccetto.obj", color(255,0,0));

Se eseguiamo il nostro progetto, otterremo una cosa del genere:

cappuccetto flat

Va quasi bene, ma il nostro modello sembra un po' troppo "piatto". In effetti, di default Processing mostra solo l'ombra del nostro modello. Per visualizzare qualcosa di più tridimensionale, dobbiamo "accendere le luci" attraverso la funzione lights() da chiamare all'interno di draw().

void draw() {
  background(#00FF00);
  lights();
  //...
}

Ora otteniamo qualcosa del genere:

cappuccetto3D

Bene, era quello che volevamo!

Nota sulla direzione degli assi

Di default, Processing ha l'asse z rivolto verso il basso. Questa convenzione è opposta a quella usata comunemente, in cui l'asse z punta verso l'alto.

Per risolvere il problema, ci sono varie soluzioni. Per ora ve ne consiglio due:

  • modificare l'orientamento dell'.obj, ad esempio con Blender potete usare la combinazione di tasti r-x-180-invio
  • ruotare il modello da dentro Processing di 180 gradi attorno all'asse X, subito dopo aver caricato il modello; il comando è quindi .rotateX(radians(180))

Costrutti ciclici

Cappuccetto Rosso deve passare attraverso una foresta, ma finora nella nostra applicazione c'è solo un grande prato. Come facciamo a disegnare tanti alberi?

La foresta nera

Cappuccetto Rosso è una storia popolare di varie parti del mondo, ma possiamo far riferimento alla versione dei Fratelli Grimm ed immaginarci che la bambina debba attraversare la foresta nera, formata da una fitta vegetazione di abeti. Per cominciare, proviamo a disegnare la foresta con gli strumenti che abbiamo visto finora.

Definizione della classe Pianta

Come abbiamo fatto finora, creiamo una nostra classe Pianta per rappresentare un albero.

Abbiamo scelto Pianta ma anche Albero o Abete potevano essere delle scelte accettabili; dipende molto da cosa immaginiamo possa servirci in futuro. In ogni caso, l'importante è che la scelta sia sensata adesso, in futuro possiamo sempre cambiare nome (questa operazione si chiama refactoring). Da evitare assolutamente invece un nome che al momento non ha molto senso ma prevediamo che possa averlo in futuro; in questo modo infatti stiamo creando un programma inconsistente, con probabili conseguenze catastrofiche.

Che caratteristiche ha la nostra pianta? Ha sicuramente una forma ed una posizione nella foresta; inoltre ci piacerebbe che le diverse piante abbiano una dimensione diversa, quindi aggiungiamo anche un attributo per la dimensione. Altra domanda: cosa possiamo fare con la nostra pianta? Diciamo che possiamo seminarla per farla comparire nella nostra foresta.

Ecco quindi una possibile definizione di questa classe.

class Pianta {
  // All'inizio mettiamo gli attributi della classe, ovvero le sue caratteristiche
  PShape forma;
  float x, y;
  float dimensione;

  // Subito dopo, scriviamo il costruttore
  Pianta(String piantaObj) {
    // assegniamo la dimensione in modo casuale
    dimensione = random(30, 80);
    // carichiamo il modello della nostra pianta, passandogli come parametro il file con il modello
    forma = loadShape(piantaObj);
    // scaliamo l'oggetto
    forma.scale(dimensione);
    // ruotiamolo lungo l'asse X per rispettare le convenzioni di Processing
    forma.rotateX(radians(180));
    // ruotiamolo casualmente lungo l'asse verticale, per dare un po' di vivacità alla foresta
    forma.rotateY(radians(random(-45,45)));

    // opzionale: coloriamo la pianta di verde. Utile se il modello non è già colorato di suo
    forma.setFill(#28C61E);

    // assegniamo una posizione casuale nello schermo
    x = random(0, width);
    y = random(0, height);
    // spostiamo la pianta nella nuova posizione
    forma.translate(x,y);
  }


  void semina() {
    // disegniamo la pianta
    shape(forma);
  }
}

OK, ora abbiamo la nostra pianta. Ora vediamo come disegnarne tante per creare una foresta.

Versione senza cicli

Immaginiamo di voler aggiungere 3 alberi. Come già sappiamo fare, creiamo tre variabili ed assegniamo ad ognuna di esse un nuovo albero.

// Dichiarazione delle variabili
Pianta abete0;
Pianta abete1;
Pianta abete2;

void setup() {
  // [...]
  // Assegnazione delle variabili
  abete0 = new Pianta("abete.obj");
  abete1 = new Pianta("abete.obj");
  abete2 = new Pianta("abete.obj");
}

void draw() {
  // [...]
  // Disegno gli alberi
  abete0.semina();
  abete1.semina();
  abete2.semina();
}

Questo metodo funziona se abbiamo pochi alberi da disegnare. Ma se ne volessimo disegnare tanti, diventerebbe molto scomodo!

Per risolvere questo problema dobbiamo usare due nuovi strumenti che ci mette a disposizione Processing: gli array e i cicli. Cominciamo dagli array.

Array

Avere tante variabili simili tra di loro abete0, abete1, etc. è scomodo ed è molto facile sbagliarsi. Sarebbe utile se potessimo dichiarare una sola variabile che contiene tutti gli alberi da disegnare, in modo da maneggiarla con più facilità. Questo in programmazione si può fare attraverso uno strumento che si chiama array.

In Processing (ovvero in Java) gli array sono una sequenza di oggetti tutti dello stesso tipo. Per dichiarare un array usiamo la seguente sintassi.

// Dichiarazione array
Pianta[] arrayAbeti;

Come si vede, aggiungendo le parentesi quadre dopo il tipo base, la variabile dichiarata si trasforma in un array; in altre parole, un insieme ordinato di piante. Per ordinato intendiamo che le piante sono in sequenza: esiste una pianta identificata con il numero0, seguita da pianta identificata con 1 e così via.

Ora che abbiamo dichiarato la variabile, dobbiamo assegnarli un valore. Per fare questo, usiamo la seguente sintassi nella funzione setup().

void setup() {
  // [...]

  // Assegnazione di un array
  arrayAbeti = new Pianta[3];
}

Prestate la massima attenzione a questa sintassi. Notiamo prima di tutto che nell'assegnazione dobbiamo ripetere la notazione con il tipo e le parentesi quadre. In questo caso però, dentro le parentesi aggiungiamo anche il numero di oggetti che vogliamo mettere nel nostro array, in questo caso tre.

Fate inoltre attenzione che questa assegnazione crea l'array, ovvero il contenitore, ma non i singoli alberi. Per creare i singoli alberi, dobbiamo comunque fare un assegnazione per ogni elemento dell'array.

// Dichiarazione array
void setup() {
  // [...]
  // Assegnazione array
  arrayAbeti = new Pianta[3];
  // Assegnazione singole piante
  arrayAbeti[0] = new Pianta("abete.obj");
  arrayAbeti[1] = new Pianta("abete.obj");
  arrayAbeti[2] = new Pianta("abete.obj");
}

Fate attenzione che, convenzionalmente, il primo elemento dell'array è identificato con l'indice 0. Ne segue che l'ultimo elemento sarà identificato con l'indice n-1, dove n è la lunghezza dell'array. Nel nostro caso n è 3 e quindi l'ultimo elemento avrà indice 2.

Finora la situazione non è che sia migliorata molto, ma dobbiamo ancora mettere in campo il secondo strumento di cui abbiamo accennato: i cicli

Ciclo for classico

Osserviamo bene queste righe:

arrayAbeti[0] = new Pianta("abete.obj");
arrayAbeti[1] = new Pianta("abete.obj");
arrayAbeti[2] = new Pianta("abete.obj");

Notiamo che sono quasi del tutto identiche, tranne che per il numero dentro le parentesi quadre. Come facciamo a mettere a fattor comune la parte che si ripete? Con i cicli!

Ricapitoliamo cosa dobbiamo fare: ripetere un'istruzione per (in inglese for) ogni valore di un indice intero che varia da 0 a 2, incrementando ogni volta l'indice di una unità. Mettendo insieme queste cose, veniamo alla formulazione classica del ciclo for:

for(int index = 0; index < 3; index = index + 1) {
  arrayAbeti[index] = new Pianta("abete.obj");
}

Ora possiamo aumentare il nostro numero di alberi semplicemente cambiando il valore 3 nell'assegnazione dell'array e nel ciclo. Ottimo!

Nota: possiamo migliorare leggermente il codice qui sopra, per evitare di sbagliare la dimensione dell'array con conseguenze spesso disastrose. Al posto del valore 3, possiamo mettere arrayAbeti.length, che ci restituisce sempre il valore corretto della lunghezza dell'array. Miglioriamo quindi il ciclo scritto qui sopra nel seguente modo:

for(int index = 0; index < arrayAbeti.length; index = index + 1) {
  arrayAbeti[index] = new Pianta("abete.obj");
}

Enhanced for loop

Il caso particolare per cui dobbiamo ripetere delle istruzioni per tutti gli elementi di un array è molto comune e per questo Processing (ovvero Java) prevedono un costrutto particolare, più chiaro e conciso.

Nella funzione draw() possiamo usare la seguente sintassi per disegnare gli alberi.

for (Pianta abete: arrayAbeti) {
  abete.semina();
}

Questo for chiama la funzione .semina() per tutti gli elementi dell'array. Come vedete, rispetto alla sintassi classica, questa versione "migliorata" (enhanced) è più breve, non rischiamo di sbagliare il valore di inizio o fine dell'indice e, soprattutto, rendiamo esplicito che vogliamo fare un'iterazione su un array. Questo è utile sia per gli altri sviluppatori, per comprendere meglio quello che sta facendo il codice, sia per il compilatore che in questo modo ha maggiori possibilità di ottimizzare il codice e renderlo più veloce.

Codice finale

Personaggio cappuccetto;

class Personaggio {
  PShape forma;
  // costruttore, chiamato quando viene usato "new"
  Personaggio(String filename) {
    forma = loadShape(filename);
    forma.setFill(color(255, 0, 0));
  }

  void disegna(float x, float y) {
    shape(forma, x, y);
  }
}

PShape house;
int xCappuccetto = 150;
// Dichiarazione array
Pianta[] arrayAbeti;

void setup() {
  fullScreen(P3D); // usa tutto lo schermo

  cappuccetto = new Personaggio("cappuccetto.obj");
  cappuccetto.forma.rotateX(radians(180));

  house = createShape(RECT, 0, 0, 100, 100);

  // Assegnazione array
  arrayAbeti = new Pianta[3];

  // Assegnazione singole piante
  for (int index = 0; index < arrayAbeti.length; index = index + 1) {
    arrayAbeti[index] = new Pianta("abete.obj");
  }
}

void draw() {
  background(#00FF00); // siamo nella foresta, lo sfondo è verde

  // accendiamo le luci per vedere gli oggetti in 3D
  lights();



  //disegniamo la casa a destra, a metà altezza dello schermo
  shape(house, width*0.8, height*0.5);

  //disegniamo cappuccetto a metà altezza dello schermo
  cappuccetto.disegna(xCappuccetto, height*0.5);

  // incremento la coordinata x di cappuccetto
  // solo se non è ancora nella casa della nonna
  if (xCappuccetto < width*0.8) {
    xCappuccetto = xCappuccetto + 5;
  }

  for (Pianta abete : arrayAbeti) {
    abete.semina();
  }
}

Assets

Qui potete trovare il link per scaricare i modelli usati in questo corso: