Rozumiem zasadę coroutines. Wiem, jak sprawić, by wzorzec StartCoroutine
/ yield return
wzorzec działał w C # w Unity, np. Wywołać metodę zwracającą się IEnumerator
przez StartCoroutine
iw 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 StartCoroutine
naprawdę robi? Co IEnumerator
jest WaitForSeconds
powrocie? W jaki sposób StartCoroutine
zwraca 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)?
IEnumerator
/IEnumerable
(lub generyczne odpowiedniki) i które zawierająyield
słowo kluczowe. Wyszukaj iteratory.Odpowiedzi:
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 .
function LongComputation() { while(someCondition) { /* Do a chunk of work */ // Pause here and carry on next frame yield; } }
IEnumerator LongComputation() { while(someCondition) { /* Do a chunk of work */ // Pause here and carry on next frame yield return null; } }
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; }
IEnumerator e = TellMeASecret(); while(e.MoveNext()) { }
IEnumerator e = TellMeASecret(); while(e.MoveNext()) { // If they press 'Escape', skip the cutscene if(Input.GetKeyDown(KeyCode.Escape)) { break; } }
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;
YieldInstruction y; if(something) y = null; else if(somethingElse) y = new WaitForEndOfFrame(); else y = new WaitForSeconds(1.0f); yield return y;
IEnumerator DoSomething() { /* ... */ } IEnumerator DoSomethingUnlessInterrupted() { IEnumerator e = DoSomething(); bool interrupted = false; while(!interrupted) { e.MoveNext(); yield return e.Current; interrupted = HasBeenInterrupted(); } }
IEnumerator UntilTrueCoroutine(Func fn) { while(!fn()) yield return null; } Coroutine UntilTrue(Func fn) { return StartCoroutine(UntilTrueCoroutine(fn)); } IEnumerator SomeTask() { /* ... */ yield return UntilTrue(() => _lives < 3); /* ... */ }
źródło
IEnumerator SomeTask() { yield return UntilTrueCoroutine(() => _lives < 3); }
chociaż prawdopodobnie nie nazwałbyś metody „UntilTrueCoroutine”, ale użyj „UntilTrue” dla tego, co obecnie nazywasz „„ UntilTrueCoroutine ”.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
,yield
itp. to funkcje języka C #, które są używane w nieco innym celu w Unity.Mówiąc najprościej,
IEnumerator
twierdzenie, że ma zbiór wartości, o które możesz żądać jedna po drugiej, podobnie jak plikList
. W języku C # funkcja z podpisem zwracającymIEnumerator
nie musi w rzeczywistości tworzyć i zwracać, ale może pozwolić C # na dostarczenie niejawnegoIEnumerator
. Funkcja może następnie udostępnić zawartość zwróconąIEnumerator
w przyszłości w sposób leniwy za pomocąyield return
instrukcji. Za każdym razem, gdy wywołujący prosi o inną wartość z tego niejawnegoIEnumerator
, funkcja wykonuje do następnejyield return
instrukcji, 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
IEnumerator
ma z tym coś wspólnego? Co to jestyield
? Dlaczegonew WaitForSeconds(3)
? Itd.). To, co dzieje się „pod maską”, polega na tym, że wartości podawane przez IEnumerator są używaneStartCoroutine()
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 funkcjiMyCoroutine()
, aż do pierwszegoyield return X
, gdzieX
jest coś takiegonull
,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 tymyield return X
wierszu, 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ćfor
pętlę, która zapętla się co dwie sekundy.Kiedy Unity wznowi twój program, zależy od tego, co
X
było w twoimyield 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 poAnotherCoroutine()
całkowitym zakończeniu, co umożliwia zagnieżdżenie zachowań w czasie. Jeśli właśnie użyłeś ayield return null;
, zostanie wznowiony zaraz po następnej klatce.źródło
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.)
źródło
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.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
IEnumerator
interfejs, i jak jest używany w korektach.źródło
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
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.
źródło
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.
źródło