Pourquoi ne suis-je pas coincé dans la boucle


8

Je suis nouveau sur Unity. J'apprenais des coroutines et j'ai écrit cela.

private void Fire()
{
    if(Input.GetButtonDown("Fire1"))
    {
        StartCoroutine(FireContinuously());
    }
    if(Input.GetButtonUp("Fire1"))
    {
        StopAllCoroutines();
    }
}

IEnumerator FireContinuously()
{
    while(true)
    {
        GameObject laser = Instantiate(LaserPrefab, transform.position, Quaternion.identity) as GameObject;
        laser.GetComponent<Rigidbody2D>().velocity = new Vector2(0, 10f);
        yield return new WaitForSeconds(firetime);
    }
}

Lorsque le bouton est enfoncé, la coroutine est appelée et elle entre dans la boucle "while". Lorsque je quitte le bouton, cela arrête la coroutine. Ne devrait-il pas rester coincé dans la boucle «while» car c'est une boucle infinie? Pourquoi?


Je suis rentré récemment dans Unity moi-même, je remarque que les méthodes d'entrée prennent une chaîne "Fire1", est-ce quelque chose que vous pouvez configurer dans le moteur pour permettre les remappages de touches plutôt que de taper Keycode.Foo?
Mkalafut

1
Il peut être utile de se rendre compte qu'il yields'agit effectivement de «contrôle du rendement pour l'appelant jusqu'à ce que l'élément suivant dans l'énumérable soit demandé».
3Dave

@Mkalafut qui ressemble à quelque chose à demander dans un nouveau message de question si vous ne trouvez pas la réponse dans les pages de documentation Unity , les didacticiels ou vos propres expériences.
DMGregory

Je ne recommande pas StopAllCoroutines()dans ce cas. C'est bien quand vous n'utilisez qu'une seule coroutine, mais si vous prévoyez d'en avoir plus d'une, cela aurait des effets indésirables. Au lieu de cela, vous devez utiliser StopCoroutine()et simplement arrêter celui qui est pertinent au lieu de tous. ( StopAllCoroutines()serait utile par exemple pour terminer le niveau ou charger une nouvelle zone, etc., mais pas pour des choses spécifiques comme "Je ne tire plus".)
Darrel Hoffman

Réponses:


14

La raison en est le mot-cléyield qui a une signification spécifique en C #.

En rencontrant les mots, yield returnune fonction en C # revient, comme on pourrait s'y attendre.

L'utilisation de yield pour définir un itérateur supprime le besoin d'une classe supplémentaire explicite

[...]

Lorsqu'une instruction return return est atteinte dans la méthode itérateur, l'expression est renvoyée et l'emplacement actuel dans le code est conservé. L'exécution est redémarrée à partir de cet emplacement lors du prochain appel de la fonction itérateur.

Il n'y a donc pas de boucle infinie. Il existe une fonction / itérateur qui peut être appelée un nombre infini de fois.

La fonction Unity StartCoroutine()oblige le framework Unity à appeler la fonction / itérateur une fois par trame.

La fonction StopAllCoroutinesUnity empêche le framework Unity d'appeler la fonction / itérateur.

Et le retour WaitForSeconds(time)de l'itérateur fait que le framework Unity suspend l'appel de la fonction / itérateur pour time.


Un commentaire confus et un vote positif tout aussi confus sur ce commentaire m'ont encouragé à approfondir ce que le mot yield- clé fait et ne fait pas.

Si vous écrivez ceci:

IEnumerable<int> Count()
{
   int i = 0;
   yield return i++;
}

Vous pouvez également écrire ceci:

IEnumerator<int> Count() {
    return new CountEnumerator ();
}
class CountEnumerator : IEnumerator<int> {
    int i = 0;
    bool IEnumerator<int>.MoveNext() { i++; return true; }
    int IEnumerator<int>.Current { get { return i; }
    void IEnumerator<int>.Reset() { throw new NotSupportedException(); }
}

Il s'ensuit que le mot yield- clé n'est pas lié au multi-threading et n'appelle absolument pasSystem.Threading.Thread.Yield() .


1
" On encountering the words yield return a function in C# returns". Non. Le texte que vous citez l'explique, tout comme Wikipedia - " In computer science, yield is an action that occurs in a computer program during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority.". Fondamentalement, «s'il vous plaît, faites-moi une pause où je suis et laissez quelqu'un d'autre courir pendant un certain temps».
Mawg dit réintégrer Monica

2
@Mawg J'ai ajouté une deuxième partie à la réponse pour répondre à votre préoccupation.
Peter

Merci beaucoup d'avoir clarifié (voté). J'ai certainement appris quelque chose de nouveau aujourd'hui :-)
Mawg dit réintégrer Monica

8

Lorsque le bouton de tir est levé, la deuxième instruction if est entrée et StopAllCoroutines est exécuté. Cela signifie que la Coroutine dans laquelle la boucle while s'exécute est terminée, il n'y a donc plus de boucle infinie. La coroutine est comme un conteneur pour le code à exécuter.

Je peux recommander le manuel Unity et l' API Unity Scripting pour mieux comprendre ce que sont les coroutines et leur puissance.

Ce blog et cette recherche sur YouTube m'ont également aidé à mieux utiliser les coroutines.


3

Les coroutines sont une étrange bête. Le retour de rendement entraîne la suspension de l'exécution de la méthode jusqu'à ce qu'elle soit ultérieurement modifiée. Dans les coulisses, cela pourrait ressembler à ceci:

class FireContinuouslyData {
    int state;
    bool shouldBreak;
}

object FireContinuously(FireContinuouslyData data) {
    switch (data.state) {
        case 0:
            goto State_0;
    }
    while (true) {
        GameObject laser = ...;
        laser.GetComponent...
        //the next three lines handle the yield return
        data.state = 0;
        return new WaitForSeconds(fireTime);
        State_0:
    }
}

Et interne à Unity / C # (puisque return yield est une fonctionnalité native c #), lorsque vous appelez StartCoroutine, il crée un FireContinuouslyDataobjet et le transmet à la méthode. En fonction de la valeur renvoyée, il détermine quand l'appeler à nouveau plus tard, stockant simplement l'objet FireContinuouslyData pour le transmettre la prochaine fois.

Si vous avez déjà fait une pause de rendement, il pourrait simplement être défini en interne data.shouldBreak = true, puis Unity jetterait simplement les données et ne les planifierait plus.

Et s'il y avait des données qui devaient être enregistrées entre les exécutions, elles seraient également stockées dans les données pour plus tard.

Un exemple de la façon dont Unity / C # pourrait implémenter la fonctionnalité coroutine:

//Internal to Unity/C#

class Coroutine {
    Action<object> method;
    object data;
}

Coroutine StartCoroutine(IEnumerator enumerator) {
    object data = CreateDataForEnumerator(method); //Very internal to C#
    Action<object> method = GetMethodForEnumerator(enumerator); //Also very internal to C#
    Coroutine coroutine = new Coroutine(method, data);
    RunCoroutine(coroutine);
    return coroutine;
}

//Called whenever this coroutine is scheduled to run
void RunCoroutine(Coroutine coroutine) {
    object yieldInstruction = coroutine.method(coroutine.data);
    if (!data.shouldBreak) {
        //Put this coroutine into a collection of coroutines to run later, by calling RunCoroutine on it again
        ScheduleForLater(yieldInstruction, coroutine);
    }
}

1

Une autre réponse mentionne que vous arrêtez les co-routines quand "Fire1"c'est terminé - c'est tout à fait correct, dans la mesure où la coroutine ne continue pas d'instancier GameObjects après la première pression de "Fire1".

Dans votre cas, cependant, ce code ne restera pas «coincé» dans une boucle infinie, ce à quoi il semble que vous cherchiez une réponse - c'est-à-dire la while(true) {}boucle, même si vous ne l'avez pas arrêtée en externe.

Il ne restera pas bloqué mais votre coroutine ne se terminera pas (sans appeler StopCoroutine()ou StopAllCoroutines()) non plus. En effet, les coroutines Unity cèdent le contrôle à leur appelant. yielding est différent de returning:

  • une returninstruction arrêtera l'exécution d'une fonction, même s'il y a plus de code qui la suit
  • une yieldinstruction mettra la fonction en pause, en commençant à la ligne suivante après yieldsa reprise.

Habituellement, les coroutines seront reprises à chaque image, mais vous retournez également un WaitForSecondsobjet.

La ligne yield return new WaitForSeconds(fireTime)se traduit grosso modo par "maintenant, suspendez-moi et ne revenez pas avant que les fireTimesecondes ne soient passées".

IEnumerator FireContinuously()
{
    // When started, this coroutine enters the below while loop...
    while(true)
    {
        // It does some things... (Infinite coroutine code goes here)

        // Then it yields control back to it's caller and pauses...
        yield return new WaitForSeconds(fireTime);
        // The next time it is called , it resumes here...
        // It finds the end of a loop, so will re-evaluate the loop condition...
        // Which passes, so control is returned to the top of the loop.
    }
}

À moins qu'elle ne soit arrêtée, il s'agit d'une coroutine qui, une fois démarrée, effectuera la boucle entière toutes les fireTimesecondes.


1

Une explication simple: sous le capot, Unity est en train d'itérer sur une collection (de YieldInstruction s ou null ou tout ce que vous yield return) en utilisant le IEnumeratorque votre fonction retourne.

Puisque vous utilisez le yieldmot - clé, votre méthode est un itérateur . Ce n'est pas la chose Unity, c'est une fonctionnalité du langage C #. Comment ça marche?

Il est paresseux et ne génère pas toute la collection à la fois (et la collection peut être infinie et impossible à générer en même temps). Les éléments de la collection sont générés selon les besoins. Votre fonction renvoie un itérateur avec lequel Unity peut travailler. Il appelle sa MoveNextméthode pour générer un nouvel élément et une nouvelle Currentpropriété pour y accéder.

Votre boucle n'est donc pas infinie, elle exécute du code, renvoie un élément et renvoie le contrôle à Unity afin qu'elle ne soit pas bloquée et puisse effectuer d'autres tâches telles que la gestion de votre entrée pour arrêter la coroutine.


0

Pensez au fonctionnement d'un foreach:

foreach (var number in Enumerable.Range(1, 1000000))
{
  if (number > 10) break;
}

Le contrôle de l'itération est sur l'appelant - si vous arrêtez l'itération (ici avec break), c'est tout.

Le yieldmot-clé est un moyen simple de rendre un énumérable en C #. Le nom l'indique - yield returnredonne le contrôle à l'appelant (dans ce cas, le nôtre foreach); c'est l'appelant qui décide quand passer à l'élément suivant. Vous pouvez donc créer une méthode comme celle-ci:

IEnumerable<int> ToInfinity()
{
  var i = 0;
  while (true) yield return i++;
}

Cela a l'air naïf de fonctionner pour toujours; mais en réalité, cela dépend entièrement de l'appelant. Vous pouvez faire quelque chose comme ça:

var range = ToInfinity().Take(10).ToArray();

Cela peut être un peu déroutant si vous n'êtes pas habitué à ce concept, mais j'espère qu'il est également évident que c'est une propriété très utile. C'était le moyen le plus simple de donner le contrôle à votre appelant, et lorsque l'appelant décide de faire un suivi, il peut simplement faire l'étape suivante (si Unity a été créé aujourd'hui, il utiliserait probablement à la awaitplace de yield; mais awaitn'existait pas en arrière puis).

Tout ce dont vous avez besoin pour implémenter vos propres coroutines (il va sans dire que les coroutines les plus stupides les plus simples) sont les suivantes:

List<IEnumerable> continuations = new List<IEnumerable>();

void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);

void MainLoop()
{
  while (GameIsRunning)
  {
    foreach (var continuation in continuations.ToArray())
    {
      if (!continuation.MoveNext()) continuations.Remove(continuation);
    }

    foreach (var gameObject in updateableGameObjects)
    {
      gameObject.Update();
    }
  }
}

Pour ajouter une WaitForSecondsimplémentation très simple , vous avez juste besoin de quelque chose comme ceci:

interface IDelayedCoroutine
{
  bool ShouldMove();
}

class Waiter: IDelayedCoroutine
{
  private readonly TimeSpan time;
  private readonly DateTime start;

  public Waiter(TimeSpan time)
  {
    this.start = DateTime.Now;
    this.time = time;
  }

  public bool ShouldMove() => start + time > DateTime.Now;
}

Et le code correspondant dans notre boucle principale:

foreach (var continuation in continuations.ToArray())
{
  if (continuation.Current is IDelayedCoroutine dc)
  {
    if (!dc.ShouldMove()) continue;
  }

  if (!continuation.MoveNext()) continuations.Remove(continuation);
}

Ta-da - c'est tout ce dont un système coroutine a besoin. Et en cédant le contrôle à l'appelant, l'appelant peut décider de n'importe quel nombre de choses; ils peuvent avoir une table d'événements triée plutôt que d'itérer dans toutes les coroutines sur chaque trame; ils peuvent avoir des priorités ou des dépendances. Il permet une mise en œuvre très simple du multitâche coopératif. Et regardez à quel point c'est simple, grâce à yield:)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.