Articoli informativiEspertiImparare C#Imparare UnityTips

Le Coroutine

La Coroutine sono estremamente utili nello sviluppo di un programma, sopratutto se si tratta di un videogioco.

Introduzione alle Coroutine

Quando scriviamo una funzione una dietro l'altra, per esempio dentro lo Start():

void Start(){
MiaFunzione();
ScondaFunzione();
}

Sappiamo che la SecondaFunzione sarà avviata solo quando MiaFunzione sarà completata. E sappiamo che durante l'esecuzione di MiaFunzione il frame sarà "bloccato" per tot millisecondi per permettere a tutte le operazioni all'interno della funzione siano eseguite, prima di procedere con la seconda funzione.
Se MiaFunzione fosse una funzione molto pesante, che impiega molto tempo ad essere eseguita, anche SecondaFunzione dovrà attendere.

Potremmo rendere MiaFunzione una Coroutine a cui "si dà un input" per essere eseguita e poi continuare ad eseguire la successiva senza dover attendere che MiaFunzione sia giunta al termine.

void Start(){

StartCoroutine(MiaFunzione());
ScondaFunzione();
}

In questo modo SecondaFunzione non dovrà attendere il completamento di MiaFunzione.

Questo ci fa intuire che le Coroutine non seguano il normale flusso di Update ma che lavorino parallelamente al normale flusso di programma.
In realtà non è proprio così, ma noi possiamo considerare le Coroutine come un sistema parallelo che lavora in background al normale flusso di aggiornamento del programma.

 

L'esempio di cui sopra non è il solo motivo per cui le Coroutine sono così importanti per il nostro lavoro. Anzi, a dire il vero noi le useremo per tutt'altri motivi.
Nel nostro caso, la vera utilità delle Coroutine sta nel fatto che possiamo far attendere una funzione per un determinato tempo, prima di proseguire, senza bloccare il normale flusso di aggioramento del gioco. Che sia specificato in secondi o fino alla fine di una determinata azione (tipo il download di informaizioni dalla rete o altro) o fino alla fine del frame corrente. Durante questo tempo il gioco non sarà bloccato in attesa della funzione ma continuerà a funzionare normalemente, mentre la Coroutine sta facendo il suo lavoro.

Per dichiarare una Courutine sarà però necessario un tipo di restituzione specifico, vale a dire IEnumerator

 public IEnumerator MiaFunzione()
{ ...codice }

e per essere chiamata, come abbiamo visto, si usa StartCoroutine(  ...  );

StartCoroutine(MiaFunzione());

Avremmo potuto anche usare una sintassi simile, identificando la coroutine tramite stringa:

StartCoroutine("MiaFunzione");

In questo modo, se ne avessimo avuto bisogno. non avremmo potuto inviare parametri.

Le Coroutine sono utili per scrivere istruzioni tipo "aspetta questa riga per un po '".
Tramite l'uso del comando WaitForSeconds la Coroutine farà aspettare il codice in un punto per un determinato tempo da noi impostato.

 

L'uso delle Coroutine

Vediamo in modo pratico come possono tornarci utili le Coroutine con alcuni esempi.

Vogliamo che un gameObject si disabiliti alla pressione del tasto F.
Scriviamo una normale funzione che lo faccia, senza l'uso di Coroutine.

void Disattiva(){
    
gameObject.SetActive(false); //Disabilita il gameObject

}

Poi nell'Update eseguiamo la funzione alla pressione del tasto F.

void Update()
{
    if (Input.GetKeyDown("F")) //Alla pressione del tasto F
    {
        Disattiva(); //Esegui il metodo Disattiva()
    }
}

A questo punto, alla pressione del tasto F, il gameObject in questione verrà disabilitato all'istante.

Ora mettiamo il caso in cui vogliamo che il gameObject sia disabilitato dopo un tot di secondi dalla pressione del tasto F.
Faccimo 2 secondi.
Per fare questo useremo una Coroutine che attende 2 secondi e poi prosegue il suo corso.

IEnumerator Disattiva() 
{

        yield return new WaitForSeconds(2); //Attendi due secondi
        gameObject.SetActive(false); //Disabilita il gameObject
    
}

Andremo a chiamre la funzione in questo modo:

void Update()
{
    if (Input.GetKeyDown("F")) //Alla pressione del tsto F
    {
        StartCoroutine(Disattiva()); //Fai partire la Coroutine Disattiva()
    }
}

 Ora, alla pressione del tasto F, verrà sempre eseguita la funzione Disattiva() ma al suo interno, quando incontrerà la riga
yeld return new WaitForSeconds(2);
la funzione si fermerà ed attenderà i secondi che abbiamo impostato (2).

