Le lien de détail des coroutines Unity3D souvent référencé est mort. Puisqu'il est mentionné dans les commentaires et les réponses, je vais publier le contenu de l'article ici. Ce contenu provient de ce miroir .
Les coroutines Unity3D en détail
De nombreux processus dans les jeux se déroulent au cours de plusieurs images. Vous avez des processus `` denses '', comme la recherche de chemin, qui travaillent dur à chaque image mais sont répartis sur plusieurs images afin de ne pas avoir un impact trop important sur la fréquence d'images. Vous avez des processus `` clairsemés '', comme les déclencheurs de gameplay, qui ne font rien pour la plupart des cadres, mais sont parfois appelés à effectuer un travail critique. Et vous avez des processus variés entre les deux.
Chaque fois que vous créez un processus qui se déroulera sur plusieurs images - sans multithreading - vous devez trouver un moyen de diviser le travail en morceaux qui peuvent être exécutés une par image. Pour tout algorithme avec une boucle centrale, c'est assez évident: un pathfinder A *, par exemple, peut être structuré de telle sorte qu'il maintient ses listes de nœuds de manière semi-permanente, ne traitant qu'une poignée de nœuds de la liste ouverte chaque image, au lieu d'essayer pour faire tout le travail en une seule fois. Il y a un certain équilibre à faire pour gérer la latence - après tout, si vous verrouillez votre fréquence d'images à 60 ou 30 images par seconde, votre processus ne prendra que 60 ou 30 étapes par seconde, et cela pourrait entraîner le processus à prendre trop long dans l'ensemble. Une conception soignée pourrait offrir la plus petite unité de travail possible à un niveau - par exemple traiter un seul nœud A * - et superposer par-dessus un moyen de regrouper le travail en morceaux plus grands - par exemple, continuer à traiter les nœuds A * pendant X millisecondes. (Certaines personnes appellent cela le «timelicing», mais je ne le fais pas).
Néanmoins, permettre au travail d'être divisé de cette manière signifie que vous devez transférer l'état d'une image à l'autre. Si vous interrompez un algorithme itératif, vous devez conserver tout l'état partagé entre les itérations, ainsi qu'un moyen de suivre l'itération à effectuer ensuite. Ce n'est généralement pas trop mal - la conception d'une 'classe A * Pathfinder' est assez évidente - mais il y a aussi d'autres cas qui sont moins agréables. Parfois, vous serez confronté à de longs calculs qui effectuent différents types de travail d'une image à l'autre; l'objet capturant leur état peut se retrouver avec un gros désordre de «locaux» semi-utiles, conservés pour transmettre des données d'une image à l'autre. Et si vous avez affaire à un processus clairsemé, vous finissez souvent par devoir implémenter une petite machine à états juste pour savoir quand le travail doit être fait.
Ne serait-il pas intéressant si, au lieu d'avoir à suivre explicitement tout cet état sur plusieurs trames, et au lieu d'avoir à lire et gérer la synchronisation et le verrouillage, etc., vous pouviez simplement écrire votre fonction comme un seul morceau de code, et marquer des endroits particuliers où la fonction doit «faire une pause» et continuer plus tard?
Unity - avec un certain nombre d'autres environnements et langages - fournit cela sous la forme de Coroutines.
A quoi ressemblent-ils? Dans «Unityscript» (Javascript):
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
En C #:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
Comment travaillent-ils? Permettez-moi de dire rapidement que je ne travaille pas pour Unity Technologies. Je n'ai pas vu le code source Unity. Je n'ai jamais vu les tripes du moteur de coroutine d'Unity. Cependant, s'ils l'ont implémenté d'une manière radicalement différente de ce que je suis sur le point de décrire, alors je serai assez surpris. Si quelqu'un d'UT veut intervenir et parler de son fonctionnement, ce serait génial.
Les gros indices sont dans la version C #. Tout d'abord, notez que le type de retour de la fonction est IEnumerator. Et deuxièmement, notez que l'une des instructions est yield return. Cela signifie que yield doit être un mot-clé, et comme le support C # de Unity est vanilla C # 3.5, ce doit être un mot-clé vanilla C # 3.5. En effet, le voici dans MSDN - parlant de quelque chose appelé «blocs d'itérateur». Alors que se passe-t-il?
Tout d'abord, il y a ce type IEnumerator. Le type IEnumerator agit comme un curseur sur une séquence, fournissant deux membres significatifs: Current, qui est une propriété vous donnant l'élément sur lequel se trouve actuellement le curseur, et MoveNext (), une fonction qui passe à l'élément suivant de la séquence. Comme IEnumerator est une interface, il ne spécifie pas exactement comment ces membres sont implémentés; MoveNext () pourrait simplement en ajouter un à Current, ou il pourrait charger la nouvelle valeur à partir d'un fichier, ou il pourrait télécharger une image à partir d'Internet et la hacher et stocker le nouveau hachage dans Current… ou il pourrait même faire une chose pour la première élément de la séquence, et quelque chose de complètement différent pour le second. Vous pouvez même l'utiliser pour générer une séquence infinie si vous le souhaitez. MoveNext () calcule la valeur suivante dans la séquence (retournant false s'il n'y a plus de valeurs),
Normalement, si vous souhaitez implémenter une interface, vous devez écrire une classe, implémenter les membres, etc. Les blocs Iterator sont un moyen pratique d'implémenter IEnumerator sans tous ces tracas - il vous suffit de suivre quelques règles, et l'implémentation IEnumerator est générée automatiquement par le compilateur.
Un bloc itérateur est une fonction régulière qui (a) renvoie IEnumerator et (b) utilise le mot-clé yield. Alors, que fait réellement le mot-clé yield? Il déclare quelle est la valeur suivante dans la séquence - ou qu'il n'y a plus de valeurs. Le point auquel le code rencontre un rendement de retour X ou une rupture de rendement est le point auquel IEnumerator.MoveNext () doit s'arrêter; un rendement de retour X fait que MoveNext () renvoie true et la valeur X lui est assignée, tandis qu'une rupture de rendement oblige MoveNext () à renvoyer false.
Maintenant, voici le truc. Les valeurs réelles renvoyées par la séquence n'ont pas d'importance. Vous pouvez appeler MoveNext () à plusieurs reprises et ignorer Current; les calculs seront toujours effectués. Chaque fois que MoveNext () est appelé, votre bloc d'itérateur s'exécute jusqu'à l'instruction 'yield' suivante, quelle que soit l'expression qu'il produit réellement. Vous pouvez donc écrire quelque chose comme:
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;
}
et ce que vous avez en fait écrit est un bloc itérateur qui génère une longue séquence de valeurs nulles, mais ce qui est significatif, ce sont les effets secondaires du travail qu'il fait pour les calculer. Vous pouvez exécuter cette coroutine en utilisant une simple boucle comme celle-ci:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
Ou, plus utilement, vous pouvez le mélanger avec d'autres travaux:
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
Tout est dans le timing Comme vous l'avez vu, chaque instruction yield return doit fournir une expression (comme null) afin que le bloc itérateur ait quelque chose à affecter réellement à IEnumerator.Current. Une longue séquence de valeurs nulles n'est pas vraiment utile, mais nous nous intéressons davantage aux effets secondaires. N'est-ce pas?
Il y a quelque chose de pratique que nous pouvons faire avec cette expression, en fait. Et si, au lieu de simplement donner null et de l'ignorer, nous produisions quelque chose qui indiquait quand nous prévoyons devoir faire plus de travail? Souvent, nous devrons passer directement à l'image suivante, bien sûr, mais pas toujours: il y aura de nombreuses fois où nous voudrons continuer après qu'une animation ou un son ait fini de jouer, ou après un certain laps de temps. Ceux while (playingAnimation) retournent null; les constructions sont un peu fastidieuses, tu ne trouves pas?
Unity déclare le type de base YieldInstruction et fournit quelques types dérivés concrets qui indiquent des types particuliers d'attente. Vous avez WaitForSeconds, qui reprend la coroutine une fois le temps spécifié écoulé. Vous avez WaitForEndOfFrame, qui reprend la coroutine à un moment donné plus tard dans la même image. Vous avez le type Coroutine lui-même, qui, lorsque la coroutine A produit la coroutine B, met la coroutine A en pause jusqu'à la fin de la coroutine B.
À quoi cela ressemble-t-il du point de vue de l'exécution? Comme je l'ai dit, je ne travaille pas pour Unity, donc je n'ai jamais vu leur code; mais j'imagine que cela pourrait ressembler un peu à ceci:
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;
Il n'est pas difficile d'imaginer comment plus de sous-types YieldInstruction pourraient être ajoutés pour gérer d'autres cas - le support au niveau du moteur pour les signaux, par exemple, pourrait être ajouté, avec un WaitForSignal ("SignalName") YieldInstruction le supportant. En ajoutant plus de YieldInstructions, les coroutines elles-mêmes peuvent devenir plus expressives - yield return new WaitForSignal ("GameOver") est plus agréable à lire que pendant (! Signals.HasFired ("GameOver")) renvoie null, si vous me demandez, en dehors de le fait de le faire dans le moteur pourrait être plus rapide que de le faire dans un script.
Quelques ramifications non évidentes Il y a quelques choses utiles dans tout cela que les gens manquent parfois et que je pensais devoir souligner.
Premièrement, yield return donne juste une expression - n'importe quelle expression - et YieldInstruction est un type régulier. Cela signifie que vous pouvez faire des choses comme:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
Les lignes spécifiques renvoient de nouveaux WaitForSeconds (), renvoient un nouveau WaitForEndOfFrame (), etc., sont courantes, mais ce ne sont pas des formes spéciales à part entière.
Deuxièmement, comme ces coroutines ne sont que des blocs d'itération, vous pouvez les parcourir vous-même si vous le souhaitez - vous n'avez pas besoin de laisser le moteur le faire pour vous. J'ai déjà utilisé ceci pour ajouter des conditions d'interruption à une coroutine:
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
Troisièmement, le fait que vous puissiez céder sur d'autres coroutines peut en quelque sorte vous permettre d'implémenter vos propres YieldInstructions, même si elles ne sont pas aussi performantes que si elles étaient implémentées par le moteur. Par exemple:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
cependant, je ne recommanderais pas vraiment cela - le coût de démarrage d'une Coroutine est un peu lourd à mon goût.
Conclusion J'espère que cela clarifie un peu ce qui se passe réellement lorsque vous utilisez une Coroutine dans Unity. Les blocs d'itérateur de C # sont une petite construction géniale, et même si vous n'utilisez pas Unity, vous trouverez peut-être utile d'en tirer parti de la même manière.
IEnumerator
/IEnumerable
(ou les équivalents génériques) et qui contiennent leyield
mot - clé. Recherchez les itérateurs.