Warum stecke ich nicht in der Schleife fest?


8

Ich bin neu in Unity. Ich habe Coroutinen gelernt und das geschrieben.

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);
    }
}

Wenn die Taste gedrückt wird, wird die Coroutine aufgerufen und sie tritt in die 'while'-Schleife ein. Wenn ich den Knopf verlasse, stoppt es die Coroutine. Sollte es nicht in der 'while'-Schleife stecken bleiben, da es sich um eine Endlosschleife handelt? Warum?


Ich bin erst kürzlich selbst zu Unity zurückgekehrt. Ich stelle fest, dass die Eingabemethoden eine Zeichenfolge enthalten "Fire1". Können Sie dies in der Engine einrichten, um Neuzuordnungen von Schlüsseln zu ermöglichen, anstatt sie auszutippen Keycode.Foo?
Mkalafut

1
Es kann hilfreich sein zu erkennen, dass yielddie Abkürzung für "Ertragskontrolle für den Anrufer, bis das nächste Element in der Aufzählung angefordert wird" effektiv ist.
3Dave

@Mkalafut, das klingt nach einer Frage in einem neuen Fragenbeitrag, wenn Sie die Antwort nicht auf den Unity- Dokumentationsseiten , Tutorials oder Ihren eigenen Experimenten finden.
DMGregory

Ich empfehle StopAllCoroutines()in diesem Fall nicht. Es ist in Ordnung, wenn Sie immer nur eine Coroutine verwenden, aber wenn Sie jemals mehr als eine Coroutine geplant haben, hätte dies unerwünschte Auswirkungen. Stattdessen sollten Sie StopCoroutine()anstelle aller alle die relevante verwenden und einfach stoppen. ( StopAllCoroutines()wäre zB nützlich, wenn du das Level beendest oder einen neuen Bereich lädst usw., aber nicht für bestimmte Dinge wie "Ich schieße nicht mehr".)
Darrel Hoffman

Antworten:


14

Der Grund ist das Schlüsselwort,yield das in C # eine bestimmte Bedeutung hat.

Bei der Begegnung mit den Wörtern yield returnkehrt erwartungsgemäß eine Funktion in C # zurück.

Durch die Verwendung von Yieldator zum Definieren eines Iterators ist keine explizite zusätzliche Klasse erforderlich

[...]

Wenn in der Iterator-Methode eine Yield-Return-Anweisung erreicht wird, wird der Ausdruck zurückgegeben und die aktuelle Position im Code beibehalten. Die Ausführung wird an dieser Stelle neu gestartet, wenn die Iteratorfunktion das nächste Mal aufgerufen wird.

Es gibt also keine Endlosschleife. Es gibt eine Funktion / einen Iterator, die unendlich oft aufgerufen werden kann.

Mit der Unity-Funktion StartCoroutine()ruft das Unity-Framework die Funktion / den Iterator einmal pro Frame auf.

Die Unity-Funktion StopAllCoroutinesbewirkt, dass das Unity-Framework den Aufruf der Funktion / des Iterators beendet.

Und der Rückkehr WaitForSeconds(time)aus dem Iterator macht die Unity Rahmen suspend Aufruf der Funktion / Iterator für time.


Ein verwirrter Kommentar und eine ebenso verwirrte Gegenstimme zu diesem Kommentar ermutigten mich, näher darauf einzugehen, was das Keyword yieldtut und was nicht.

Wenn Sie dies schreiben:

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

Sie können stattdessen auch Folgendes schreiben:

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(); }
}

Daraus folgt, dass das Schlüsselwort yieldnicht mit Multithreading zusammenhängt und absolut nicht aufruft System.Threading.Thread.Yield().


1
" On encountering the words yield return a function in C# returns". Nein, tut es nicht. Der Text, den Sie zitieren, erklärt es ebenso wie 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.". Grundsätzlich: "Bitte halten Sie mich an, wo ich bin, und lassen Sie jemanden eine Weile laufen."
Mawg sagt, Monica wieder

2
@Mawg Ich habe der Antwort einen zweiten Teil hinzugefügt, um Ihr Anliegen anzusprechen.
Peter

Vielen Dank für die Klarstellung (positiv bewertet). Ich habe heute sicherlich etwas Neues gelernt :-)
Mawg sagt, Monica

8

Wenn der Feuerknopf gedrückt wird, wird die zweite if-Anweisung eingegeben und StopAllCoroutines ausgeführt. Dies bedeutet, dass die Coroutine, in der die while-Schleife ausgeführt wird, beendet wird, sodass keine Endlosschleife mehr vorhanden ist. Die Coroutine ist wie ein Container, in dem der Code ausgeführt werden soll.

Ich kann das Unity-Handbuch und die Unity Scripting-API empfehlen , um besser zu verstehen, was Coroutinen sind und wie leistungsfähig sie sein können.

Dieser Blog- und Youtube-Beitrag war auch hilfreich für mich, um Coroutinen besser zu nutzen.


3

Coroutinen sind ein seltsames Tier. Yield Return bewirkt, dass die Methode die Ausführung anhält, bis sie später schrittweise ausgeführt wird. Hinter den Kulissen könnte es ungefähr so ​​aussehen:

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:
    }
}

Und innerhalb von Unity / C # (da Yield Return eine native C # -Funktion ist) wird beim Aufrufen von StartCoroutine ein FireContinuouslyDataObjekt erstellt und an die Methode übergeben. Basierend auf dem Rückgabewert wird festgelegt, wann es später erneut aufgerufen werden soll, indem einfach das FireContinuouslyData-Objekt gespeichert wird, um es beim nächsten Mal zu übergeben.

Wenn Sie jemals eine Ertragsunterbrechung durchgeführt haben, könnte diese intern nur festgelegt werden, data.shouldBreak = trueund dann würde Unity die Daten einfach wegwerfen und nicht erneut planen.

Und wenn zwischen den Ausführungen Daten gespeichert werden müssten, würden diese auch für später in den Daten gespeichert.

Ein Beispiel dafür, wie Unity / C # die Coroutine-Funktionalität implementieren könnte:

//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

In einer anderen Antwort wird erwähnt, dass Sie die Co-Routinen stoppen, wenn "Fire1"sie aktiv sind. Dies ist insofern völlig korrekt, als die Coroutine GameObjects nach dem ersten Drücken von nicht weiter instanziiert "Fire1".

In Ihrem Fall bleibt dieser Code jedoch nicht in einer Endlosschleife stecken. So wie es aussieht, suchen Sie nach einer Antwort auf die while(true) {}Schleife, auch wenn Sie sie nicht extern gestoppt haben.

Es wird nicht stecken bleiben, aber Ihre Coroutine wird auch nicht enden (ohne anzurufen StopCoroutine()oder StopAllCoroutines()). Dies liegt daran , Unity Koroutinen ergeben Kontrolle ihrer Anrufer. yielding unterscheidet sich von returning:

  • Eine returnAnweisung beendet die Ausführung einer Funktion, selbst wenn ihr mehr Code folgt
  • Eine yieldAnweisung unterbricht die Funktion und beginnt in der nächsten Zeile nach der yieldWiederaufnahme.

Normalerweise werden Coroutinen in jedem Frame fortgesetzt, aber Sie geben auch ein WaitForSecondsObjekt zurück.

Die Zeile bedeutet yield return new WaitForSeconds(fireTime)grob übersetzt "Jetzt suspendiere mich und komme erst zurück, wenn fireTimeSekunden vergangen sind".

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.
    }
}

Sofern nicht gestoppt, ist dies eine Coroutine, die nach dem Start die gesamte Schleife alle fireTimeSekunden ausführt .


1

Eine einfache Erklärung: Unter der Haube iteriert Unity über eine Sammlung (von YieldInstruction s oder nulls oder was auch immer Sie yield return) mit der IEnumerator, die Ihre Funktion zurückgibt.

Da Sie das yieldSchlüsselwort verwenden, ist Ihre Methode ein Iterator . Es ist nicht die Unity-Sache, es ist eine C # -Sprachenfunktion. Wie funktioniert es?

Es ist faul und generiert nicht die gesamte Sammlung auf einmal (und die Sammlung kann unendlich sein und kann nicht auf einmal generiert werden). Elemente der Sammlung werden nach Bedarf generiert. Ihre Funktion gibt einen Iterator zurück, mit dem Unity arbeiten kann. Es ruft seine MoveNextMethode auf, um ein neues Element und eine neue CurrentEigenschaft für den Zugriff darauf zu generieren .

Ihre Schleife ist also nicht endlos, sie führt Code aus, gibt ein Element zurück und gibt die Steuerung an Unity zurück, damit sie nicht hängen bleibt und andere Arbeiten ausführen kann, z. B. die Eingabe, um die Coroutine zu stoppen.


0

Überlegen Sie, wie ein foreachfunktioniert:

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

Die Kontrolle über die Iteration liegt beim Aufrufer - wenn Sie die Iteration stoppen (hier mit break), ist es das.

Das yieldSchlüsselwort ist eine einfache Möglichkeit, eine Aufzählung in C # zu erstellen. Der Name deutet darauf hin - yield returngibt dem Anrufer (in diesem Fall unserem foreach) die Kontrolle zurück ; Es ist der Anrufer, der entscheidet, wann mit dem nächsten Element fortgefahren werden soll. So können Sie eine Methode wie folgt erstellen:

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

Das sieht naiv so aus, als würde es für immer laufen; In Wirklichkeit hängt es jedoch ganz vom Anrufer ab. Sie können so etwas tun:

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

Dies kann etwas verwirrend sein, wenn Sie nicht an dieses Konzept gewöhnt sind, aber ich hoffe, es ist auch offensichtlich, dass dies eine sehr nützliche Eigenschaft ist. Es war der einfachste Weg, Ihrem Anrufer die Kontrolle zu geben, und wenn der Anrufer beschließt, nachzufolgen, kann er nur den nächsten Schritt ausführen (wenn Unity heute erstellt wurde, würde es wahrscheinlich awaitanstelle von verwenden yield, existierte aber awaitnicht zurück dann).

Alles, was Sie brauchen, um Ihre eigenen Coroutinen zu implementieren (natürlich die einfachsten, dümmsten Coroutinen), ist Folgendes:

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();
    }
  }
}

Um eine sehr einfache WaitForSecondsImplementierung hinzuzufügen , benötigen Sie lediglich Folgendes:

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;
}

Und der entsprechende Code in unserer Hauptschleife:

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

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

Ta-da - das ist alles, was ein einfaches Coroutine-System benötigt. Und indem er dem Anrufer die Kontrolle übergibt, kann der Anrufer über eine beliebige Anzahl von Dingen entscheiden. Sie haben möglicherweise eine sortierte Ereignistabelle, anstatt alle Coroutinen in jedem Frame zu durchlaufen. Sie können Prioritäten oder Abhängigkeiten haben. Es ermöglicht eine sehr einfache Implementierung von kooperativem Multitasking. Und schau dir an, wie einfach das ist, dank yield:)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.