Se per esempio, avessimo voluto che alla pressione del tasto F il gameObject fosse cambiato di colore, diventando verde alla pressione del tasto rimanendo verde per un secondo, poi rosso per due secondi e poi essere disabilitato:

IEnumerator Disattiva() 
{       
        gameObject.GetComponent<Renderer>().material.color = Color.green; //cambia colore in Giallo
        yield return new WaitForSeconds(1); //attendi un secondo
        gameObject.GetComponent<Renderer>().material.color = Color.red; //cambia colore in Rosso
        yield return new WaitForSeconds(2); //attendi altri due secondi
        gameObject.SetActive(false); //disabilita l'oggetto
    
}

Durante il conteggio di questi secondi, il normale flusso del gioco continua normalmente, come se il lavoro all'interno della Coroutine fosse un processo esterno e parallelo al normale Update.

Esempio 2

Uno degli esempi sulle Coroutine che ci mostra Unity riguarda l'ottimizzazione del codice per rendere l'esecuzione di un gioco più veloce.

Mettiamo di avere una funzione molto pesante, che controlla la distanza di ogni singolo nemico dal giocatore.
Nell'Update, ad ogni frame, dovremmo fare il controllo delle distanze:

    bool ProximityCheck()
    {
        for (int i = 0; i < enemies.Length; i++)
        {
            if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance)
            {
                return true;
            }
        }

        return false;
    }

Il ciclo for prende in esame tutti i nemici nella scena, che potrebbero essere decine o anche centinaia. E per centinaia di volte dovrà essere eseguito il controllo tra due vettori e poi confrontati con la dangerDistance. Durante questo ciclo l'Update dovrà attendere il controllo completo di tutti i nemici prima di proseguire al frame successivo.
Questo graverà pesantemente sulle prestazioni del gioco.

Invece di chiamare questa funzione del normale ciclo di Update (facendo dunque il controllo decine di volte al secondo) potremmo creare un intervallo di tempo in cui fare questo controllo e non necessariamente tanto frequentemente.

IEnumerator DoCheck() 
{
    for(;;) 
    {
        ProximityCheck();
        yield return new WaitForSeconds(.5f);
    }
}

In questo modo il metodo ProximityCheck() verrà eseguito ogni mezzo secondo, dunque due volte al secondo invece che trenta o più a seconda del framerate, alleggerendo molto il carico del processore senza incidere sul gameplay.
Il ciclo for(;;) senza dichiarazioni farà in modo che la Coroutine, una volta eseguita per esempio nello Start(), sarà eseguita costantemente, in questo caso ogni mezzo secondo.

 

Esempio 3

Un altro utilizzo molto utile delle Coroutine è attendere la finalizzazione di una determinata operazione che si preannuncia molto lunga, senza far interrompere il normale flusso di gioco, per poi far fare qualcosa solo al suo termine.

Mettiamo per esempio che il nostro gioco deve scaricare delle risorse dalla rete e che volessimo eseguire una determinata funzione a download completato.
Mettiamo sia un video e che vogliamo sia eseguita la funzione PlayVideo() appena finito il download.
Ovviamente non possiamo sapere quanto tempo impiegherà il download e non possiamo usare WaitForSeconds();
Potremmo usare invece WaitUntil().

IEnumerator DownloadVideo(string url) {
    UnityWebRequest www = UnityWebRequest.Get(url); //Richiesta del file
    yield return www.SendWebRequest(); //Attendi che la richiesta abbia successo
    byte[] result = www.downloadHandler.data; //Scarica i dati e mettili in result
    File.WriteAllBytes(path, result); //Scrivi il file nella cartella stabilita in "path"
    yield return new WaitUntil(() => File.Exists(path)); //Attendi affinchè il file non esista nella cartella
    PlayVideo(path); //Manda in Play il video
}

In questo caso abbiamo due yield return che non fanno altro che bloccare l'esecuzione del codice fino a che non sia soddisfatta una condizione.
Nal primo caso,
yield return www.SendWebRequest();
ferma l'esecusione della coroutine fino a che la richiesta di comunicazione non sia stata accettata.
Nel secondo caso
yield return new WaitUntil(() => File.Exists(path));
il codice viene bloccato fino a che File.Exixts(path) non risulti vera, cioè che esista quello specifico file in quel path (percorso di cartelle). Quando il file è stato creato allora esso può andare in Play.

