Come funziona davvero StartCoroutine / rendimento modello in Unity?


134

Capisco il principio delle coroutine. So come far funzionare lo standard StartCoroutine/ yield returnpattern in C # in Unity, ad es. Invocare un metodo che ritorna IEnumeratorvia StartCoroutinee in quel metodo fare qualcosa, fare yield return new WaitForSeconds(1);aspettare un secondo, quindi fare qualcos'altro.

La mia domanda è: cosa sta succedendo davvero dietro le quinte? Cosa fa StartCoroutineveramente? Cosa IEnumeratorsta WaitForSecondstornando? In che modo StartCoroutinerestituisce il controllo alla parte "qualcos'altro" del metodo chiamato? In che modo tutto ciò interagisce con il modello di concorrenza di Unity (in cui molte cose accadono contemporaneamente senza l'uso di coroutine)?


3
Il compilatore C # trasforma i metodi che restituiscono IEnumerator/ IEnumerable(o gli equivalenti generici) e che contengono la yieldparola chiave. Cerca iteratori.
Damien_The_Unbeliever,

4
Un iteratore è un'astrazione molto conveniente per una "macchina a stati". Comprendilo prima e otterrai anche le coroutine Unity. en.wikipedia.org/wiki/State_machine
Hans Passant,

2
Il tag unity è riservato da Microsoft Unity. Per favore, non abusarne.
Lex Li,

11
Ho trovato questo articolo abbastanza illuminante: coroutine Unity3D in dettaglio
Kay

5
@Kay - Vorrei poterti comprare una birra. Quell'articolo è esattamente quello di cui avevo bisogno. Stavo iniziando a mettere in dubbio la mia sanità mentale poiché sembrava che la mia domanda non avesse nemmeno senso, ma l'articolo risponde direttamente alla mia domanda meglio di quanto potessi immaginare. Forse puoi aggiungere una risposta con questo link che posso accettare, a beneficio dei futuri utenti SO?
Ghopper21,

Risposte:


109

Il link coroutine Unity3D spesso menzionato nel dettaglio è morto. Dal momento che è menzionato nei commenti e nelle risposte, pubblicherò qui i contenuti dell'articolo. Questo contenuto proviene da questo mirror .


Coroutine Unity3D in dettaglio

Molti processi nei giochi avvengono nel corso di più frame. Hai processi 'densi', come il pathfinding, che lavorano duramente ogni fotogramma ma vengono suddivisi su più fotogrammi in modo da non influire troppo sul framerate. Hai processi "sparsi", come i trigger di gioco, che non fanno nulla nella maggior parte dei frame, ma a volte sono chiamati a fare un lavoro critico. E hai processi assortiti tra i due.

Ogni volta che stai creando un processo che si svolgerà su più frame, senza il multithreading, devi trovare un modo per suddividere il lavoro in blocchi che possono essere eseguiti uno per frame. Per qualsiasi algoritmo con un ciclo centrale, è abbastanza ovvio: un pathfinder A *, ad esempio, può essere strutturato in modo tale da mantenere i suoi elenchi di nodi semi-permanentemente, elaborando solo una manciata di nodi dall'elenco aperto ogni frame, invece di provare per fare tutto il lavoro in una volta sola. C'è qualche bilanciamento da fare per gestire la latenza: dopo tutto, se stai bloccando il tuo framerate a 60 o 30 fotogrammi al secondo, il tuo processo prenderà solo 60 o 30 passi al secondo, e ciò potrebbe causare solo il processo troppo lungo nel complesso. Un design pulito potrebbe offrire la più piccola unità di lavoro possibile ad un livello - ad es elaborare un singolo nodo A * e sovrapporre un modo per raggruppare il lavoro in blocchi più grandi, ad esempio continuare a elaborare i nodi A * per X millisecondi. (Alcune persone lo chiamano "timeslicing", anche se io non lo faccio).

Tuttavia, consentire al lavoro di essere suddiviso in questo modo significa che è necessario trasferire lo stato da un fotogramma al successivo. Se stai rompendo un algoritmo iterativo, devi preservare tutto lo stato condiviso tra le iterazioni, oltre a un mezzo per tracciare quale iterazione dovrà essere eseguita successivamente. Di solito non è così male - il design di una 'classe A * Pathfinder' è abbastanza ovvio - ma ci sono anche altri casi che sono meno piacevoli. A volte dovrai affrontare lunghi calcoli che eseguono diversi tipi di lavoro da un frame all'altro; l'oggetto che acquisisce il loro stato può finire con un gran casino di "locali" semi utili, conservati per il passaggio di dati da un fotogramma all'altro. E se hai a che fare con un processo scarso, spesso finisci per dover implementare una piccola macchina a stati solo per tenere traccia del lavoro da svolgere.

Non sarebbe bello se, invece di dover tracciare esplicitamente tutto questo stato su più frame e invece di dover eseguire il multithreading e gestire la sincronizzazione e il blocco e così via, potresti semplicemente scrivere la tua funzione come un singolo blocco di codice, e segnare luoghi particolari in cui la funzione dovrebbe "mettere in pausa" e proseguire in un secondo momento?

L'unità - insieme a una serie di altri ambienti e linguaggi - fornisce questo sotto forma di Coroutine.

Come sembrano? In "Unityscript" (Javascript):

function LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield;
    }
}

