[Unity] Programmare in C# – Coroutine
Coroutine di Unity3D in dettaglio
Innanzitutto è essenziale capire che i motori di gioco (come Unity 3D) lavorano su un paradigma “frame based“.
Questo significa che il codice viene eseguito durante ogni frame.
Quando si pensa a questi, è importante capire che è complicato sapere quando i frame sono eseguiti. Non sono eseguiti secondo un ordine ben preciso. Gli intervalli tra i frame potrebbero essere, ad esempio, 0,02632 dopo 0,021167, poi 0,029778 e così via. Negli esempi sono tutti a “circa” 1/50 di secondo, ma sono tutti diversi. In ogni instante, si può chiedere più o meno tempo; e il codice può essere eseguito in qualsiasi momento all’interno del frame.
Tenendo questo a mente, potreste chiedervi: come si accede a questi frame nel codice di Unity?
Molto semplicemente, si usa la funzione Update () o una coroutine. Sono esattamente la stessa cosa: permettono al codice di essere eseguire “un qualcosa ad ogni fotogramma”.
Lo scopo di una coroutine è:
- eseguire un po’ di codice e poi “fermarsi e attendere” fino ad alcuni frame futuri.
Si può aspettare fino al fotogramma successivo, un numero di fotogrammi , oppure un po’ di tempo approssimativo (ad esempio in secondi).
Infatti, si può aspettare “circa un secondo”, il che significa che il programma aspetterà per circa un secondo, e quindi metterà il codice in qualche fotogramma approssimativamente di un secondo. All’interno di quel frame, il codice potrebbe essere eseguito in qualsiasi momento. Lo ribadiamo: non si tratterà esattamente di un secondo. La precisione non esiste, purtroppo, in un motore di gioco.
Per aspettare un frame all’interno di una coroutine:
// do something
yield return null; // wait until next frame
// do something
Per aspettare tre frame:
// do something
yield return null; // wait until three frames from now
yield return null;
yield return null;
// do something
Per attendere circa mezzo secondo:
// do something
yield return new WaitForSeconds (0.5f); // wait for a frame in about .5 seconds
// do something
Far qualcosa ogni singolo fotogramma:
while (true)
{
// do something
yield return null; // wait until the next frame
}
Questo esempio è identico al semplice inserimento di “un qualcosa” all’interno della funzione “Update” di Unity: il codice di “fare qualcosa” verrà eseguito ogni frame.
Un altro esempio
Per questo esempio bisognerà allegare lo script “Ticker” a un oggetto GameObject
. Mentre quell’oggetto di gioco è attivo, verrà eseguito il codice seguente. Bisogna notare che quando lo script blocca la coroutine l’oggetto del gioco diventa inattivo. Questo, di solito, è un aspetto importante per un corretto utilizzo della coroutine.
using UnityEngine;
using System.Collections;
public class Ticker:MonoBehaviour {
void OnEnable()
{
StartCoroutine(TickEverySecond());
}
void OnDisable()
{
StopAllCoroutines();
}
IEnumerator TickEverySecond()
{
var wait = new WaitForSeconds(1f); // REMEMBER: IT IS ONLY APPROXIMATE
while(true)
{
Debug.Log("Tick");
yield return wait; // wait for a frame, about 1 second from now
}
}
}
Coroutine a catena
Le coroutine possono far parte di una “reazione a catena” e attendere l’esecuzione di altre coroutine .
Quindi le coroutine verranno eseguite “una dopo l’altra”.
Questo concetto è molto semplice, ed è una tecnica di base, fondamentale in Unity.
È assolutamente naturale che, nei giochi , certe cose debbano accadere “in ordine”. Quasi ogni “scena” di un gioco inizia con una serie di eventi, in un certo lasso di tempo, in un certo ordine. Ecco come potreste iniziare, ad esempio, un gioco di corse automobilistiche:
IEnumerator BeginRace()
{
yield return StartCoroutine(PrepareRace());
yield return StartCoroutine(Countdown());
yield return StartCoroutine(StartRace());
}
Quindi, quando chiamiamo BeginRace …
StartCoroutine(BeginRace());
Il framework eseguirà prima “PrepareRace“. Forse, facendo un po’ di effetti di luci e rumori di folla, azzerando i punteggi e così via. Quando l’azione sarà finita, Unity eseguirà il “Countdown“, dove, forse, potremo animare un conto alla rovescia sull’interfaccia utente. Al termine, verrà eseguito il codice di partenza della gara, durante la quale si potrebbero aggiungere degli effetti sonori, spostare la telecamera in un determinato modo e così via.
Per chiarezza, è importante capire che le tre funzioni
yield return StartCoroutine(PrepareRace());
yield return StartCoroutine(Countdown());
yield return StartCoroutine(StartRace());
devono essere in una coroutine. Vale a dire, devono essere in una funzione del tipo IEnumerator
. Quindi nel nostro esempio è IEnumerator BeginRace
. Nel codice, l’azione si esegue con la chiamata StartCoroutine
.
StartCoroutine(BeginRace());
Per comprendere meglio il concatenamento, ecco una funzione che esegue tutte le coroutine. Con un ciclo che recupera le coroutine e le avvia. La funzione esegue tutte le coroutine, in ordine, una dopo l’altra.
// run various routines, one after the other
IEnumerator OneAfterTheOther( params IEnumerator[] routines )
{
foreach ( var item in routines )
{
while ( item.MoveNext() ) yield return item.Current;
}
yield break;
}
Diciamo che ci sono tre funzioni. Ricordiamo che devono essere tutte IEnumerator
:
IEnumerator PrepareRace()
{
// codesay, crowd cheering and camera pan around the stadium
yield break;
}
IEnumerator Countdown()
{
// codesay, animate your countdown on UI
yield break;
}
IEnumerator StartRace()
{
// codesay, camera moves and light changes and launch the AIs
yield break;
}
Potremo chiamarle in questo modo:
StartCoroutine( MultipleRoutines( PrepareRace(), Countdown(), StartRace() ) );
o anche così:
IEnumerator[] routines = new IEnumerator[] {
PrepareRace(),
Countdown(),
StartRace() };
StartCoroutine( MultipleRoutines( routines ) );
Per ripetere, uno dei requisiti di base dei giochi è che certe cose accadono una dopo l’altra “in una sequenza” nel tempo. In Unity si fa molto semplicemente, ad esempio in questo modo:
yield return StartCoroutine(PrepareRace());
yield return StartCoroutine(Countdown());
yield return StartCoroutine(StartRace());
Metodi MonoBehaviour che possono essere Coroutine
Ci sono dei metodi in MonoBehaviour che possono includere una coroutine.
- OnBecameVisible ()
- OnLevelWasLoaded ()
Questi vengono utilizzati, ad esempio, per eseguire lo script solo quando l’oggetto è visibile a una telecamera.
using UnityEngine;
using System.Collections;
public class RotateObject : MonoBehaviour
{
IEnumerator OnBecameVisible()
{
var tr = GetComponent<Transform>();
while (true)
{
tr.Rotate(new Vector3(0, 180f * Time.deltaTime));
yield return null;
}
}
void OnBecameInvisible()
{
StopAllCoroutines();
}
}
Modi per cedere
Si può aspettare fino al fotogramma successivo.
yield return null; // wait until sometime in the next frame
È possibile avere più chiamate “in fila”, e attendere semplicemente il numero di frame desiderato.
//wait for a few frames
yield return null;
yield return null;
Attendere circa per “n” secondi. È estremamente importante capire che questo è solo del tempo approssimativo.
yield return new WaitForSeconds(n);
Non è assolutamente possibile utilizzare la chiamata “WaitForSeconds” per qualsiasi forma di sincronizzazione accurata.
Spesso si usa per concatenare delle azioni. Quindi, “fai qualcosa, e quando questo è finito allora fai qualcos’altro, e quando questo è finito fai qualcos’altro“. Per riuscirci, aspetta un’altra coroutine:
yield return StartCoroutine(coroutine);
Si può chiamare una coroutine anche in questo modo:
StartCoroutine(Test());
È da qui si avvia una coroutine con un pezzo di codice “normale”.
Quindi, all’interno della coroutine in esecuzione:
Debug.Log("A");
StartCoroutine(LongProcess());
Debug.Log("B");
Ciò mostrerà A, poi avviando il LongProcess e rimostrerà immediatamente B. L’azione non finirà con LongProcess. D’altro canto:
Debug.Log("A");
yield return StartCoroutine(LongProcess());
Debug.Log("B");
Ciò mostrerà A, avvierà il LongProcess , attenderà fino a quando non sarà finito , e quindi mostrerà B.
Vale sempre la pena ricordare che le coroutine non hanno assolutamente alcuna connessione fra di loro, in alcun modo. Con questo codice:
Debug.Log("A");
StartCoroutine(LongProcess());
Debug.Log("B");
è facile pensare ad un thread in background con l’avvio di LongProcess. Ma è assolutamente scorretto. È solo una coroutine. I motori di gioco sono basati su frame e “coroutines” e, in Unity, permettono semplicemente di accedere ai frame.
È anche molto semplice aspettare che una richiesta web venga completata.
void Start() {
string url = "http://google.com";
WWW www = new WWW(url);
StartCoroutine(WaitForRequest(www));
}
IEnumerator WaitForRequest(WWW www) {
yield return www;
if (www.error == null) {
//use www.data);
}
else {
//use www.error);
}
}
Per completare l’argomento, c’è una funzione WaitForFixedUpdate()
, anche se in realtà non viene mai utilizzata. E’ una funzione specifica ( WaitForEndOfFrame()
nella versione corrente di Unity) che viene usata in determinate situazioni durante lo sviluppo. (Il meccanismo esatto cambia di volta in volta con gli aggiornamenti di Unity, quindi guardate su Google per le ultime informazioni pertinenti).
Terminare una coroutine
Spesso le coroutine possono terminare quando vengono raggiunti determinati obiettivi.
IEnumerator TickFiveSeconds()
{
var wait = new WaitForSeconds(1f);
int counter = 1;
while(counter < 5)
{
Debug.Log("Tick");
counter++;
yield return wait;
}
Debug.Log("I am done ticking");
}
Per stoppare una coroutine da “dentro” la coroutine, non si può semplicemente “tornare indietro” come succede in una funzione ordinaria. Così si usa yield break
.
IEnumerator ShowExplosions()
{
... show basic explosions
if(player.xp < 100) yield break;
... show fancy explosions
}
Si possono anche “forzare” tutte le coroutine lanciate dallo script a fermarsi prima di finire l’azione svolta.
void OnDisable()
{
// Stops all running coroutines
StopAllCoroutines();
}
Se abbiamo avviato una coroutine con una stringa:
StartCoroutine("YourAnimation");
si può stoppare chiamando la funzione StopCoroutine con parametro lo stesso nome della stringa:
StopCoroutine("YourAnimation");
In alternativa:
public class SomeComponent : MonoBehaviour
{
Coroutine routine;
void Start () {
routine = StartCoroutine(YourAnimation());
}
void Update () {
// later, in response to some input...
StopCoroutine(routine);
}
IEnumerator YourAnimation () { /* ... */ }
}