Secondo appuntamento con Unity. Stavolta vedremo come aggiungere interattività agli oggetti nella scena scrivendo del codice in C#.
Forse la copertina sembra un po' esagerata, ma vi assicuro, neppure tanto. Perché capita molto spesso (o meglio, a me è capitato) di perdere le staffe perché Unity non sembra comportarsi come noi vorremmo. In realtà, siamo noi che non riusciamo a capire come vuole sentirselo dire.
Quello che vi propongo oggi non è un tutorial. Se avessi voluto fare un tutorial, avrei fatto delle scelte diverse. Mi piace invece definirlo un incoraggiamento per tutti coloro che non hanno mai usato Unity. Oggi vedrete che non è poi così difficile metterci le mani dentro.
Detto questo, allacciate le cinture, perché oggi si ballerà un po'.
I componenti di Unity
I componenti, nel gergo di Unity, sono elementi aggiuntivi che possono essere inseriti in un oggetto della scena per definirne il comportamento. Si tratta di elementi software che aggiungono una regola che dichiara come l'oggetto deve evolversi nel tempo, oppure come deve comportarsi quando interagisce con l'utente o con il resto della scena.
Costruire un componente è molto semplice: basta creare una classe in C# che estende la classe MonoBehaviour (si, scritto alla maniera britannica). Cliccate con il tasto destro nel database degli asset, selezionate "Create" e poi "C# Script" (o "MonoBehaviour Script" se avete installato Unity 6).
Quello che verrà aggiunto agli asset è un pezzo di codice simile a questo:
using UnityEngine;
public class Dummy : MonoBehaviour {
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start() {
}
// Update is called once per frame
void Update() {
}
}
Lasciamo perdere per ora i commenti; la parte importante è che, diversamente da molti altri contesti di programmazione, non c’è nessuna funzione main. Non c’è un punto d’inizio da cui partire a fare qualcosa. Il nostro oggetto sulla scena è un po’ come una palla da biliardo: rimarrà fermo a meno che qualcuno non lo spingerà.
I metodi che definiamo dentro la classe verranno chiamati dal Core dell’engine nel momento in cui viene sollecitato all’oggetto che contiene lo script.
In uno script vuoto abbiamo già due funzioni (per ora senza codice) pronte a essere chiamate.
Start viene chiamata quando l’oggetto inizia a esistere nella scena. Quando facciamo partire la simulazione tutte le funzioni Start di tutti gli script di tutti gli oggetti vengono eseguite. Se no, per gli oggetti creati dinamicamente, appena l’oggetto entra in scena. Qui dobbiamo mettere il codice che ci serve per inizializzare lo stato del nostro oggetto.
Update viene chiamata a rotazione dal Core per far evolvere gli oggetti nel tempo. Se il nostro oggetto deve compiere un’azione costante tipo ruotare su se stesso metteremo il codice qui dentro. Se, per caso, il nostro oggetto in scena non deve fare nulla da solo ma aspetta solo stimoli esterni, allora possiamo anche cancellare il metodo Update. Il Core non considera questo un errore.
La nostra prima scena
Possiamo creare una scena iniziale con un cubo sospeso sopra un piano più o meno come visto settimana scorsa.
Adesso creiamo un nuovo script e chiediamo al cubo di ruotare a velocità angolare costante. Basterà usare questo codice.
public class Spin : MonoBehaviour {
public float degreesPerSecond = 45f;
void Update() {
transform.Rotate(transform.up, degreesPerSecond * Time.deltaTime);
}
}
La variabile transform ci viene messa a disposizione dalla classe padre (MonoBehaviour) per darci un riferimento diretto al componente della trasformata dell'oggetto. Usandola possiamo governare posizione, rotazione e scala. Tra i vari metodi della classe Transform (notate la T maiuscola) di cui transform (t minuscola) è un'istanza troviamo anche Rotate. Diamo a Rotate due parametri: un asse di rotazione (in questo caso l'asse verticale dell'oggetto) e il numero di gradi di cui effettuare la rotazione.
Qui incontriamo il primo problema. Update viene chiamata a rotazione senza sosta dal Core. Se abbiamo molti oggetti in scena potrebbe essere chiamata 100 volte al secondo; se ne abbiamo pochi (come in questo caso) qualche migliaia di volte. Per cui, applicare il valore 45 sarebbe sbagliato. Il cubo girerebbe come un pazzo e, comunque, non a velocità costante. Per far diventare il parametro davvero dei "gradi al secondo" dobbiamo moltiplicarlo per il tempo trascorso dall'ultima volta che la nostra Update è stata chiamata. A questo ci pensa Time.deltaTime, che esprime l’intervallo di tempo in secondi.
Una volta finito lo script, lo aggiungiamo all'oggetto trascinandolo con il mouse oppure usando "Add Component" nel pannello dell'Inspector.
Della compilazione se ne occupa Unity in autonomia. Se per caso nello script abbiamo fatto un errore, ci sarà una scritta rossa appena sotto il pannello del database degli asset.
Quando gli errori saranno più di uno, troveremo la lista nel pannello con il titolo “Console”
Se tutto è a posto, facendo partire la scena il risultato dovrebbe essere questo::
Non è il gioco più eccitante del mondo, ma almeno è un primo segno di vita digitale.
Se guardiamo nell’inspector, vediamo che il nostro componente ha un suo modulo nell’interfaccia con campo numerico che corrisponde al campo pubblico della classe C#.
Possiamo cambiare il valore del campo anche durante l'esecuzione per trovare la velocità che più ci piace. Attenzione però a segnare il numero, perché quando fermiamo la simulazione il campo tornerà al valore iniziale. Se invece lo cambiamo prima dell'esecuzione, il valore sarà fissato e Unity se lo ricorderà. Possiamo scrivere 90, il fatto che nello script c'è scritto 45 non è influente.
Stimoli dall’esterno
Proviamo adesso a fare una scena un po' più complicata. Il cubo non ci piace se rimane a mezz'aria e vorremmo cadesse e che nel momento in cui tocca il piano, rimbalza.
Innanzitutto, per far cadere il cubo, ci serve applicare le regole della fisica. Questo si fa semplicemente aggiungendo il componente Rigidbody al cubo. Bisogna farlo con "Add Component" dall'Inspector.
Per far rimbalzare il cubo, abbiamo bisogno di un nuovo script. Stavolta non ci interessa più che lo stato venga aggiornato periodicamente, vogliamo che reagisca all'impatto con il piano.
Se andiamo a vedere nella documentazione della classe MonoBehaviour, scopriamo che c'è un metodo che fa per noi e che si chiama OnCollisionEnter.
Dentro OnCollisionEnter dobbiamo chiedere all’oggetto un riferimento al componente della fisica da usare per applicare una forza verso l’alto al cubo. Il codice è più o meno così:
public class PushUp : MonoBehaviour {
public float kick = 300f;
private void OnCollisionEnter(Collision collision) {
GetComponent<Rigidbody>().AddForce(transform.up * kick);
}
}
AddForce accetta come parametro un vettore la cui norma è l’intensità della spinta. Siccome transform.up è un vettore unitario, serve applicare un fattore di scala moltiplicandolo per un numero.
Il risultato, però, non è per nulla quello che ci saremmo aspettati. E qui, immagino, qualcuno comincerà a capire il senso della copertina di oggi.
Questo comportamento ha due motivazioni.
Innanzitutto, il calcolo della fisica introduce delle approssimazioni per poter risparmiare sull'uso della CPU. Questo fa sì che il rimbalzo non sia verticale ma deviato lateralmente; il rimbalzo e l'impatto successivo fanno il resto. La soluzione più semplice è andare nelle configurazioni di RigidBody e vincolare le posizioni x e z dell’oggetto. Si, è un po' barare, ma l'importante è il risultato.
Secondo, la spinta verso l'alto agisce in riferimento al cubo. Pertanto, se il cubo ruota, finiremo a spingerlo lateralmente o addirittura verso il basso. Possiamo applicare una forza verso l'alto in senso assoluto (utilizzando Vector3.up), oppure, preferibilmente, usare l'orientamento del piano. Basta ricavare la trasformata dell'oggetto con cui è avvenuta la collisione dai parametri del metodo. Questa seconda opzione è più versatile, poiché permette di avere anche un piano inclinato con il cubo rimbalza mentre scivola a valle.
private void OnCollisionEnter(Collision collision) {
GetComponent<Rigidbody>().AddForce(collision.transform.up * kick);
}
Combinazione di componenti
Possiamo aggiungere tutti i componenti vogliamo a ogni oggetto della scena; verranno usati tutti senza distinzione.
Il mio suggerimento è sempre quello di creare script brevi e specializzati nel risolvere un solo problema. In questo modo, state scrivendo codice più facile da verificare e riutilizzare.
Se, ad esempio, volessi aggiungere un effetto particellare al cubo, per cui quando tocca terra si solleva anche una nuvola di polvere, lo farei con due script. Uno si occuperebbe solo di spingere verso l'alto e l'altro unicamente di generare il fumo.
Se due script devono comunicare tra loro, non è un problema. Basta usare GetComponent specificando tipo dell’altra classe e, una volta che abbiamo il suo riferimento, possiamo modificare i valori dei campi e chiamare tutti i metodi pubblici.
Interazione con il giocatore
Anche a questo ci pensa MonoBehaviour. Non abbiamo dei metodi che vengono chiamati nel momento in cui il giocatore preme un tasto o muove il mouse, ma abbiamo i metodi forniti dalla classe Input per monitorare cosa sta facendo l’utente.
Ad esempio, Input.GetAxis ci permette di capire se c’è una richiesta di spostarsi orizzontalmente o verticalmente, indipendentemente dal fatto che stiamo usando la tastiera, un controller o altro. L’unica scomodità è che la classe Input va usata dall’interno del metodo Update. Il codice, però, non è molto complesso.
public float speed = 0.025f;
void Update() {
float xMov = Input.GetAxis("Horizontal");
float zMov = Input.GetAxis("Vertical");
transform.position += new Vector3(xMov * speed, 0f, zMov * speed);
}
Questo script ci permette di controllare la posizione del nostro cubo mentre rimbalza. È vero che il componente che gestisce la fisica non può cambiare la posizione dell’oggetto sul piano orizzontale (detto anche trasverso) ma gli altri componenti si.
La composizione di oggetti
Spostare e far rimbalzare forme geometriche sono tra le prime cose che si imparano quando si inizia a programmare con Unity. Tuttavia, c'è un aspetto a cui bisogna prestare molta attenzione.
Nel momento in cui componiamo tra loro degli oggetti, creiamo delle gerarchie ad albero. In queste gerarchie, le trasformate vengono applicate dal sistema grafico partendo dalle foglie e andando verso la radice. Questo significa che la radice determina la posizione dell'oggetto nella scena, mentre le trasformate delle foglie definiscono la loro posizione rispetto alla radice.
Molto spesso accade che uno script fatto con poca cura funzioni correttamente solo nelle foglie o solo a nella radice. Se il movimento che osservate non è quello che vi aspettate o se cambia in base all’oggetto al quale applicate lo script, fatevi venire il dubbio!
Questo meccanismo può anche essere sfruttato a nostro vantaggio. Prendiamo ad esempio il cubo che rimbalza e spostiamolo lateralmente. Al centro della scena inseriamo un oggetto vuoto (lo trovate nel menu in alto). Un oggetto vuoto non ha componenti a parte la trasformata, per cui occupa un punto nello spazio ma non ha una visualizzazione grafica.. Successivamente, rendiamo il cubo un figlio dell'oggetto vuoto. Infine, all'oggetto vuoto aggiungiamo lo script di rotazione (lo stesso che abbiamo visto all'inizio)
Nell’immagine ho chiamato l’oggetto vuoto Pivot per ricordarmi a cosa serve. Il nome non ha nessun effetto sulla scena.
Il risultato della scena sarà un cubo che salta sul posto relativamente al pivot. Il pivot però gira attorno all’asse verticale. Nella scena, quindi, sembrerà che ci sia un cubo che rimbalza seguendo un percorso circolare.
Un universo tutto da scoprire
Oggi ho scritto molto e vi chiedo scusa; c'erano tante cose da dire. Quello che abbiamo visto, però, è solo il punto di ingresso in un vasto universo tutto da scoprire e in continua evoluzione. Pezzo per pezzo, lo scopriremo insieme con questa newsletter.
Adesso che abbiamo gettato le basi per capirci un po' meglio su Unity, possiamo andare avanti e parlare di molto altro. La settimana prossima, però, risponderò a una domanda che mi è stata fatta da uno di voi. Per ora non vi dico nulla, ma farò come al solito una piccola anticipazione su mio profilo Instagram in settimana.
Ci sentiamo tra sette giorni. E per chi non è ancora iscritto alla newsletter questo è un ottimo momento per porvi rimedio con il pulsante qui sotto, per ricevere tutte le prossime uscite direttamente via mail.
Happy hacking!