Wie funktioniert das StartCoroutine / Yield Return-Muster in Unity wirklich?

134

Ich verstehe das Prinzip der Coroutinen. Ich weiß, wie man den Standard StartCoroutine/ das yield returnMuster in C # in Unity zum Laufen bringt, z. B. eine Methode aufruft, die IEnumeratorüber zurückkehrt, StartCoroutineund in dieser Methode etwas tut yield return new WaitForSeconds(1);, eine Sekunde wartet und dann etwas anderes tut.

Meine Frage ist: Was ist wirklich hinter den Kulissen los? Was macht das StartCoroutinewirklich? Was IEnumeratorkommt WaitForSecondszurück? Wie StartCoroutinekehrt die Steuerung zum "etwas anderen" Teil der aufgerufenen Methode zurück? Wie interagiert all dies mit dem Parallelitätsmodell von Unity (bei dem viele Dinge gleichzeitig ohne Verwendung von Coroutinen ablaufen)?

Ghopper21
quelle
3
Der C # -Compiler transformiert Methoden, die IEnumerator/ IEnumerable(oder die generischen Entsprechungen) zurückgeben und das yieldSchlüsselwort enthalten . Nach Iteratoren suchen.
Damien_The_Unbeliever
4
Ein Iterator ist eine sehr bequeme Abstraktion für eine "Zustandsmaschine". Wenn Sie das zuerst verstehen, erhalten Sie auch Unity-Coroutinen. en.wikipedia.org/wiki/State_machine
Hans Passant
2
Das Unity-Tag wird von Microsoft Unity reserviert. Bitte missbrauchen Sie es nicht.
Lex Li
11
Ich fand diesen Artikel ziemlich aufschlussreich
Kay
5
@ Kay - Ich wünschte ich könnte dir ein Bier kaufen. Dieser Artikel ist genau das, was ich brauchte. Ich fing an, meine geistige Gesundheit in Frage zu stellen, da meine Frage anscheinend nicht einmal Sinn ergab, aber der Artikel beantwortet meine Frage direkt besser, als ich es mir hätte vorstellen können. Vielleicht können Sie mit diesem Link eine Antwort hinzufügen, die ich zum Nutzen zukünftiger SO-Benutzer akzeptieren kann?
Ghopper21

Antworten:

110

Der häufig referenzierte Link zu Unity3D-Coroutinen im Detail ist tot. Da es in den Kommentaren und Antworten erwähnt wird, werde ich den Inhalt des Artikels hier veröffentlichen. Dieser Inhalt stammt aus diesem Spiegel .


Unity3D-Coroutinen im Detail

Viele Prozesse in Spielen finden im Verlauf mehrerer Frames statt. Sie haben "dichte" Prozesse wie die Pfadfindung, die in jedem Frame hart arbeiten, aber auf mehrere Frames aufgeteilt werden, um die Framerate nicht zu stark zu beeinflussen. Sie haben "spärliche" Prozesse, wie z. B. Gameplay-Trigger, die in den meisten Frames nichts bewirken, aber gelegentlich zu kritischer Arbeit aufgefordert werden. Und Sie haben verschiedene Prozesse zwischen den beiden.

Wenn Sie einen Prozess erstellen, der über mehrere Frames hinweg ausgeführt wird - ohne Multithreading -, müssen Sie eine Möglichkeit finden, die Arbeit in Blöcke aufzuteilen, die einzeln pro Frame ausgeführt werden können. Für jeden Algorithmus mit einer zentralen Schleife ist es ziemlich offensichtlich: Ein A * -Pfadfinder kann beispielsweise so strukturiert werden, dass er seine Knotenlisten semipermanent verwaltet und nur eine Handvoll Knoten aus der offenen Liste jedes Frames verarbeitet, anstatt es zu versuchen die ganze Arbeit auf einmal erledigen. Es muss ein gewisser Ausgleich vorgenommen werden, um die Latenz zu verwalten. Wenn Sie Ihre Framerate auf 60 oder 30 Bilder pro Sekunde beschränken, dauert Ihr Prozess nur 60 oder 30 Schritte pro Sekunde, und dies kann dazu führen, dass der Prozess nur dauert insgesamt zu lang. Ein ordentliches Design bietet möglicherweise die kleinstmögliche Arbeitseinheit auf einer Ebene - z Verarbeiten Sie einen einzelnen A * -Knoten - und überlagern Sie ihn, um die Arbeit in größere Blöcke zu gruppieren - z. B. verarbeiten Sie A * -Knoten weiterhin X Millisekunden lang. (Einige Leute nennen dies "Timeslicing", obwohl ich es nicht tue).

Wenn Sie jedoch zulassen, dass die Arbeit auf diese Weise aufgeteilt wird, müssen Sie den Status von einem Frame zum nächsten übertragen. Wenn Sie einen iterativen Algorithmus auflösen, müssen Sie den gesamten Status beibehalten, der über die Iterationen hinweg gemeinsam genutzt wird, sowie nachverfolgen, welche Iteration als Nächstes ausgeführt werden soll. Das ist normalerweise nicht schlecht - das Design einer 'A * Pathfinder-Klasse' ist ziemlich offensichtlich - aber es gibt auch andere Fälle, die weniger angenehm sind. Manchmal stehen Sie vor langen Berechnungen, die von Frame zu Frame unterschiedliche Arbeiten ausführen. Das Objekt, das seinen Status erfasst, kann zu einem großen Durcheinander von nützlichen "Einheimischen" führen, die für die Weitergabe von Daten von einem Frame zum nächsten aufbewahrt werden. Und wenn Sie mit einem spärlichen Prozess zu tun haben, müssen Sie häufig eine kleine Zustandsmaschine implementieren, um zu verfolgen, wann überhaupt gearbeitet werden sollte.

Wäre es nicht ordentlich, wenn Sie, anstatt all diesen Status explizit über mehrere Frames hinweg verfolgen zu müssen und anstatt Multithread zu betreiben und Synchronisation und Sperren usw. zu verwalten, Ihre Funktion einfach als einen einzelnen Codeabschnitt schreiben könnten, und Markieren Sie bestimmte Stellen, an denen die Funktion angehalten und zu einem späteren Zeitpunkt fortgesetzt werden soll.

Unity bietet dies - zusammen mit einer Reihe anderer Umgebungen und Sprachen - in Form von Coroutinen.

Wie sehen sie aus? 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;
    }
}

Wie arbeiten Sie? Lassen Sie mich kurz sagen, dass ich nicht für Unity Technologies arbeite. Ich habe den Unity-Quellcode nicht gesehen. Ich habe noch nie den Mut von Unitys Coroutine-Engine gesehen. Wenn sie es jedoch auf eine Weise implementiert haben, die sich grundlegend von dem unterscheidet, was ich beschreiben werde, bin ich ziemlich überrascht. Wenn sich jemand von UT einschalten und darüber sprechen möchte, wie es tatsächlich funktioniert, dann wäre das großartig.

Die großen Hinweise sind in der C # -Version. Beachten Sie zunächst, dass der Rückgabetyp für die Funktion IEnumerator ist. Und zweitens ist zu beachten, dass eine der Aussagen die Rendite ist. Dies bedeutet, dass Yield ein Schlüsselwort sein muss. Da die C # -Unterstützung von Unity Vanilla C # 3.5 ist, muss es sich um ein Vanilla C # 3.5-Schlüsselwort handeln. In der Tat ist es hier in MSDN - es spricht von etwas, das als "Iteratorblöcke" bezeichnet wird. So was ist los?

Erstens gibt es diesen IEnumerator-Typ. Der IEnumerator-Typ verhält sich wie ein Cursor über einer Sequenz und stellt zwei wichtige Elemente bereit: Current, eine Eigenschaft, die Ihnen das Element angibt, über dem sich der Cursor derzeit befindet, und MoveNext (), eine Funktion, die zum nächsten Element in der Sequenz wechselt. Da IEnumerator eine Schnittstelle ist, wird nicht genau angegeben, wie diese Mitglieder implementiert werden. MoveNext () könnte einfach einen zu Current hinzufügen, oder es könnte den neuen Wert aus einer Datei laden, oder es könnte ein Bild aus dem Internet herunterladen und es hashen und den neuen Hash in Current speichern ... oder es könnte sogar eines für das erste tun Element in der Sequenz und etwas ganz anderes für die Sekunde. Sie können es sogar verwenden, um eine unendliche Sequenz zu generieren, wenn Sie dies wünschen. MoveNext () berechnet den nächsten Wert in der Sequenz (gibt false zurück, wenn keine weiteren Werte vorhanden sind).

Wenn Sie eine Schnittstelle implementieren möchten, müssen Sie normalerweise eine Klasse schreiben, die Mitglieder implementieren usw. Iteratorblöcke sind eine bequeme Möglichkeit, IEnumerator ohne großen Aufwand zu implementieren. Sie befolgen nur einige Regeln, und die IEnumerator-Implementierung wird vom Compiler automatisch generiert.

Ein Iteratorblock ist eine reguläre Funktion, die (a) IEnumerator zurückgibt und (b) das Schlüsselwort yield verwendet. Was macht das Yield-Keyword eigentlich? Es gibt an, was der nächste Wert in der Sequenz ist - oder dass es keine weiteren Werte gibt. Der Punkt, an dem der Code auf eine Ertragsrendite X oder eine Ertragsunterbrechung stößt, ist der Punkt, an dem IEnumerator.MoveNext () anhalten sollte. Eine Ertragsrückgabe X bewirkt, dass MoveNext () true zurückgibt und Current der Wert X zugewiesen wird, während eine Ertragsunterbrechung bewirkt, dass MoveNext () false zurückgibt.

Hier ist der Trick. Es muss keine Rolle spielen, welche tatsächlichen Werte von der Sequenz zurückgegeben werden. Sie können MoveNext () wiederholt aufrufen und Current ignorieren. Die Berechnungen werden weiterhin durchgeführt. Jedes Mal, wenn MoveNext () aufgerufen wird, wird Ihr Iteratorblock zur nächsten 'Yield'-Anweisung ausgeführt, unabhängig davon, welchen Ausdruck er tatsächlich liefert. So können Sie etwas schreiben wie:

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

und was Sie tatsächlich geschrieben haben, ist ein Iteratorblock, der eine lange Folge von Nullwerten generiert. Was jedoch von Bedeutung ist, sind die Nebenwirkungen der Arbeit, die er zur Berechnung dieser Werte leistet. Sie können diese Coroutine mit einer einfachen Schleife wie der folgenden ausführen:

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

Oder, nützlicher, Sie könnten es mit anderen Arbeiten mischen:

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

Wie Sie gesehen haben, muss jede Yield Return-Anweisung einen Ausdruck (wie null) enthalten, damit der Iteratorblock IEnumerator.Current tatsächlich etwas zuweisen kann. Eine lange Folge von Nullen ist nicht gerade nützlich, aber wir sind mehr an den Nebenwirkungen interessiert. Sind wir nicht?

Mit diesem Ausdruck können wir tatsächlich etwas Praktisches anfangen. Was wäre, wenn wir, anstatt nur Null zu geben und es zu ignorieren, etwas liefern würden, das anzeigt, wann wir voraussichtlich mehr Arbeit leisten müssen? Oft müssen wir sicher mit dem nächsten Frame weitermachen, aber nicht immer: Es wird viele Male geben, in denen wir weitermachen möchten, nachdem eine Animation oder ein Sound abgespielt wurde oder nachdem eine bestimmte Zeit verstrichen ist. Diejenigen, die while (playAnimation) ergeben, geben null zurück. Konstrukte sind etwas langweilig, findest du nicht?