In C #:

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield return null;
    }
}

Come funzionano? Consentitemi di dire rapidamente che non lavoro per Unity Technologies. Non ho visto il codice sorgente di Unity. Non ho mai visto il fegato del motore coroutine di Unity. Tuttavia, se l'hanno implementato in un modo radicalmente diverso da quello che sto per descrivere, sarò piuttosto sorpreso. Se qualcuno di UT vuole entrare in contatto e parlare di come funziona effettivamente, sarebbe fantastico.

I grandi indizi sono nella versione C #. Innanzitutto, notare che il tipo restituito per la funzione è IEnumerator. E in secondo luogo, nota che una delle dichiarazioni è il rendimento. Ciò significa che rendimento deve essere una parola chiave e poiché il supporto C # di Unity è C # 3.5 vanilla, deve essere una parola chiave C # 3.5 vanilla. In effetti, eccolo qui in MSDN - parla di qualcosa chiamato "blocchi iteratori". Quindi cosa sta succedendo?

Innanzitutto, esiste questo tipo IEnumerator. Il tipo IEnumerator si comporta come un cursore su una sequenza, fornendo due membri significativi: Current, che è una proprietà che fornisce l'elemento su cui si trova attualmente il cursore, e MoveNext (), una funzione che si sposta sull'elemento successivo nella sequenza. Poiché IEnumerator è un'interfaccia, non specifica esattamente come vengono implementati questi membri; MoveNext () potrebbe semplicemente aggiungerne uno a Current, oppure caricare il nuovo valore da un file oppure scaricare un'immagine da Internet e hash e archiviare il nuovo hash in Current ... oppure potrebbe anche fare una cosa per la prima elemento nella sequenza e qualcosa di completamente diverso per il secondo. Potresti anche usarlo per generare una sequenza infinita se lo desideri. MoveNext () calcola il valore successivo nella sequenza (restituendo false se non ci sono più valori),

Di solito, se si desidera implementare un'interfaccia, è necessario scrivere una classe, implementare i membri e così via. I blocchi Iterator sono un modo conveniente per implementare IEnumerator senza tutta quella seccatura: basta seguire alcune regole e l'implementazione di IEnumerator viene generata automaticamente dal compilatore.

Un blocco iteratore è una funzione regolare che (a) restituisce IEnumerator e (b) utilizza la parola chiave yield. Quindi cosa fa effettivamente la parola chiave yield? Dichiara quale sarà il valore successivo nella sequenza o che non ci sono più valori. Il punto in cui il codice incontra una resa return X o una rottura della resa è il punto in cui IEnumerator.MoveNext () dovrebbe fermarsi; un rendimento return X fa sì che MoveNext () restituisca true e Current sia assegnato il valore X, mentre un'interruzione del rendimento fa sì che MoveNext () restituisca false.

Ora, ecco il trucco. Non deve importare quali siano i valori effettivi restituiti dalla sequenza. È possibile chiamare MoveNext () ripetutamente e ignorare Current; i calcoli verranno comunque eseguiti. Ogni volta che viene chiamato MoveNext (), il blocco iteratore viene eseguito sull'istruzione 'yield' successiva, indipendentemente dall'espressione effettivamente prodotta. Quindi puoi scrivere qualcosa del tipo:

IEnumerator TellMeASecret()
{
  PlayAnimation("LeanInConspiratorially");
  while(playingAnimation)
    yield return null;

  Say("I stole the cookie from the cookie jar!");
  while(speaking)
    yield return null;

  PlayAnimation("LeanOutRelieved");
  while(playingAnimation)
    yield return null;
}

e quello che hai effettivamente scritto è un blocco iteratore che genera una lunga sequenza di valori null, ma ciò che è significativo sono gli effetti collaterali del lavoro che fa per calcolarli. È possibile eseguire questo coroutine utilizzando un semplice ciclo come questo:

IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }

O, più utilmente, potresti mescolarlo con altri lavori:

IEnumerator e = TellMeASecret();
while(e.MoveNext()) 
{ 
  // If they press 'Escape', skip the cutscene
  if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}

È tutto nella tempistica Come hai visto, ogni istruzione return return deve fornire un'espressione (come null) in modo che il blocco iteratore abbia qualcosa da assegnare effettivamente a IEnumerator.Current. Una lunga sequenza di null non è esattamente utile, ma siamo più interessati agli effetti collaterali. No?

In realtà c'è qualcosa di utile che possiamo fare con quell'espressione. E se invece di cedere nulla e ignorarlo, avessimo prodotto qualcosa che indicava quando ci aspettavamo di dover fare più lavoro? Spesso dovremo continuare direttamente con il fotogramma successivo, certo, ma non sempre: ci saranno molte volte in cui vogliamo continuare dopo che un'animazione o un suono ha terminato la riproduzione o dopo che è trascorso un determinato periodo di tempo. Quelli mentre (playingAnimation) restituiscono null; i costrutti sono un po 'noiosi, non credi?

Unity dichiara il tipo di base YieldInstruction e fornisce alcuni tipi derivati ​​concreti che indicano particolari tipi di attesa. Hai WaitForSeconds, che riprende la routine dopo che è trascorso il tempo designato. Hai WaitForEndOfFrame, che riprende la routine in un determinato punto più tardi nello stesso frame. Hai il tipo Coroutine stesso, che, quando la Coroutine A produce la Coroutine B, mette in pausa la Coroutine A fino al termine della Coroutine B.

Come appare dal punto di vista del runtime? Come ho detto, non lavoro per Unity, quindi non ho mai visto il loro codice; ma immagino che potrebbe apparire un po 'così:

List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;

foreach(IEnumerator coroutine in unblockedCoroutines)
{
    if(!coroutine.MoveNext())
        // This coroutine has finished
        continue;

    if(!coroutine.Current is YieldInstruction)
    {
        // This coroutine yielded null, or some other value we don't understand; run it next frame.
        shouldRunNextFrame.Add(coroutine);
        continue;
    }

    if(coroutine.Current is WaitForSeconds)
    {
        WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
        shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
    }
    else if(coroutine.Current is WaitForEndOfFrame)
    {
        shouldRunAtEndOfFrame.Add(coroutine);
    }
    else /* similar stuff for other YieldInstruction subtypes */
}

unblockedCoroutines = shouldRunNextFrame;

Non è difficile immaginare come si possano aggiungere altri sottotipi di YieldInstruction per gestire altri casi: potrebbe essere aggiunto il supporto a livello di motore per i segnali, ad esempio, con un YieldInstruction WaitForSignal ("SignalName") che lo supporta. Aggiungendo più YieldInstructions, le stesse coroutine possono diventare più espressive: rendimento del rendimento nuovo WaitForSignal ("GameOver") è più piacevole da leggere del resto (! Signals.HasFired ("GameOver")) rendimento return null, se me lo chiedi, a parte il fatto che farlo nel motore potrebbe essere più veloce che farlo nello script.

Un paio di ramificazioni non ovvie Ci sono un paio di cose utili su tutto ciò che a volte le persone mancano e che pensavo di dover sottolineare.

In primo luogo, il rendimento restituito sta semplicemente producendo un'espressione - qualsiasi espressione - e YieldInstruction è un tipo regolare. Ciò significa che puoi fare cose come:

YieldInstruction y;

if(something)
 y = null;
else if(somethingElse)
 y = new WaitForEndOfFrame();
