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.
IEnumerator
/IEnumerable
(o gli equivalenti generici) e che contengono layield
parola chiave. Cerca iteratori.