Unity deklariert den YieldInstruction-Basistyp und stellt einige konkrete abgeleitete Typen bereit, die bestimmte Arten von Wartezeiten angeben. Sie haben WaitForSeconds, mit dem die Coroutine nach Ablauf der festgelegten Zeit fortgesetzt wird. Sie haben WaitForEndOfFrame, mit dem die Coroutine zu einem bestimmten Zeitpunkt später im selben Frame fortgesetzt wird. Sie haben den Coroutine-Typ selbst, der, wenn Coroutine A Coroutine B ergibt, Coroutine A pausiert, bis Coroutine B beendet ist.

Wie sieht das aus Sicht der Laufzeit aus? Wie gesagt, ich arbeite nicht für Unity, deshalb habe ich ihren Code nie gesehen. aber ich würde mir vorstellen, dass es ein bisschen so aussehen könnte:

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;

Es ist nicht schwer vorstellbar, wie weitere YieldInstruction-Subtypen hinzugefügt werden könnten, um andere Fälle zu behandeln. Beispielsweise könnte die Unterstützung von Signalen auf Engine-Ebene hinzugefügt werden, wobei eine YieldInstruction von WaitForSignal ("SignalName") dies unterstützt. Durch Hinzufügen weiterer YieldInstructions können die Coroutinen selbst ausdrucksvoller werden - Yield Return New WaitForSignal ("GameOver") ist besser zu lesen als während (! Signals.HasFired ("GameOver")) Yield Return Null, wenn Sie mich fragen, ganz abgesehen davon die Tatsache, dass es in der Engine schneller sein könnte als in einem Skript.

Ein paar nicht offensichtliche Konsequenzen Es gibt ein paar nützliche Dinge an all dem, die die Leute manchmal vermissen, und ich dachte, ich sollte darauf hinweisen.

Erstens liefert YieldInstruction nur einen Ausdruck - einen beliebigen Ausdruck - und YieldInstruction ist ein regulärer Typ. Dies bedeutet, dass Sie Dinge tun können wie:

YieldInstruction y;

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

yield return y;

Die spezifischen Zeilen ergeben return new WaitForSeconds (), return return new WaitForEndOfFrame () usw. sind häufig, aber keine eigenständigen Sonderformen.

Zweitens, da diese Coroutinen nur Iteratorblöcke sind, können Sie sie selbst durchlaufen, wenn Sie möchten - Sie müssen die Engine nicht für Sie erledigen lassen. Ich habe dies verwendet, um einer Coroutine zuvor Interrupt-Bedingungen hinzuzufügen:

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

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

Drittens kann die Tatsache, dass Sie anderen Coroutinen nachgeben können, es Ihnen ermöglichen, Ihre eigenen YieldInstructions zu implementieren, wenn auch nicht so leistungsfähig, als ob sie von der Engine implementiert würden. Beispielsweise:

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

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

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

Ich würde dies jedoch nicht wirklich empfehlen - die Kosten für den Start einer Coroutine sind für meinen Geschmack etwas hoch.

Fazit Ich hoffe, dies verdeutlicht ein wenig, was wirklich passiert, wenn Sie eine Coroutine in Unity verwenden. Die Iteratorblöcke von C # sind ein tolles kleines Konstrukt, und selbst wenn Sie Unity nicht verwenden, ist es möglicherweise nützlich, sie auf die gleiche Weise zu nutzen.

James McMahon
quelle
2
Vielen Dank, dass Sie das hier wiedergeben. Es ist ausgezeichnet und hat mir sehr geholfen.
Naikrovek
95

Die erste Überschrift unten ist eine klare Antwort auf die Frage. Die beiden folgenden Überschriften sind für den täglichen Programmierer nützlicher.

Möglicherweise langweilige Implementierungsdetails von Coroutinen

Coroutinen werden in Wikipedia und anderswo erklärt. Hier werde ich nur einige Details aus praktischer Sicht liefern. IEnumerator, yieldusw. sind C # -Sprachfunktionen , die in Unity für einen etwas anderen Zweck verwendet werden.

Um es ganz einfach auszudrücken: Ein IEnumeratorAnspruch auf eine Sammlung von Werten, die Sie einzeln anfordern können, ähnlich wie a List. In C # muss eine Funktion mit einer Signatur zum Zurückgeben einer IEnumeratornicht tatsächlich eine Signatur erstellen und zurückgeben, sondern kann C # implizit bereitstellen lassen IEnumerator. Die Funktion kann dann den Inhalt dessen, was IEnumeratorin Zukunft zurückgegeben wird, durch yield returnAnweisungen auf träge Weise bereitstellen . Jedes Mal, wenn der Aufrufer einen anderen Wert von diesem impliziten Wert anfordert IEnumerator, wird die Funktion bis zur nächsten yield returnAnweisung ausgeführt, die den nächsten Wert bereitstellt. Als Nebenprodukt wird die Funktion angehalten, bis der nächste Wert angefordert wird.

In Unity verwenden wir diese nicht, um zukünftige Werte bereitzustellen. Wir nutzen die Tatsache aus, dass die Funktion angehalten wird. Aufgrund dieser Ausbeutung sind viele Dinge über Coroutinen in Unity nicht sinnvoll (Was hat IEnumeratormit irgendetwas zu tun? Was ist yield? Warum new WaitForSeconds(3)? Usw.). Was "unter der Haube" passiert, ist, dass die Werte, die Sie über den IEnumerator bereitstellen, verwendet werden, um StartCoroutine()zu entscheiden, wann nach dem nächsten Wert gefragt werden soll, der bestimmt, wann Ihre Coroutine wieder unterbrochen wird.

Dein Unity-Spiel ist Single Threaded (*)

Coroutinen sind keine Fäden. Es gibt eine Hauptschleife von Unity, und alle Funktionen, die Sie schreiben, werden nacheinander von demselben Hauptthread aufgerufen. Sie können dies überprüfen, indem Sie ein while(true);in eine Ihrer Funktionen oder Coroutinen einfügen. Es wird das Ganze einfrieren, sogar den Unity-Editor. Dies ist ein Beweis dafür, dass alles in einem Hauptthread läuft. Dieser Link , den Kay in seinem obigen Kommentar erwähnt hat, ist auch eine großartige Ressource.

(*) Unity ruft Ihre Funktionen von einem Thread aus auf. Sofern Sie keinen Thread selbst erstellen, ist der von Ihnen geschriebene Code ein einzelner Thread. Natürlich verwendet Unity andere Threads und Sie können Threads selbst erstellen, wenn Sie möchten.

Eine praktische Beschreibung der Coroutinen für Spielprogrammierer

Grundsätzlich, wenn Sie anrufen StartCoroutine(MyCoroutine()), es ist genau wie bei einem normalen Funktionsaufruf MyCoroutine(), bis die ersten yield return X, in dem Xso etwas wie ist null, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), breaketc. Dies ist , wenn es beginnt , von einer Funktion unterscheidet. Unity "pausiert" diese Funktion direkt in dieser yield return XZeile, fährt mit anderen Geschäften fort und einige Frames vergehen, und wenn es wieder soweit ist, nimmt Unity diese Funktion direkt nach dieser Zeile wieder auf. Es speichert die Werte für alle lokalen Variablen in der Funktion. Auf diese Weise können Sie forbeispielsweise alle zwei Sekunden eine Schleife erstellen.

Wann Unity wieder aufgenommen wird, hängt davon ab, was Xsich in Ihrer Coroutine befand yield return X. Wenn Sie beispielsweise verwendet haben yield return new WaitForSeconds(3);, wird es nach Ablauf von 3 Sekunden fortgesetzt. Wenn Sie verwendet haben yield return StartCoroutine(AnotherCoroutine()), wird es fortgesetzt, nachdem AnotherCoroutine()es vollständig abgeschlossen ist, sodass Sie das Verhalten rechtzeitig verschachteln können. Wenn Sie gerade a verwendet haben yield return null;, wird es direkt beim nächsten Frame fortgesetzt.

Gazihan Alankus
quelle
2
Das ist schade, UnityGems scheint mittlerweile eine Weile außer Betrieb zu sein. Einige Leute auf Reddit haben es geschafft, die letzte Version des Archivs zu bekommen: web.archive.org/web/20140702051454/http://unitygems.com/…
ForceMagic
3
Dies ist sehr vage und es besteht die Gefahr, dass es falsch ist. Hier erfahren Sie, wie der Code tatsächlich kompiliert wird und warum er funktioniert. Auch dies beantwortet die Frage nicht. stackoverflow.com/questions/3438670/…
Louis Hong
Ja, ich glaube, ich habe aus der Sicht eines Spielprogrammierers erklärt, wie Coroutinen in Unity funktionieren. Die eigentliche Frage war, was unter der Haube passiert. Wenn Sie auf falsche Teile meiner Antwort hinweisen können, würde ich das gerne beheben.
Gazihan Alankus
4
Ich bin damit einverstanden, dass die Rendite falsch ist. Ich habe sie hinzugefügt, weil jemand meine Antwort dafür kritisiert hat, dass sie nicht vorhanden ist, und ich es eilig hatte, zu überprüfen, ob sie überhaupt nützlich war, und einfach den Link hinzugefügt. Ich habe es jetzt entfernt. Ich denke jedoch, dass Unity Single Threaded ist und wie Coroutinen dazu passen, ist nicht jedem klar. Viele Unity-Programmierer für Anfänger, mit denen ich gesprochen habe, haben ein sehr vages Verständnis für das Ganze und profitieren von einer solchen Erklärung. Ich habe meine Antwort bearbeitet, um eine tatsächliche Antwort auf die Frage zu geben. Vorschläge willkommen.
Gazihan Alankus
2
Unity ist kein Single-Threaded-Fwiw. Es hat einen Hauptthread, in dem MonoBehaviour-Lebenszyklusmethoden ausgeführt werden - aber es gibt auch andere Threads. Sie können sogar Ihre eigenen Threads erstellen.
Benthehutt
10

Einfacher geht es nicht:

Unity (und alle Game Engines) basieren auf Frames .

Der ganze Punkt, die ganze Existenzberechtigung der Einheit, ist, dass sie rahmenbasiert ist. Der Motor erledigt "jeden Frame" für Sie. (Animiert, rendert Objekte, macht Physik und so weiter.)

Sie könnten fragen ... "Oh, das ist großartig. Was ist, wenn ich möchte, dass der Motor in jedem Frame etwas für mich tut? Wie kann ich dem Motor sagen, dass er das und das in einem Frame tun soll?"

Die Antwort ist ...

Genau dafür ist eine "Coroutine" gedacht.

So einfach ist das.

Und bedenken Sie dies ....

Sie kennen die Funktion "Update". Ganz einfach, alles, was Sie dort eingeben, wird in jedem Frame ausgeführt . Es ist buchstäblich genau das gleiche, überhaupt kein Unterschied zur Coroutine-Yield-Syntax.

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

Es gibt absolut keinen Unterschied.

Fußnote: Wie jeder betont hat, hat Unity einfach keine Fäden . Die "Frames" in Unity oder einer anderen Game-Engine haben keinerlei Verbindung zu Threads.

Coroutinen / Ausbeute sind einfach, wie Sie auf die Frames in Unity zugreifen. Das ist es. (Und in der Tat ist es absolut dasselbe wie die von Unity bereitgestellte Update () - Funktion.) Das ist alles, es ist so einfach.

