Jak naprawdę działa wzorzec zwrotu StartCoroutine / yield w Unity?

135

Rozumiem zasadę coroutines. Wiem, jak sprawić, by wzorzec StartCoroutine/ yield returnwzorzec działał w C # w Unity, np. Wywołać metodę zwracającą się IEnumeratorprzez StartCoroutineiw tej metodzie zrób coś, yield return new WaitForSeconds(1);poczekaj sekundę, a potem zrób coś innego.

Moje pytanie brzmi: co tak naprawdę dzieje się za kulisami? Co tak StartCoroutinenaprawdę robi? Co IEnumeratorjest WaitForSecondspowrocie? W jaki sposób StartCoroutinezwraca kontrolę do części „coś innego” wywoływanej metody? Jak to wszystko współgra z modelem współbieżności Unity (w którym wiele rzeczy dzieje się w tym samym czasie bez użycia coroutines)?

Ghopper21
źródło
3
Kompilator C # przekształca metody, które zwracają IEnumerator/ IEnumerable(lub generyczne odpowiedniki) i które zawierają yieldsłowo kluczowe. Wyszukaj iteratory.
Damien_The_Unbeliever,
4
Iterator jest bardzo wygodną abstrakcją dla „automatu stanowego”. Zrozum to najpierw, a otrzymasz także programy Unity. en.wikipedia.org/wiki/State_machine
Hans Passant
2
Znacznik jedności jest zarezerwowany przez firmę Microsoft Unity. Proszę, nie używaj go niewłaściwie.
Lex Li
11
Uważam, że ten artykuł jest dość pouczający: szczegółowe informacje o procesach Unity3D
Kay
5
@Kay - chciałbym kupić ci piwo. Ten artykuł jest dokładnie tym, czego potrzebowałem. Zaczynałem kwestionować moje zdrowie psychiczne, ponieważ wydawało się, że moje pytanie nie ma nawet sensu, ale artykuł bezpośrednio odpowiada na moje pytanie lepiej, niż mogłem sobie wyobrazić. Może mógłbyś dodać odpowiedź z tym linkiem, którą mogę zaakceptować, z korzyścią dla przyszłych użytkowników SO?
Ghopper21

Odpowiedzi:

110

Często przywoływany link do programów Unity3D w szczegółach jest martwy. Ponieważ jest o tym mowa w komentarzach i odpowiedziach, zamieszczam tutaj treść artykułu. Ta treść pochodzi z tego lustra .


Szczegółowe informacje na temat programów Unity3D

Wiele procesów w grach odbywa się w trakcie wielu klatek. Masz `` gęste '' procesy, takie jak wyszukiwanie ścieżki, które ciężko pracują w każdej klatce, ale są dzielone na wiele klatek, aby nie wpływać zbytnio na liczbę klatek na sekundę. Masz „rzadkie” procesy, takie jak wyzwalacze rozgrywki, które nie robią nic w większości klatek, ale czasami są wzywani do wykonania krytycznej pracy. I masz różne procesy między nimi.

Ilekroć tworzysz proces, który będzie się odbywał w wielu klatkach - bez wielowątkowości - musisz znaleźć sposób na podzielenie pracy na fragmenty, które można uruchomić po jednej na klatkę. Dla każdego algorytmu z centralną pętlą jest to dość oczywiste: na przykład pathfinder A * może być tak skonstruowany, że utrzymuje swoje listy węzłów półtrwale, przetwarzając tylko kilka węzłów z listy otwartej w każdej ramce, zamiast próbować wykonać całą pracę za jednym zamachem. Trzeba trochę zbalansować, aby zarządzać opóźnieniami - w końcu, jeśli blokujesz liczbę klatek na sekundę na 60 lub 30 klatek na sekundę, twój proces zajmie tylko 60 lub 30 kroków na sekundę, co może spowodować, że proces po prostu zajmie ogólnie za długo. Zgrabny projekt może oferować najmniejszą możliwą jednostkę pracy na jednym poziomie - np przetwarzaj pojedynczy węzeł A * - i warstwę na wierzchu w sposób grupujący prace razem w większe porcje - np. kontynuuj przetwarzanie węzłów A * przez X milisekund. (Niektórzy nazywają to „podziałem czasu”, chociaż ja tego nie robię).