else
 y = new WaitForSeconds(1.0f);

yield return y;

Le righe specifiche offrono return new WaitForSeconds (), yield return new WaitForEndOfFrame (), ecc., Sono comuni, ma in realtà non sono forme speciali a sé stanti.

In secondo luogo, poiché queste coroutine sono solo blocchi di iteratori, puoi iterarli su di te se vuoi - non devi avere il motore che lo fa per te. Ho usato questo per aggiungere condizioni di interruzione a un coroutine prima:

IEnumerator DoSomething()
{
  /* ... */
}

IEnumerator DoSomethingUnlessInterrupted()
{
  IEnumerator e = DoSomething();
  bool interrupted = false;
  while(!interrupted)
  {
    e.MoveNext();
    yield return e.Current;
    interrupted = HasBeenInterrupted();
  }
}

In terzo luogo, il fatto che tu possa cedere su altre coroutine può in qualche modo permetterti di implementare le tue YieldInstructions, anche se non in modo altrettanto performante come se fossero state implementate dal motore. Per esempio:

IEnumerator UntilTrueCoroutine(Func fn)
{
   while(!fn()) yield return null;
}

Coroutine UntilTrue(Func fn)
{
  return StartCoroutine(UntilTrueCoroutine(fn));
}

IEnumerator SomeTask()
{
  /* ... */
  yield return UntilTrue(() => _lives < 3);
  /* ... */
}

tuttavia, non lo consiglierei davvero: il costo per iniziare una Coroutine è un po 'pesante per i miei gusti.

Conclusione Spero che questo chiarisca un po 'di ciò che sta realmente accadendo quando usi un Coroutine in Unity. I blocchi di iteratori di C # sono un piccolo costrutto groovy e anche se non stai usando Unity, forse ti sarà utile sfruttarli allo stesso modo.


2
Grazie per averlo riprodotto qui. È eccellente e mi ha aiutato in modo significativo.
Naikrovek,

96

La prima intestazione di seguito è una risposta diretta alla domanda. Le due voci successive sono più utili per il programmatore quotidiano.

Possibilmente noiosi dettagli di implementazione delle coroutine

Le coroutine sono spiegate in Wikipedia e altrove. Qui fornirò solo alcuni dettagli da un punto di vista pratico. IEnumerator, yieldecc. sono funzionalità del linguaggio C # che vengono utilizzate per scopi diversi in Unity.

Per dirla in parole povere, IEnumeratorafferma di avere una raccolta di valori che puoi richiedere uno per uno, un po 'come a List. In C #, una funzione con una firma per restituire un IEnumeratornon deve effettivamente crearne e restituirne una, ma può consentire a C # di fornire un implicito IEnumerator. La funzione può quindi fornire il contenuto di quello restituito IEnumeratorin futuro in modo pigro, attraverso yield returndichiarazioni. Ogni volta che il chiamante richiede un altro valore da tale implicito IEnumerator, la funzione viene eseguita fino yield returnall'istruzione successiva , che fornisce il valore successivo. Come sottoprodotto di ciò, la funzione viene messa in pausa fino a quando non viene richiesto il valore successivo.

In Unity non li usiamo per fornire valori futuri, sfruttiamo il fatto che la funzione si interrompe. A causa di questo sfruttamento, molte cose sulle coroutine in Unity non hanno senso (Cosa IEnumeratorc'entra con qualcosa? Cos'è yield? Perché new WaitForSeconds(3)? Ecc.). Quello che succede "sotto il cofano" è che i valori forniti tramite IEnumerator vengono utilizzati StartCoroutine()per decidere quando chiedere il valore successivo, che determina quando la coroutine si interromperà nuovamente.

Il tuo gioco Unity è a thread singolo (*)

Le coroutine non sono discussioni. Esiste un ciclo principale di Unity e tutte quelle funzioni che scrivi vengono chiamate dallo stesso thread principale in ordine. Puoi verificarlo inserendo a while(true);in una qualsiasi delle tue funzioni o coroutine. Congelerà il tutto, anche l'editor Unity. Questa è la prova che tutto funziona in un thread principale. Questo link che Kay ha menzionato nel suo commento sopra è anche una grande risorsa.