Mettiamo invece di voler caricare un'immagine, che sia dalla rete o dall'hard disk, per poi impostarla sulla UI o come texture per un modello. Questa operazione è piuttosto lunga e non possiamo far bloccare il gioco fino a che la risorsa non sia stata ottenuta.
Usiamo dunque una Coroutine che lavori in background mentre il gioco continua a funzionare normalmente.

    Texture2D tex; //La mia texture, inzialmente vuota
    IEnumerator GetTexture()
    {
        WWW myTextureReader= new WWW(pathWithPrefix); //Prendi l'immagine dal path indicato
        yield return myTextureReader; //Attendi fino a che myTexture non sia completa
     
        tex = myTextureReader.texture;
    }

yeld return myTextureReader significa  il codice attenderà fino a che myTextureReader non sarà acquisita.

"Una Coroutine è come una funzione che ha la capacità di mettere in pausa l'esecuzione di se stessa e restituire il controllo al normale flusso di codice, ma poi di riprendere da dove era stata interrotta nel frame seguente."
Approfondimento

Come abbiamo detto, in realtà le Coroutine non lavorano su un processo diverso da quello principale ma simulano questo comportamento.

Coroutine è una classe di Unity mentre IEnumerator appartiene a .NET.

Già da Unity 5.3 Unity ha aggiunto alcune funzionalità alla classe Coroutine che sono WaitUntil, WaitWhile. Queste funzionalità possono essere utilizzate solo con l'istruzione Yield. Abbiamo già visto WaitUntil.

WaitUntil sospende l'esecuzione fino a quando la condizione data è vera.
WaitWhile sospende l'esecuzione fino a quando la condizione specificata rimane falsa.

Quando si scorre attraverso un array o si accede a un file di grandi dimensioni, in alternativa che l'intera azione interrompa tutti gli altri processi, IEnumerator consente di interrompere il processo in un momento specifico, restituire quella parte di oggetto (o null) e tornare a quel punto ogni volta che sarà necessario. Questo fino a quando il metodo MoveNext di IEnumerator non restituisce false indicando che abbiamo raggiunto la fine dell'array/file.

Immagina di avere un effetto visivo da eseguire ciclicamente ogni pochi minuti.
Se lo gestissimo in Update, come potremmo gestirete l'esecuzione quando non dovrà fare nulla? Verrà chiamato Update anche se non deve fare nulla.
In alternativa possiamo usare alcuni booleani come "isPlaying"? Ma il problema principale è che Update continuerà inutilmente a controllare sempre, anche dopo che l'effetto desiderato sarà terminato.
Sarebbe come chiamare qualcuno continuamente sapendo che nessuno dovrebbe rispondere! Spreco di energia. E in questo caso di risorse per i calcoli.

Le coroutine hanno solo un piccolo sovraccarico e possono essere preferite rispetto a un metodo di aggiornamento che viene chiamato continuamente ed inutilmente.
Ma attenzione, una Coroutine non è un sostituto dell'Update!

L'uso delle Coroutine va comunque dosato con parsimonia perché utilizzano più risorse di un nomale flusso di aggiornamento.

 

Mettiamo di avere una funzione che chiameremo GrandiCalcoli. Al suo interno abbiamo 1000 cicli e all'interno di ogni ciclo altri 1000 cicli che  a loro volta fanno un controllo, tipo su quale numero sia più alto tra i due inviati.

void GrandiCalcoli()
{
    for (int i = 0; i < 1000; i++)
    {
        for (int j = 0; j < 1000; j++)
        { 
            FaiUnControllo(i,j);
        }
    }
}

Davvero tanta roba da fare in un unico frame. Se usassimo questa funzione nel normale ciclo di Update, le prestazioni ne risentirebbero pesantemente.

Se invece usassimo una Coroutine, ad ogni songolo frame verrà eseguito un solo il primo ciclo for (i =0 ...
A
l prossimo frame i sarà =1 e così via, frame dopo frame.

IEnumerator GrandiCalcoli()
{
    for (int i = 0; i < 1000; i++)
    {
        for (int j = 0; j < 1000; j++)
        { 
            FaiUnControllo(i,j);
        }
        yield return 0;
    }
}
In pratica la Coroutine "centellina" i controlli del ciclo for eseguendone uno per frame e non tutti in un unico frame.
Questo può significare un esecuzione che impiegherà più tempo per giungere a termine, ma senza affaticare l'esecuzione del programma.

Un altro degli usi più importanti delle Coroutine è anche quello che implica l'uso asincrono dei dati come il login su vari servizi in rete.

IEnumerator LoginToFacebook()
{
    FB.Login(); //Esegui il Login
    while (!FB.isLoggedIn()) //Finchè non si è loggati
    {
        yield return null; //Aspetta
    }
    SomeProccessOnLogin(); //Adesso fai ciò che devi fare dopo il Login
}

 

Un pensiero su “Le Coroutine

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *