Monobehaviour è la classe dalla quale ereditano tutti i componenti dei nostri giochi. È fondamentale parlare di questa classe, poiché ci permette di associare a ciascun oggetto sulla scena, alcuni comportamenti particolari al verificarsi di certe condizioni.
Le condizioni sono rappresentate dai metodi della classe Monobehaviour sotto forma di eventi, cui ogni oggetto può reagire.
Prendiamo ad esempio un evento come MonoBehaviour.OnCollisionEnter()
: questo evento si verifica ogni volta che un oggetto entra in collisione e ogni volta che accade Unity richiama automaticamente il metodo OnCollisionEnter
.
A questo punto perché il nostro oggetto reagisca a eventi come questo, bisognerà assegnare ad essi una funzione, nella quale avremo scritto il comportamento del nostro oggetto, ovvero le istruzioni necessarie a reagire all'evento.
Le funzioni che servono a gestire gli eventi tipicamente si chiamano event handler ma in Unity prendono il nome funzioni evento, per semplicità.
In questa lezione vedremo alcune delle funzioni evento della classe Monobehaviour, che ci permettono di controllare il flusso del gioco.
Utilizzare le funzioni evento
Come abbiamo detto, Monobehaviour espone una serie di funzioni evento che, se inserite in uno script, vengono chiamate automaticamente da Unity al verificarsi di determinate condizioni.
Ad ogni frame, Unity scorre tutti gli script attivi in scena, e se trova una di queste funzioni predefinite, la chiama (passando quindi il controllo alla funzione). Al termine dell'esecuzione, il controllo viene restituito a Unity.
Sarà necessario rispettare in modo preciso i nomi degli eventi, quando definiamo una funzione evento, altrimenti essa non sarà richiamata, anche se lo script non restituirà errori. Ad esempio, un metodo di nome Start
sarà eseguito in automatico alla creazione di un GameObject, ma se noi definiamo Starting
, otterremo una semplice funzione che sarà chiamata solo se saremo noi a farlo esplicitamente.
Purtroppo per queste funzioni non sempre abbiamo il supporto dell'autocompletamento dell'ambiente di sviluppo, quindi si puà sbagliare facilmente un nome, col rischio poi di perdere tempo a cercare di capire perché quella funzione non fa ciò che ci aspettiamo, quando in realtà non viene proprio chiamata perché Unity la vede come un’altra funzione.
Alcuni di questi metodi, possono opzionalmente venire chiamati con dei parametri. Ad esempio OnApplicationFocus viene chiamato sia quando il gioco riceve il focus (specialmente utile nel caso di giochi nel browser, per sapere quando l'utente ha cliccato nell'area del gioco) che quando lo perde (se l'utente digita Alt-Tab
o riduce ad icona il gioco, ad esempio). In questo caso, basta dichiarare il metodo così:
void OnApplicationFocus(bool focus){
}
In questo modo, la variabile booleana focus
ci permette di sapere se il focus è stato guadagnato o perso, e di agire di conseguenza.
Forzare le funzioni evento
Questi metodi sono definiti con visibilità private
per default, tuttavia è possibile anche dichiararli public
. Così facendo, possiamo richiamarli comodamente da altri script, forzando un comportamento anche se non si verifica una condizione di gioco. Dichiararli public
non influisce sul normale comportamento, quindi Unity continuerà anche a richiamarli in maniera predefinita (quindi è possibile che in un certo frame uno di questi metodi venga chiamato due volte, una da noi ed una da Unity).
Le funzioni Awake e Start
I metodi Awake e Start vengono chiamati una sola volta all'inizio del ciclo di vita di un oggetto. La differenza fra i due sta nel fatto che Awake viene chiamato molto presto, durante la preparazione della scena. In quel momento gli oggetti in scena non sono completamente valorizzati, quindi in Awake è sconsigliato svolgere operazioni che richiedono i valori di altri oggetti, come ad esempio leggere il tag di un altro oggetto in scena.
Start è utile per eseguire operazioni di preparazione al gioco, come la creazione di array che verranno riempiti in seguito, o la ricerca di elementi nella scena di cui vogliamo salvare un riferimento per poi lavorarci più avanti durante l'esecuzione.
Le grosse differenze fra Awake e Start sono due:
- Awake viene sempre chiamato prima di Start: se ad esempio in uno script su di un oggetto inseriamo una funzione Awake ed in un altro una Start, saremo sicuri che quando Start verrà chiamato l’Awake sarà già stato eseguito;
- mentre Awake viene sempre eseguita al caricamento di un oggetto, Start viene eseguita solo se lo script è attivo (mediante la checkbox nell'Inspector vicino al suo nome).
Per questo motivo, uno script con solo Awake e senza Start di solito non ha la checkbox vicino al nome, perché di fatto non esiste il concetto di attivo o disattivo: sia che fosse attivo che disattivo, Awake verrebbe eseguito comunque su di esso.
Attivare e disattivare l'oggetto su cui risiede lo script crea una serie di casi interessanti. Eccoli nel dettaglio:
Stato GameObject |
Stato script |
Conseguenza |
---|---|---|
attivo | abilitato | viene chiamato prima Awake, poi Start |
attivo | disabilitato | viene chiamato solo Awake. Se successivamente l'oggetto viene attivato, verrebbe chiamato anche Start (ma non di nuovo Awake) |
disattivo | abilitato o disabilitato |
né Awake né Start vengono chiamati. Se il gameObject viene attivato, passiamo nelle condizioni precedenti. |
In ogni caso, Unity non chiamerà mai Awake e Start più di una volta per uno nell'intero ciclo di vita di uno script.
Update
Il metodo Update viene chiamato ad ogni frame, il che lo rende dipendente dal framerate (vedi sotto) subito prima che venga eseguito il rendering.
Per questo motivo, spesso in Update
si aggiorna la posizione degli oggetti che si muovono (mediante operazioni sui loro componenti Transform
e/o Rigidbody
), in modo che vengano renderizzati nella nuova posizione dando al giocatore l'illusione del movimento.
Nell'Update
di solito si eseguono anche controlli sull'input del giocatore. Ad esempio, molte funzioni della classe Input
sono pensate per leggere gli input del giocatore in quel dato frame.
È importante comprendere che tutto ciò che è contenuto nell'Update viene eseguito completamente nello spazio di un frame, prima che il giocatore possa vedere alcunché. Per questo motivo, non si può implementare ad esempio un fade con un semplice loop for nel quale ad ogni ciclo viene diminuita l'alpha dell'oggetto.
void Update()
{
for (float f = 1f; f <= 0; f -= 0.1f)
{
// Diminuisci l'alpha di un po'
}
}
Il loop verrebbe eseguito interamente nello spazio di un frame, e solo dopo l'oggetto verrebbe renderizzato, quindi l'utente vedrebbe scomparire l'oggetto istantaneamente. Per ottenere l'effetto voluto nel tempo ci sono altri metodi (come le Coroutine) che vedremo più avanti.
Time.deltaTime
Il metodo Update
viene usato in Unity per creare quello che viene definito game loop: una ripetizione ciclica di controlli e di operazioni, che avviene una volta per frame e che determina cosa avviene in scena. Questo introduce un concetto molto importante: in quasi tutti i giochi, il framerate non è fisso, ma variabile. Questo vuol dire che non sapremo mai quanti frame ci sono in un secondo prima che il gioco venga eseguito, e quindi tutte le operazioni che dipendono dal tempo vanno in qualche modo adattate al framerate attuale.
Per fare un esempio, se un oggetto si deve muovere di 10 unità in 10 secondi, non potremo dargli una velocità fissa ad ogni frame. Se così facessimo, ipotizzando un framerate di 50 frame al secondo, verrebbe:
10 unità / 5 secondi / 50 frame = 0,04 unità per frame
Se per qualche motivo il framerate dovesse calare durante quei cinque secondi, l'oggetto non riuscirebbe a spostarsi di 10 unità in quel tempo. Ad esempio, in un secondo da 30 fotogrammi, l'oggetto percorrerebbe solo 1,2 unità invece di due (perché 30 x 0,04 = 1,2), rimanendo indietro di 0,8 unità rispetto al nostro piano
.
Per questo motivo, tutti gli spostamenti e le grandezze che devono essere indipendenti dal framerate, si moltiplicano per un valore che equivale al tempo trascorso dal frame precedente. Questo valore si può trovare in una variabile statica della classe Time, e si chiama Time.deltaTime. Questa quantità, se moltiplicata per il movimento, ci restituisce sempre lo stesso valore indipendentemente dal framerate.
Nell'esempio di prima, se volessimo muovere un oggetto di 10 unità in 5 secondi, basterebbe usare:
10 unità / 5 secondi x Time.deltaTime = 2 x Time.deltaTime = X
Quella X
è la velocità in un dato frame. Supponendo un framerate di 50 frame per secondo, Time.deltaTime
varrà 0,02
, quindi X
sarà 0,04
. In un secondo l'oggetto si muoverebbe di 0,04 x 50 frame
, quindi di 2 unità.
Se il framerate dovesse scendere anche a miseri 15 frame al secondo, Time.deltaTime
varrà circa 0,1335
che moltiplicato per 15
fa sempre 2 unità, dandoci quindi una velocità costante indipendente dal framerate.
La variabile deltaTime
non è utile solo per gli spostamenti, ma per tutti quegli incrementi che dipendono dal tempo, e che devono rimanere costanti indipendentemente dal framerate del computer su cui gira il gioco.
FixedUpdate
Simile ad Update, FixedUpdate è una funzione che Unity chiama in automatico ad intervalli regolari che non tengono conto del frame rate (di default FixedUpdate
viene chiamato ogni 0.02
secondi).
FixedUpdate viene chiamato subito prima di fare i calcoli relativi al motore fisico, e per questo motivo viene utilizzato principalmente per effettuare operazioni che riguardano la fisica, come applicare forze ai componenti rigidbody
, o operare con i collider
.
Come è facile intuire, poiché Unity renderizza la vista meno frequentemente di quanto viene chiamato FixedUpdate
, di solito in questa funzione non si effettua alcuna operazione che riguarda la grafica, come spostamento di transform
, cambiamenti di colore, intensità delle luci, ecc. Fare queste operazioni nel FixedUpdate sarebbe uno spreco di risorse: il risultato sarebbe che verrebbero eseguite più volte (nel FixedUpdate
) prima che la scena possa essere renderizzata, ma senza che il giocatore veda effettivamente alcun cambiamento.
Di contro, è utile effettuare calcoli riguardanti la fisica nel FixedUpdate perché in questo modo questi potranno essere quanto più precisi possibile. Un esempio classico è il moto dei proiettili: poiché questi di solito si muovono a grande velocità, è possibile che in un frame un proiettile sia da un lato di un muro, e nel frame successivo sia dall'altro, di fatto trapassandolo perché Unity non rileva nessuna collisione (collisione che sarebbe dovuta avvenire in un frame intermedio, che non c'è).
L'intervallo a cui FixedUpdate viene eseguito può essere cambiato in Edit > Project Settings > Time
, e modificando il Fixed Timestep. Valori minori daranno più precisione alla simulazione fisica, ma caricheranno di più il processore che, su hardware meno potente, potrebbe non eseguire correttamente alcuni calcoli. Di contro, a volte una grande precisione non è necessaria, e si può quindi aumentare il timestep per sforzare di meno il processore.
Altre funzioni evento
Unity ci mette a disposizione molte altre funzioni che verranno chiamate in automatico allo scattare di diversi eventi, descriverle tutte qui sarebbe anche abbastanza inutile, visto che molte sono autoesplicative.
Una lista completa di tutti i metodi predefiniti di Unity è disponibile nella documentazione ufficiale alla pagina della classe Monobehaviour.