Fattie
quelle
Vielen Dank! Ihre Antwort erklärt jedoch, wie Coroutinen verwendet werden - nicht wie sie hinter den Kulissen funktionieren.
Ghopper21
1
Es ist mir ein Vergnügen, danke. Ich verstehe, was Sie meinen - dies kann eine gute Antwort für Anfänger sein, die immer fragen, was die verdammten Coroutinen sind. Prost!
Fattie
1
Eigentlich - keine der Antworten, auch nur geringfügig, erklärt, was "hinter den Kulissen" vor sich geht. (Das heißt, es ist ein IEnumerator, der in einen Scheduler gestapelt wird.)
Fattie
Sie sagten: "Es gibt absolut keinen Unterschied." Warum hat Unity dann Coroutines erstellt, wenn sie bereits eine exakt funktionierende Implementierung haben Update()? Ich meine, es sollte zumindest einen kleinen Unterschied zwischen diesen beiden Implementierungen und ihren Anwendungsfällen geben, was ziemlich offensichtlich ist.
Leandro Gecozo
hey @LeandroGecozo - Ich würde mehr sagen, dass "Update" nur eine Art ("alberne") Vereinfachung ist, die sie hinzugefügt haben. (Viele Leute benutzen es nie, benutzen nur Coroutinen!) Ich glaube, es gibt keine gute Antwort auf Ihre Frage, es ist nur, wie Einheit ist.
Fattie
5

Habe mich in letzter Zeit damit befasst und hier einen Beitrag geschrieben - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - der die Interna (mit dichten Codebeispielen), die zugrunde liegende IEnumeratorSchnittstelle, beleuchtet. und wie es für Coroutinen verwendet wird.

Die Verwendung von Sammlungsaufzählern für diesen Zweck erscheint mir immer noch etwas seltsam. Es ist das Gegenteil von dem, wofür sich Enumeratoren entwickelt fühlen. Der Punkt der Enumeratoren ist der zurückgegebene Wert bei jedem Zugriff, aber der Punkt der Coroutinen ist der Code zwischen den zurückgegebenen Werten. Der tatsächlich zurückgegebene Wert ist in diesem Zusammenhang sinnlos.

Geri Borbás
quelle
0

Die Basisfunktionen in Unity, die Sie automatisch erhalten, sind die Funktionen Start () und Update (). Coroutines sind also im Wesentlichen Funktionen wie die Funktionen Start () und Update (). Jede alte Funktion func () kann genauso aufgerufen werden wie eine Coroutine. Die Einheit hat offensichtlich bestimmte Grenzen für Coroutinen gesetzt, die sie von regulären Funktionen unterscheiden. Ein Unterschied ist statt

  void func()

Du schreibst

  IEnumerator func()

für Coroutinen. Und genauso können Sie die Zeit in normalen Funktionen mit Codezeilen wie steuern

  Time.deltaTime

Eine Coroutine hat einen bestimmten Griff, wie die Zeit gesteuert werden kann.

  yield return new WaitForSeconds();

Obwohl dies nicht das einzige ist, was in einem IEnumerator / Coroutine möglich ist, ist es eines der nützlichen Dinge, für die Coroutines verwendet werden. Sie müssten die Skript-API von Unity untersuchen, um andere spezifische Verwendungen von Coroutines zu erfahren.

Alexhawkburr
quelle
0

StartCoroutine ist eine Methode zum Aufrufen einer IEnumerator-Funktion. Es ähnelt dem Aufrufen einer einfachen Void-Funktion. Der Unterschied besteht jedoch darin, dass Sie sie für IEnumerator-Funktionen verwenden. Diese Art von Funktion ist einzigartig, da Sie damit eine spezielle Ertragsfunktion verwenden können. Beachten Sie, dass Sie etwas zurückgeben müssen. Soweit ich weiß. Hier habe ich ein einfaches Flackerspiel über die Textmethode in Einheit geschrieben

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

Ich habe es dann aus dem IEnumerator heraus aufgerufen

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

Wie Sie sehen können, habe ich die StartCoroutine () -Methode verwendet. Hoffe ich habe irgendwie geholfen. Ich bin selbst ein Anfänger. Wenn Sie mich also korrigieren oder schätzen, wäre jede Art von Feedback großartig.

Deepanshu Mishra
quelle