La serie InViaggio tratta di piccoli programmi fatti mentre sono in treno
Obbiettivo: creare un generatore di diverse forme di vario colore in (0,0,0)
Progetto
Vogliamo avere un vettore statico di oggetti primitivi a cui assegnare un materiale da colorare successivamente. Dunque:
Update() e LateUpdate() sono soggetti al framerate per funzionare correttamente, FixedUpdate() no. Infatti può essere chiamato anche più volte in un solo frame.
Gli oggetti da spawnare sono selezionabili nell'inspector:
Vogliamo ciclare tra tutti gli elementi del vettore alla pressione di qualsiasi tasto (sia della tastiera, che del mouse, o al tocco su iPhone/AndroidPhone). Dunque:
L'oggetto viene creato, ma in una posizione casuale! Centriamo l'oggetto appena creato in (0,0,0) e, allo scopo di farlo cadere nel vuoto, aggiungiamo un RigidBody alla quale applichiamo una forza in una direzione casuale:
Il RigidBody è un componente per gli oggetti che permette di gestire il movimento del suddetto oggetto secondo parametri fisici, passandolo al motore fisico di Unity.
Ora, vogliamo cambiare il colore all'oggetto appena creato. Ciò viene fatto aggiungendo un componente Renderer al tipo primitivo, assegnandogli un materiale e dandogli un colore casuale tramite l'apposita funzione contenuta in Random: (qui correggo una piccola distrazione: l'if che contiene l'indice và dentro la condizione di Input, non fuori!)
Sceglie un colore casuale in base ai parametri dati (tinta minima, tinta massima, saturazione minima, saturazione massima, valore minimo, valore massimo, luminosità minima, luminosità massima), tutti float compresi tra 0 ed 1
L'unico problema è che gli oggetti così creati non vengono distrutti! Aggiungiamo un piccolo script che distrugge gli oggetti spawnati dopo pochi secondi:
ed aggiungiamo questa riga allo script precedente:
Risultato:
Voilà! Modificando leggermente lo script, è possibile far spawnare anche oggetti creati ad hoc.
L'idea è di creare un frattale a partire da un oggetto di base, creado di iterazione in iterazione dei figli del tutto identici all'originale se non per dimensione. È un buon esercizio mentale, senza contare che si può lavorare su di un concetto fondamentale, l'iterazione.
Creiamo un oggetto vuoto nella scena, ed assegnamo la classe "Frattale"
Dunque, vogliamo che il frattale parta da una certa mesh che gli assegneremo in partenza e, da lì, che calcoli e instanzi i figli. La classe dovrà avere dunque 2 variabili pubbliche MeshRenderer e MeshFilter.
Il componente MeshRenderer prende la geometria fornita dal componente MeshFilter e la renderizza alle coordinate fornite dalla Transform
Il componente MeshFilter prende una mesh dalle risorse del progetto (personalmente aggiunta o dalle primitive) e la passa al MeshRenderer per renderizzarla. È necessario perché definisce dinamicamente la mesh.
Assegnamo dall'Inspector un materiale creato ad hoc e scegliamo una mesh dalle primitive di Unity.
Basta mettere una texture (cioè un'immagine) in qualsiasi punto della cartella Assets del progetto, e trascinarla su di un oggetto all'interno della scena. Unity provvederà a creare automaticamente un materiale con quella texture.
Il metodo Start() viene chiamato da Unity dopo che l'oggetto è stato creato, attivato e prima che Update() venga chiamata per la prima volta per quell'oggetto specifico (se ha una funzione Update() definita).
Il metodo AddComponent() aggiunge un componente della classe T definita, attaccandola al gameObject a cui fa riferimento. Siccome ritorna un riferimento all'oggetto si può anche scrivere gameObject.AddComponent().mesh = Mesh_del_Frattale; per creare ed inizializzare la mesh.
Aggiungiamo dunque dei figli al frattale. Il modo più semplice con cui aggiungere dei figli ad un oggetto tramite script è tramite la sintassi:
"new" è un comando che viene usato per costruire una nuova instanza di un oggetto o di una struct. È seguito dal metodo costruttore, con lo stesso nome della classe di appartenenza.
Con tale sintassi, però, vengono istanziati infiniti oggetti Frattale, con il risultato di far bloccare il PC. Introduciamo quindi una variabile che definisce il numero massimo di iterazioni ed il numero corrente di iterazioni:
E le inizializziamo tramite il metodo di Unity Initialize():
this è una parola chiave che ritorna un riferimento all'oggetto corrente o, in alternativa, all'oggetto di cui si sta chiamando il metodo.
Initialize() viene chiamata subito dopo AddComponent(). L'ordine è Creazione Oggetto -> Creazione componente Frattale -> Awake() -> OnEnable() -> AddComponent() -> Initialize() -> Start().
Start viene chiamata al frame succesivo.
Vengono dunque creati, così, 4 figli nella gerarchia. Tuttavia noi vogliamo che gli oggetti siano figli dell'oggetto frattale principale. Cioè:
E lo facciamo tramite il comando transform.setParent(Genitore.transform); :
Tuttavia anche così i figli istanziati rimangono nella stessa posizione del padre. Introduciamo una variabile per gestire la scala dei figli e li posizioniamo sopra l'oggetto di Origine:
Il risultato:
Un vero frattale, però, ha figli anche nelle altre direzioni. Ristrutturiamo dunque il metodo Initialize() per includere la direzione nella quale istanziare i figli:
Il risultato:
Ora, il problema è che, ipotizzando di istanziare figli su tutte le 6 facce del cubo, si otterrebbe che in pochi frame si istanziano 6^4 = 1296 oggetti, con conseguente rallentamento ed impossibilità di vedere il frattale effettivamente "crescere". Poniamo dunque un intervallo tra una istanziazione e l'altra attraverso la creazione di un metodo IEnumerator e l'utilizzo di StartCoroutine(). Dunque:
Creare una coroutine in Unity vuol dire essenzialmente creare un iteratore. Quando si passa l'iteratore al metodo StartCoroutine(), il motore chiede il prossimo oggetto di quel metodo ad ogni frame successivo, finché il metodo termina. La parola chiave yield è quella che produce gli oggetti, tutto il resto è da considerare un effetto collaterale dell'iteratore. WaitForSeconds è usato per dare alla coroutine un comportamento controllato, ma in generale si comporta esattamente come iteratore.
Quando si utilizza la parola chiave yield in un'istruzione, si indica che il metodo, l'operatore o la funzione di accesso get in cui appare è un iteratore. Utilizzando yield per definire un iteratore, si elimina la necessità di una classe esplicita aggiuntiva (la classe che contiene lo stato per un'enumerazione, vedere IEnumerator per un esempio) quando si implementano i modelli IEnumerator e di IEnumerable per un tipo di raccolta personalizzato.
Bene, ma non benissimo. I figli vengono istanziati in piccole raffiche che causano ancora rallentamenti. Introduciamo dunque un ritardo randomico a WaitForSeconds in modo da distribuire la creazione dei figli in un certo intervallo di tempo:
C'è ancora un problema: i figli vengono instanziati all'interno del padre. Utilizzando la visuale Overdraw questo è piuttosto evidente:
Vogliamo dunque ruotare i figli nella direzione in cui si istanziano. Per fare questo rimuoviamo l'istanziazione a Vector3.down, e rimaniamo le altre, aggiungendo ad Initialize() un altro parametro per gestire la rotazione:
Il risultato:
Tuttavia questo fa rimanere una parte del frattale vuota. Dunque, ristrutturando brevemente e concisamente il codice possiamo fare in modo che l'istanziazione dei figli sia indicizzata:
tale risultato è raggiunto semplicemente aggiungendo dei vettori con la posizione e la rotazione dei figli come variabili private, ed aggiustando in modo consono Initialize(). Il risultato è identico a prima, ma ora possiamo aggiungere semplicemente le direzioni in cui vogliamo i figli:
Ed il risultato:
abbiamo ottenuto il frattale completo! Possiamo apportare infinite modifiche, da questo punto in poi: possiamo dare un colore diverso ad ogni componente, farli lampeggiare, aggiungere rotazioni extra ai figli, farli istanziare in modo asimmetrico...le possibilità sono legate solo alla fantasia (e pazienza!).
Iniziamo col creare un nuovo progetto. La scena di default inizia con due componenti, la Main Camera posizionata alle coordinate (0,1,-10) e la Directional Light alle coordinate (0,3,0).
La Camera (ita. telecamera) è l'oggetto attraverso il quale il giocatore può osservare il mondo. È una matrice, il cui punto di origine è il punto inferiore sinistro di coordinate (0,0), ed il punto finale è quello in alto a destra di coordinate (larghezza,altezza)
Necessitiamo di una struttura per costruire l'orologio. Questo orologio sarà composto da semplici cubi, animati attraverso scripts.
Creiamo dunque nel pannello della gerarchia (eng. Hierarchy) un nuovo oggetto vuoto attraverso Create -> Create Empty. Con lo stesso procedimento, avendo selezionato il nuovo oggetto vuoto, creiamo tre cubi attraverso Create->3D Object->Cube, rendendoli dunque figli del nuovo oggetto. Rinominiamo il nuovo oggetto creato "Orologio", ed impostiamo le sue coordinate a (0,0,0).
Sostanzialmente, qualsiasi cosa si trovi nella scena (cioé il livello che su cu stiamo lavorando) è un GameObject. Ha un nome, un tag, un layer, una componente Transform, e può essere segnato come "Static". Di per sè non è nulla se non un contenitore, utile dunque a contenere altri oggetti.
Un figlio (eng. Child) è un oggetto che nella gerarchia è contenuto da un altro oggetto (detto genitore, eng. parent). Ne eredita la Transform e la applica prima di ogni altra modifica. Dunque se per esempio il figlio si trova a (5,5,0), ed il padre a (1,1,1), il figlio finisce a (6,6,1).
Rinominiamo i cubi creati in "Ore", "Minuti", "Secondi" e cambiamo le scale in modo da avere come posizioni(0, 1, 0),(0, 1.5, 0),(0, 2, 0) e come scala (0.5, 2, 0.5),(0.25, 3, 0.25),(0.1, 4, 0.1), rispettivamente per le ore, i minuti ed i secondi.
Passiamo ora ad animare l'orologio.
Creiamo uno script nella vista del Progetto e chiamiamolo "Animatore_Orologio". Lo apriamo e cominciamo a scrivere le 3 variabili Transform delle Ore, Minuti e Secondi. Attacchiamo questo script all'oggetto creato (io ne ho creato un'altro chiamato "Detentore_Scripts". In genere è l'oggetto che detiene tutti gli script che si occupano di gestire script da non assegnare ad altri oggetti, dunque unici) e trasciniamo sopra le lancette ai relativi campi.
Notiamo che stiamo usando il namespace di UnityEngine, senza il quale non potremmo includere MonoBehaviour.
È come una collezione di definizioni, tipi, strutture, macro, funzioni e quant'altro. Includere i namespace in un progetto è utile per evitare di riscrivere codice già sviluppato. I namespace possono essere innestati (es. System.IO -> namespace System contiene namespace IO)
È la classe del namespace UnityEngine. Contiene tutte le fantastiche funzioni di Unity ed è la classe che fa funzionare cose come Update(), Start(), Awake().
Cominciamo ora a scrivere:
Update(), come altri metodi specifici di Unity, è considerato in modo particolare. Li troverà in ogni caso e li invocherà quando necessario, non importa come li si dichiari. Inoltre è buon uso non dichiarare Update() public perché può essere accesso erroneamente da altre classi, mentre con private qualsiasi altra funzione non potrà vederlo (es. bottoni)
Come dovrebbe muoversi l'orologio? Consideriamo, per ora, un orologio digitale.
Consideriamo la lancetta delle ore. Per compiere un giro completo (360°) deve fare 12 passi; la lancetta dei minuti, invece, 60, come la lancetta dei secondi. Dunque si ha:
la variabile "in_Analogico" tornerà utile più tardi.
Vogliamo conoscere il tempo ad ogni aggiornamento, dunque scriviamo:
Unity simula la scena frame per frame. Idealmente si vuole un rapporto frame/secondi quanto più alto possibile, e Unity consente di settare a quanto si vuole arrivare con Application.targetFrameRate. È di solito ci si assesta attorno ai 60fps (ma molti videogiochi vanno per i 30 per ragioni legate ai motori fisici). Questo vuol dire che Update() viene chiamata 60 volte in un secondo. Di contro la funzione FixedUpdate() viene richiamata ogni K frames. LateUpdate() viene chiamata dopo Update(). C'era anche EarlyUpdate(), ma è stata rimossa visto che è più facile impiegare flag di stato in LateUpdate().
Vogliamo dunque ruotare le lancette modificandone la rotazione:
I quaternioni sono dei costrutti matematici che utilizzano i numeri complessi per gestire movimenti angolari.
Abbiamo ora un orologio digitale in tempo reale. E se ne volessimo uno analogico?
Per cambiare da analogico a digitale ci basta premere il segno di spunta.
Questo blog seguirà i miei sforzi di imparare meglio Unity.
Scelgo di scrivere queste "guide" sotto forma di testo e non di video perché il testo è più facile da seguire, interrompere, riadattare al proprio ritmo di pensiero.
Ogni post si prefiggerà come obbiettivo di completare un progetto (o parte di esso) più o meno semplice, spiegando al contempo il perché di di determinate scelte, corredando il tutto con codice, illustrazioni e, alla fine del post, con il materiale sviluppato scaricabile gratuitamente sotto forma di .unitypackage . Sentitevi liberi di farne ciò che volete.
Gli script saranno scritti in C#, questo per due motivi principali:
C# ha una sintassi più restrittiva di Javascript, imponendo un certo ordine di pensiero rispetto a linguaggi più libertari.
Javascript in Unity non è effettivamente Javascript, ma una versione modificata chiamata Unityscript, che sta venendo lentamente abbandonata. C#, invece, può fare uso anche di tutte le librerie proprie (es. System.IO)
Farò del mio meglio per nominare tutte le variabili in italiano comprensibile e per spiegare tutte le funzioni di Unity. I link che posterò di riferimento saranno per forza di cosa in inglese, dal momento che sarebbe troppo costoso, in termini di tempo, tradurre interi libri di riferimento (sic!) di Unity.