Jednak pozwolenie na podzielenie pracy w ten sposób oznacza, że ​​musisz przenosić stan z jednej ramki do drugiej. Jeśli łamiesz algorytm iteracyjny, musisz zachować cały stan współdzielony przez iteracje, a także sposób śledzenia, która iteracja ma zostać wykonana jako następna. Zwykle nie jest tak źle - konstrukcja klasy „A * pathfinder” jest dość oczywista - ale są też inne przypadki, które są mniej przyjemne. Czasami będziesz musiał zmierzyć się z długimi obliczeniami, które wykonują różne rodzaje pracy od klatki do klatki; obiekt przechwytujący ich stan może skończyć się dużym bałaganem częściowo przydatnych „lokalnych”, przechowywanych do przekazywania danych z jednej klatki do drugiej. A jeśli masz do czynienia z rzadkim procesem, często musisz zaimplementować małą maszynę stanową tylko po to, aby śledzić, kiedy należy w ogóle wykonać pracę.

Czy nie byłoby fajnie, gdyby zamiast jawnego śledzenia całego tego stanu w wielu ramkach i zamiast wielowątkowości i zarządzania synchronizacją i blokowaniem itd., Mógłbyś po prostu napisać swoją funkcję jako pojedynczy fragment kodu i zaznaczyć konkretne miejsca, w których funkcja powinna się „zatrzymać” i kontynuować w późniejszym czasie?

Jedność - wraz z wieloma innymi środowiskami i językami - zapewnia to w postaci Coroutines.

Jak wyglądają? W „Unityscript” (Javascript):

function LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield;
    }
}

W C #:

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield return null;
    }
}

Jak oni pracują? Powiem szybko, że nie pracuję dla Unity Technologies. Nie widziałem kodu źródłowego Unity. Nigdy nie widziałem wnętrzności podstawowego silnika Unity. Jeśli jednak zaimplementowali to w sposób radykalnie różny od tego, co mam zamiar opisać, to będę dość zaskoczony. Jeśli ktoś z UT chciałby się do niego włączyć i porozmawiać o tym, jak to naprawdę działa, byłoby świetnie.

Duże wskazówki znajdują się w wersji C #. Po pierwsze, zwróć uwagę, że typem zwracanym dla funkcji jest IEnumerator. Po drugie, zwróć uwagę, że jednym ze stwierdzeń jest zwrot zysku. Oznacza to, że yield musi być słowem kluczowym, a ponieważ obsługa języka C # w Unity to vanilla C # 3.5, musi to być słowo kluczowe vanilla C # 3.5. Rzeczywiście, tutaj jest w MSDN - mowa o czymś, co nazywa się „blokami iteratorów”. Więc co się dzieje?

Po pierwsze, istnieje ten typ IEnumerator. Typ IEnumerator działa jak kursor nad sekwencją, zapewniając dwa znaczące elementy członkowskie: Current, która jest właściwością dającą element, nad którym obecnie znajduje się kursor, oraz MoveNext (), funkcję, która przechodzi do następnego elementu w sekwencji. Ponieważ IEnumerator jest interfejsem, nie określa dokładnie, w jaki sposób te elementy członkowskie są implementowane; MoveNext () może po prostu dodać jedną wartość doCurrent lub może załadować nową wartość z pliku lub może pobrać obraz z Internetu i zaszyfrować go i zapisać nowy hash w Current… lub może nawet zrobić jedną rzecz za pierwszą element w sekwencji i coś zupełnie innego dla drugiego. Możesz nawet użyć go do wygenerowania nieskończonej sekwencji, jeśli chcesz. MoveNext () oblicza następną wartość w sekwencji (zwraca false, jeśli nie ma więcej wartości),

Zwykle, jeśli chciałbyś zaimplementować interfejs, musiałbyś napisać klasę, zaimplementować członków i tak dalej. Bloki iteratora to wygodny sposób implementacji IEnumerator bez wszystkich kłopotów - wystarczy przestrzegać kilku reguł, a implementacja IEnumerator jest generowana automatycznie przez kompilator.

Blok iteratora to zwykła funkcja, która (a) zwraca IEnumerator i (b) używa słowa kluczowego yield. Co właściwie robi słowo kluczowe zysku? Deklaruje, jaka jest następna wartość w sekwencji - lub że nie ma więcej wartości. Punkt, w którym kod napotyka zwrot zysku X lub przerwanie zysku, jest punktem, w którym IEnumerator.MoveNext () powinien się zatrzymać; a yield return X powoduje, że MoveNext () zwraca prawdę, a właściwość Current ma przypisaną wartość X, podczas gdy podział zysku powoduje, że MoveNext () zwraca wartość false.

Oto sztuczka. Nie musi mieć znaczenia, jakie są rzeczywiste wartości zwracane przez sekwencję. Możesz wielokrotnie wywoływać MoveNext () i ignorować Current; obliczenia będą nadal wykonywane. Za każdym razem, gdy wywoływana jest funkcja MoveNext (), blok iteratora przechodzi do następnej instrukcji „yield”, niezależnie od tego, jakie wyrażenie faktycznie daje. Możesz więc napisać coś takiego:

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

tak naprawdę napisałeś blok iteratora, który generuje długą sekwencję wartości null, ale najważniejsze są efekty uboczne pracy, jaką wykonuje, aby je obliczyć. Możesz uruchomić ten program za pomocą prostej pętli, takiej jak ta:

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

Lub, bardziej użytecznie, możesz połączyć to z innymi pracami:

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

Wszystko zależy od czasu Jak już widzieliśmy, każda instrukcja yield return musi zawierać wyrażenie (np. Null), aby blok iteratora miał coś do przypisania do IEnumerator.Current. Długa sekwencja wartości zerowych nie jest dokładnie przydatna, ale bardziej interesują nas skutki uboczne. Prawda?

Właściwie jest coś przydatnego, co możemy zrobić z tym wyrażeniem. A co by było, gdybyśmy zamiast po prostu dać wartość null i ją zignorować, uzyskalibyśmy coś, co wskazywało, kiedy spodziewamy się, że będziemy musieli wykonać więcej pracy? Często będziemy musieli przejść bezpośrednio do następnej klatki, oczywiście, ale nie zawsze: będzie wiele razy, w których będziemy chcieli kontynuować po zakończeniu odtwarzania animacji lub dźwięku lub po upływie określonego czasu. Te while (playingAnimation) dają zwrot null; konstrukcje są trochę uciążliwe, nie sądzisz?

Unity deklaruje typ podstawowy YieldInstruction i udostępnia kilka konkretnych typów pochodnych, które wskazują określone rodzaje oczekiwania. Masz WaitForSeconds, który wznawia coroutine po upływie wyznaczonego czasu. Masz WaitForEndOfFrame, który wznawia coroutine w określonym punkcie później w tej samej klatce. Masz sam typ Coroutine, który, gdy coroutine A daje coroutine B, zatrzymuje coroutine A aż do zakończenia programu B.

Jak to wygląda z punktu widzenia środowiska wykonawczego? Jak powiedziałem, nie pracuję dla Unity, więc nigdy nie widziałem ich kodu; ale wyobrażam sobie, że może to wyglądać trochę tak:

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;

Nietrudno sobie wyobrazić, jak można dodać więcej podtypów YieldInstruction, aby obsłużyć inne przypadki - na przykład można dodać obsługę sygnałów na poziomie silnika, z obsługą WaitForSignal („SignalName”) YieldInstruction. Dodając więcej YieldInstructions, same programy mogą stać się bardziej wyraziste - yield return new WaitForSignal ("GameOver") jest przyjemniejszy do czytania niż podczas gdy (! Signals.HasFired ("GameOver")) zwracają wartość null, jeśli o mnie chodzi, zupełnie poza fakt, że zrobienie tego w silniku mogłoby być szybsze niż zrobienie tego w skrypcie.