(*) Unity chiama le tue funzioni da un thread. Quindi, a meno che tu non crei tu stesso un thread, il codice che hai scritto è a thread singolo. Ovviamente Unity impiega altri thread e puoi creare tu stesso thread se vuoi.

Una descrizione pratica di Coroutine per programmatori di giochi

In sostanza, quando si chiama StartCoroutine(MyCoroutine()), è esattamente come una chiamata di funzione per regolare MyCoroutine(), fino al primo yield return X, dove Xè qualcosa di simile null, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), break, ecc Questo è quando inizia diversa da una funzione. Unity "mette in pausa" che funzionano proprio su quella yield return Xlinea, continua con altre attività e alcuni frame passano e, quando è di nuovo tempo, Unity riprende quella funzione subito dopo quella linea. Ricorda i valori per tutte le variabili locali nella funzione. In questo modo, ad esempio, è possibile avere un forloop che gira ogni due secondi.

Quando Unity riprenderà la tua routine dipende da cosa Xc'era nel tuo yield return X. Ad esempio, se utilizzato yield return new WaitForSeconds(3);, riprende dopo che sono trascorsi 3 secondi. Se lo hai usato yield return StartCoroutine(AnotherCoroutine()), riprende dopo che AnotherCoroutine()è stato completato, il che ti consente di annidare i comportamenti nel tempo. Se hai appena usato a yield return null;, riprende subito al fotogramma successivo.


2
È un peccato, UnityGems sembra ormai inattivo da un po '. Alcune persone su Reddit sono riuscite a ottenere l'ultima versione dell'archivio: web.archive.org/web/20140702051454/http://unitygems.com/…
ForceMagic

3
Questo è molto vago e rischia di non essere corretto. Ecco come viene effettivamente compilato il codice e perché funziona. Inoltre, anche questo non risponde alla domanda. stackoverflow.com/questions/3438670/…
Louis Hong,

Sì, immagino di aver spiegato "come funzionano le coroutine in Unity" dal punto di vista di un programmatore di giochi. La vera domanda è stata chiedere cosa sta succedendo sotto il cofano. Se riesci a sottolineare parti errate della mia risposta, sarei felice di risolverlo.
Gazihan Alankus,

4
Sono d'accordo sulla resa del rendimento falso, l'ho aggiunto perché qualcuno ha criticato la mia risposta per non averlo ed ero di fretta di rivedere se fosse utile, e ho semplicemente aggiunto il link. L'ho rimosso ora. Tuttavia, penso che l'Unità sia a thread singolo e come le coroutine si adattino a questo non è ovvio per tutti. Molti programmatori Unity per principianti con cui ho parlato hanno una comprensione molto vaga dell'intera faccenda e traggono beneficio da tale spiegazione. Ho modificato la mia risposta per fornire una risposta alternativa alla domanda. Suggerimenti benvenuti.
Gazihan Alankus,

2
Unity non è un singolo thread a prima vista. Ha un thread principale in cui vengono eseguiti i metodi del ciclo di vita di MonoBehaviour, ma ha anche altri thread. Sei anche libero di creare i tuoi thread.
benthehutt,

10

Non potrebbe essere più semplice:

Unity (e tutti i motori di gioco) sono basati su frame .

L'intero punto, l'intera ragion d'essere dell'Unità, è che si basa sul frame. Il motore fa le cose "ogni frame" per te. (Animazione, rendering di oggetti, fisica e così via.)

Potresti chiedere ... "Oh, è grandioso. E se volessi che il motore facesse qualcosa per me in ogni frame? Come faccio a dire al motore di fare questo e quel in un frame?"

La risposta è ...

È esattamente a questo che serve una "coroutine".

È così semplice.

E considera questo ....

Conosci la funzione "Aggiorna". Molto semplicemente, tutto ciò che si inserisce viene fatto in ogni fotogramma . È letteralmente esattamente lo stesso, nessuna differenza, dalla sintassi della resa coroutine.

void Update()
 {
 this happens every frame,
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 }

...in a coroutine...
 while(true)
 {
 this happens every frame.
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 yield return null;
 }

Non c'è assolutamente alcuna differenza.

Nota a piè di pagina: come tutti hanno sottolineato, Unity semplicemente non ha discussioni . I "frame" in Unity o in qualsiasi motore di gioco non hanno assolutamente alcuna connessione ai thread in alcun modo.