Kilka nieoczywistych konsekwencji Jest kilka przydatnych rzeczy w tym wszystkim, których ludzie czasami pomijają, a które moim zdaniem powinny zwrócić uwagę.

Po pierwsze, zwrot zysku daje po prostu wyrażenie - dowolne wyrażenie - a YieldInstruction jest typem regularnym. Oznacza to, że możesz wykonywać takie czynności jak:

YieldInstruction y;

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

yield return y;

Konkretne wiersze dają return new WaitForSeconds (), yield return new WaitForEndOfFrame () itd. Są powszechne, ale nie są w rzeczywistości specjalnymi formami same w sobie.

Po drugie, ponieważ te programy są po prostu blokami iteratorów, możesz je samodzielnie iterować, jeśli chcesz - nie musisz mieć silnika, aby robił to za Ciebie. Używałem tego do dodawania warunków przerwań do programu wcześniej:

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

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

Po trzecie, fakt, że możesz ustąpić miejsca innym programom, może w pewnym sensie pozwolić ci na wdrożenie własnych YieldInstructions, chociaż nie tak wydajnie, jakby były implementowane przez silnik. Na przykład:

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

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

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

jednak tak naprawdę nie polecałbym tego - koszt rozpoczęcia Coroutine jest trochę wysoki jak na mój gust.

Wniosek Mam nadzieję, że to wyjaśnia trochę tego, co naprawdę dzieje się, gdy używasz Coroutine in Unity. Bloki iteratora C # to fajna mała konstrukcja, a nawet jeśli nie używasz Unity, być może uznasz, że przydatne będzie ich wykorzystanie w ten sam sposób.

James McMahon
źródło
2
Dziękuję za odtworzenie tego tutaj. Jest doskonały i znacznie mi pomógł.
Naikrovek
Nie rozumiem ostatniej części. Dlaczego miałbyś w tym celu rozpocząć nowy program? Myślę, że możesz odrzucić obecną metodę „UntilTrue” (która i tak zwraca Coroutine, a nie IEnumerator) i zrobić to bezpośrednio, IEnumerator SomeTask() { yield return UntilTrueCoroutine(() => _lives < 3); }chociaż prawdopodobnie nie nazwałbyś metody „UntilTrueCoroutine”, ale użyj „UntilTrue” dla tego, co obecnie nazywasz „„ UntilTrueCoroutine ”.
matthias_buehlmann
Obawiam się, że naprawdę nie mogę poprzeć tej odpowiedzi. (Wspomniany artykuł to śmieci. Rzeczywiście, to był powód, dla którego zadałem sobie trud napisania „naprawdę słynnego artykułu o programach”, który był na tej „specjalistycznej” stronie, którą SO miała przez chwilę, ale potem została zamknięta). to tylko IEnumerator (czym innym mógłby być? to po prostu powrót, który się powtarza). W pełni wyjaśniam całą sprawę w kilku krótkich zdaniach poniżej.
Fattie
Nie chcę być niegrzeczny ani niemiły, ale cytowany artykuł jest jednym z artykułów napisanych przez osobę, która nie wie nic na temat specjalistyczny i szuka informacji, ponieważ składa „artykuł” na ten temat (ja boję się). Zasadniczo każde zdanie jest całkowicie błędne, błędne i tak naprawdę nie rozumie (niezwykle prostego) systemu w pracy. Szkoda, że ​​jest ogłaszany na 100 000 wyświetleń QA w Internecie: /
Fattie
97

Pierwszy nagłówek poniżej to prosta odpowiedź na pytanie. Dwa następne nagłówki są bardziej przydatne dla codziennego programisty.

Prawdopodobnie nudne szczegóły wdrożeniowe programów

Korekty są wyjaśnione w Wikipedii i w innych miejscach. Tutaj podam tylko kilka szczegółów z praktycznego punktu widzenia. IEnumerator, yielditp. to funkcje języka C #, które są używane w nieco innym celu w Unity.

Mówiąc najprościej, IEnumeratortwierdzenie, że ma zbiór wartości, o które możesz żądać jedna po drugiej, podobnie jak plik List. W języku C # funkcja z podpisem zwracającym IEnumeratornie musi w rzeczywistości tworzyć i zwracać, ale może pozwolić C # na dostarczenie niejawnego IEnumerator. Funkcja może następnie udostępnić zawartość zwróconą IEnumeratorw przyszłości w sposób leniwy za pomocą yield returninstrukcji. Za każdym razem, gdy wywołujący prosi o inną wartość z tego niejawnego IEnumerator, funkcja wykonuje do następnej yield returninstrukcji, która dostarcza następną wartość. Jako produkt uboczny, funkcja zatrzymuje się do momentu zażądania następnej wartości.

W Unity nie używamy ich do dostarczania przyszłych wartości, wykorzystujemy fakt, że funkcja zatrzymuje się. Z powodu tego wyzysku wiele rzeczy dotyczących coroutines w Unity nie ma sensu (co IEnumeratorma z tym coś wspólnego? Co to jest yield? Dlaczego new WaitForSeconds(3)? Itd.). To, co dzieje się „pod maską”, polega na tym, że wartości podawane przez IEnumerator są używane StartCoroutine()do decydowania, kiedy poprosić o następną wartość, która określa, kiedy program ponownie uruchomi się.

Twoja gra Unity jest jednowątkowa (*)

Korekty nie są wątkami. Jest jedna główna pętla Unity i wszystkie te funkcje, które piszesz, są wywoływane w tej samej kolejności przez ten sam główny wątek. Możesz to sprawdzić, umieszczając while(true);w dowolnej funkcji lub programie. Zamrozi to wszystko, nawet edytor Unity. To dowód na to, że wszystko działa w jednym głównym wątku. Ten link, o którym Kay wspomniał w swoim powyższym komentarzu, jest również świetnym źródłem informacji.

(*) Unity wywołuje funkcje z jednego wątku. Tak więc, jeśli sam nie utworzysz wątku, napisany kod jest jednowątkowy. Oczywiście Unity używa innych wątków i jeśli chcesz, możesz tworzyć wątki samodzielnie.

Praktyczny opis programów dla programistów gier

Zasadniczo, gdy dzwonisz StartCoroutine(MyCoroutine()), to dokładnie jak zwykły wywołanie funkcji MyCoroutine(), aż do pierwszego yield return X, gdzie Xjest coś takiego null, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), break, itd. To jest, gdy zaczyna różniące się od funkcji. Unity „zatrzymuje się”, które działa bezpośrednio w tym yield return Xwierszu, kontynuuje inne sprawy i niektóre klatki mijają, a gdy nadejdzie czas, Unity wznawia działanie zaraz po tym wierszu. Zapamiętuje wartości wszystkich zmiennych lokalnych w funkcji. W ten sposób możesz na przykład mieć forpętlę, która zapętla się co dwie sekundy.

Kiedy Unity wznowi twój program, zależy od tego, co Xbyło w twoim yield return X. Na przykład, jeśli użyłeś yield return new WaitForSeconds(3);, zostanie wznowiony po upływie 3 sekund. Jeśli użyłeś yield return StartCoroutine(AnotherCoroutine()), zostanie wznowiony po AnotherCoroutine()całkowitym zakończeniu, co umożliwia zagnieżdżenie zachowań w czasie. Jeśli właśnie użyłeś a yield return null;, zostanie wznowiony zaraz po następnej klatce.

Gazihan Alankus
źródło
2
Szkoda, UnityGems wydaje się być teraz niedostępny. Niektórym osobom na Reddicie udało się zdobyć ostatnią wersję archiwum: web.archive.org/web/20140702051454/http://unitygems.com/…
ForceMagic
3
Jest to bardzo niejasne i może być nieprawidłowe. Oto, jak kod faktycznie kompiluje się i dlaczego działa. To też nie odpowiada na pytanie. stackoverflow.com/questions/3438670/…
Louis Hong
Tak, chyba wyjaśniłem „jak działają programy w Unity” z punktu widzenia programisty gier. Właściwy quastion pytał, co się dzieje pod maską. Jeśli potrafisz wskazać nieprawidłowe części mojej odpowiedzi, chętnie to naprawię.
Gazihan Alankus
4
Zgadzam się co do zwrotu wartości false, dodałem to, ponieważ ktoś skrytykował moją odpowiedź za jej brak i spieszyłem się, aby sprawdzić, czy jest to w ogóle przydatne i po prostu dodałem link. Usunąłem go teraz. Jednak myślę, że jednowątkowe Unity i jak pasują do tego programy nie są oczywiste dla wszystkich. Wielu początkujących programistów Unity, z którymi rozmawiałem, miało bardzo mgliste zrozumienie całej sprawy i skorzystało z takiego wyjaśnienia. Zredagowałem swoją odpowiedź, aby udzielić rzeczowej odpowiedzi na pytanie. Sugestie mile widziane.
Gazihan Alankus
2
Jedność nie jest jednowątkową fwiw. Ma główny wątek, w którym działają metody cyklu życia MonoBehaviour - ale ma również inne wątki. Możesz nawet tworzyć własne wątki.
benthehutt
8

To nie może być prostsze:

Unity (i wszystkie silniki gier) są oparte na klatkach .

Cały punkt, cała racja bytu Jedności polega na tym, że jest oparta na ramie. Silnik robi za Ciebie „każdą klatkę”. (Animuje, renderuje obiekty, zajmuje się fizyką itd.)

Możesz zapytać… „Och, to świetnie. A co jeśli chcę, żeby silnik robił coś za mnie na każdej ramie? Jak mam kazać silnikowi robić takie a takie w ramie?”

Odpowiedź to ...

Właśnie do tego służy „coroutine”.

To takie proste.

Uwaga dotycząca funkcji „Aktualizuj” ...

Po prostu wszystko, co umieścisz w „Aktualizuj”, jest wykonywane w każdej klatce . To jest dosłownie dokładnie to samo, bez żadnej różnicy, od składni coroutine-yield.

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

Nie ma absolutnie żadnej różnicy.

Wątki w żaden sposób nie mają żadnego połączenia z ramkami / procedurami. Nie ma żadnego związku.

Klatki w silniku gry w żaden sposób nie mają żadnego połączenia z wątkami . Są to całkowicie, całkowicie, całkowicie niezwiązane kwestie.

(Często słyszysz, że „Jedność jest jednowątkowa!” Zauważ, że nawet to stwierdzenie jest bardzo zagmatwane. Ramki / programy po prostu nie mają absolutnie żadnego związku z wątkami. Jeśli Unity był wielowątkowy, wielowątkowy lub działał na komputerze kwantowym !! ... po prostu nie miałoby żadnego związku z ramkami / korektami. Jest to całkowicie, całkowicie, absolutnie niezwiązany problem.)

Obliczenia kwantowe w żaden sposób nie mają żadnego związku z ramkami / procedurami. Nie ma żadnego związku.

Po prostu powtórzyć !!

Gdyby Unity był wielowątkowy, hiperwątkowy lub działał na komputerze kwantowym !! ... po prostu nie miałoby żadnego połączenia z ramkami / procedurami. Jest to kwestia całkowicie, całkowicie, absolutnie niezwiązana.

Podsumowując ...

Tak więc, Coroutines / yield to po prostu sposób uzyskiwania dostępu do ramek w Unity. Otóż ​​to.

(I rzeczywiście, jest to absolutnie to samo, co funkcja Update () udostępniana przez Unity.)

To wszystko, co w tym chodzi, to takie proste.

Dlaczego IEnumerator?

Nie może być prostsze: IEnumerator zwraca rzeczy „w kółko”.

(Ta lista rzeczy może mieć określoną długość, np. „10 rzeczy”, lub po prostu ciągnąć się w nieskończoność).

(Możesz zwrócić rzecz, taką jak interger, lub, jak w przypadku każdej funkcji, możesz po prostu „zwrócić”, tj. Zwrócić void.)

Dlatego oczywiście IEnumerator jest tym, czego należy użyć.

W każdym miejscu w .Net, do którego chcesz powracać w kółko, istnieje IEnumerator.

Wszystkie obliczenia oparte na ramkach, z .Net, oczywiście używają IEnumerator do zwracania każdej ramki. Czego jeszcze mógłby użyć?

(Jeśli jesteś nowy w C #, pamiętaj, że IEnumerator jest również używany do zwracania „zwykłych” rzeczy jeden po drugim, takich jak po prostu elementy w tablicy itp.)

Fattie
źródło
Dzięki! Ale Twoja odpowiedź wyjaśnia, jak używać programów, a nie jak działają one za kulisami.
Ghopper21,
1
Powiedziałeś: „Nie ma absolutnie żadnej różnicy”. Dlaczego więc Unity stworzyło Coroutines, skoro mają już dokładnie działającą implementację Update()? Mam na myśli, że powinna istnieć przynajmniej niewielka różnica między tymi dwiema implementacjami a ich przypadkami użycia, co jest dość oczywiste.
Leandro Gecozo
@LeandroGecozo - Aktualizacja to tylko głupie uproszczenie, które dodali dla początkujących, nie ma tajemnicy. Jednak jedyną różnicą jest to, że wiesz, że aktualizacja działa w określonej kolejności (można łatwo sprawdzić niejasną kolejność, w której występują różne wywołania ramek).
Fattie
@ Ghopper21 - w pełni wyjaśnione na końcu; to trywialne.
Fattie
W tej odpowiedzi jest wiele błędów. Korekty są bardziej złożone i zawierają dużo więcej zastrzeżeń niż to. Jeśli kiedykolwiek używałeś ich tylko do prostych rzeczy - świetnie! Dobra robota! Cieszę się! - ale przegapiłeś sens tego, co robią i jak działają.
Adam
6

Zagłębiłem się ostatnio w to, napisałem tutaj post - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - który rzucił światło na elementy wewnętrzne (z gęstymi przykładami kodu), podstawowy IEnumeratorinterfejs, i jak jest używany w korektach.

Używanie do tego celu modułów wyliczających kolekcje nadal wydaje mi się nieco dziwne. Jest to odwrotność tego, do czego czują się wyliczający. Punktem modułów wyliczających jest zwracana wartość przy każdym dostępie, ale punktem Coroutines jest kod znajdujący się pomiędzy zwracaną wartością. W tym kontekście rzeczywista wartość zwracana jest bezcelowa.

Geri Borbás
źródło
-1

Podstawowymi funkcjami w Unity, które otrzymujesz automatycznie, są funkcja Start () i funkcja Update (), więc funkcje Coroutine są zasadniczo takie same, jak funkcje Start () i Update (). Dowolną starą funkcję func () można wywołać w ten sam sposób, w jaki można wywołać Coroutine. Unity oczywiście wyznaczyła pewne granice dla Coroutines, które odróżniają je od zwykłych funkcji. Jedna różnica jest zamiast

  void func()

Ty piszesz

  IEnumerator func()

dla programów. W ten sam sposób możesz kontrolować czas w normalnych funkcjach za pomocą linii kodu, takich jak

  Time.deltaTime

Program ma określony sposób kontrolowania czasu.

  yield return new WaitForSeconds();

Chociaż nie jest to jedyna rzecz, którą można zrobić w IEnumerator / Coroutine, jest to jedna z przydatnych rzeczy, do których są używane programy Coroutine. Musisz zbadać skryptowe API Unity, aby poznać inne konkretne zastosowania Coroutines.

Alexhawkburr
źródło
-1

StartCoroutine to metoda wywoływania funkcji IEnumerator. Jest to podobne do wywołania prostej funkcji void, z tą różnicą, że używasz jej w funkcjach IEnumerator. Ten typ funkcji jest wyjątkowy, ponieważ umożliwia użycie specjalnej funkcji zysku , pamiętaj, że musisz coś zwrócić. O ile wiem. Tutaj napisałem prostą grę Flicker Over text w jedności

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

Następnie wywołałem go z samego IEnumeratora

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

Jak widać, jak użyłem metody StartCoroutine (). Mam nadzieję, że jakoś pomogłem. Sam jestem początkującym, więc jeśli mnie poprawisz lub docenisz, każda informacja zwrotna byłaby świetna.

Deepanshu Mishra
źródło