Coroutine / rendimento sono semplicemente le modalità di accesso ai frame in Unity. Questo è tutto. (E in effetti, è assolutamente la stessa della funzione Update () fornita da Unity.) Questo è tutto ciò che c'è da fare, è così semplice.


Grazie! Ma la tua risposta spiega come usare le coroutine, non come funzionano dietro le quinte.
Ghopper21,

1
È un piacere, grazie. Capisco cosa intendi: questa può essere una buona risposta per i principianti che chiedono sempre quali sono le coroutine del diavolo. Saluti!
Fattie,

1
In realtà, nessuna delle risposte, neanche leggermente, spiega cosa sta succedendo "dietro le quinte". (Il che è che è un IEnumerator che viene impilato in uno scheduler.)
Fattie

Hai detto "Non c'è assolutamente alcuna differenza". Allora perché Unity ha creato Coroutine quando hanno già un'implementazione funzionante esatta come Update()? Voglio dire che dovrebbe esserci almeno una leggera differenza tra queste due implementazioni e i loro casi d'uso, il che è abbastanza ovvio.
Leandro Gecozo,

hey @LeandroGecozo - Direi di più, che "Update" è solo una sorta di semplificazione ("sciocca") che hanno aggiunto. (Molte persone non la usano mai, usano solo le coroutine!) Non credo che ci sia una buona risposta alla tua domanda, è solo come è Unity.
Fattie,

5

Recentemente ho approfondito questo argomento, ho scritto un post qui - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - che ha fatto luce sugli interni (con esempi di codice denso), l' IEnumeratorinterfaccia sottostante , e come viene utilizzato per le coroutine.

L'uso degli enumeratori di raccolta a questo scopo mi sembra ancora un po 'strano. È l'inverso di ciò per cui gli enumeratori si sentono progettati. Il punto degli enumeratori è il valore restituito su ogni accesso, ma il punto di Coroutine è il codice tra i valori restituiti. Il valore restituito effettivo è inutile in questo contesto.


0

Le funzioni di base in Unity che ottieni automaticamente sono la funzione Start () e la funzione Update (), quindi le Coroutine sono essenzialmente funzioni come la funzione Start () e Update (). Qualsiasi vecchia funzione func () può essere chiamata allo stesso modo in cui è possibile chiamare una Coroutine. L'unità ha ovviamente fissato alcuni limiti per le Coroutine che le rendono diverse dalle normali funzioni. Una differenza è invece di

  void func()

Scrivi

  IEnumerator func()

per coroutine. E allo stesso modo è possibile controllare l'ora in normali funzioni con righe di codice come

  Time.deltaTime

Una coroutine ha un handle specifico sul modo in cui il tempo può essere controllato.

  yield return new WaitForSeconds();

Sebbene questa non sia l'unica cosa possibile da fare all'interno di un IEnumerator / Coroutine, è una delle cose utili per le quali sono usate le Coroutine. Dovresti cercare l'API di scripting di Unity per imparare altri usi specifici delle Coroutine.


0

StartCoroutine è un metodo per chiamare una funzione IEnumerator. È simile al solo chiamare una semplice funzione vuota, la differenza è che la usi su funzioni IEnumerator. Questo tipo di funzione è unica in quanto consente di utilizzare una funzione di rendimento speciale , si noti che è necessario restituire qualcosa. Questo per quanto ne so. Qui ho scritto un semplice gioco di sfarfallio sul metodo del testo in unità

    public IEnumerator GameOver()
{
    while (true)
    {
        _gameOver.text = "GAME OVER";
        yield return new WaitForSeconds(Random.Range(1.0f, 3.5f));
        _gameOver.text = "";
        yield return new WaitForSeconds(Random.Range(0.1f, 0.8f));
    }
}

L'ho quindi chiamato da IEnumerator stesso

    public void UpdateLives(int currentlives)
{
    if (currentlives < 1)
    {
        _gameOver.gameObject.SetActive(true);
        StartCoroutine(GameOver());
    }
}

Come puoi vedere come ho usato il metodo StartCoroutine (). Spero di aver aiutato in qualche modo. Anch'io sono un principiante, quindi se mi correggi o mi apprezzi, qualsiasi tipo di feedback sarebbe fantastico.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.