Kako originalni predložak StartCoroutine / yield zapravo funkcionira u jedinstvu?

Razumijem princip koroutina. Znam kako, na primjer, dobiti standardni StartCoroutine / yield return za rad na C # u StartCoroutine . nazovite metodu koja vraća IEnumerator preko StartCoroutine , iu ovoj metodi učinite nešto, napravite yield return new WaitForSeconds(1); Čekajte malo, a zatim učinite nešto drugo.

Moje pitanje glasi: što se doista događa iza scene? Što čini StartCoroutine stvarno? Što IEnumerator vraća WaitForSeconds ? Kako StartCoroutine vraća kontrolu na dio "nečeg drugog" nazvane metode? Kako je sve to u interakciji s modelom konkurentnosti Unity (gdje se mnoge stvari događaju u isto vrijeme bez upotrebe coroutina)?

83
17 окт. postavio Ghopper21 17. list . 2012-10-17 13:30 '12 u 13:30 2012-10-17 13:30
@ 4 odgovora

Veza povezanog s Unity3D koroutinama detaljno je onemogućena. Jer u komentarima i odgovorima ovdje spominjem sadržaj članka. Ovaj sadržaj dolazi iz ovog zrcala .


Detaljan opis Unity3D

Mnogi procesi u igrama odvijaju se u nekoliko okvira. Imate "guste procese, kao što je pathfinding", koji rade svaki okvir, ali su podijeljeni u nekoliko okvira, tako da ne utječu previše na brzinu kadra. Imate "rijetke procese, kao što su okidači igranja", koji ne rade ništa u većini okvira, ali ponekad zahtijevaju kritički rad. I između vas postoje različiti procesi.

Kad god stvorite proces koji će se izvoditi na više okvira - bez višedretvenosti - morate pronaći način da razbijete rad na dijelove koje možete pokrenuti jedan po jedan. Za bilo koji algoritam sa središnjom petljom, to je prilično očito: na primjer, A * pathfinder može biti strukturiran na takav način da održava svoje popise čvorova polu-trajno, obrađujući samo nekoliko čvorova iz otvorenog popisa svaki okvir, umjesto toga pokušavajući sve raditi u isto vrijeme. Postoji balansiranje za kontrolu latencije - nakon svega, ako blokirate brzinu snimanja od 60 ili 30 slika u sekundi, proces će trajati 60 ili 30 koraka u sekundi, što može dovesti do toga da proces traje predugo. i općenito. Optimalni dizajn može ponuditi najmanju moguću jedinicu rada na istoj razini - na primjer, obraditi jedan A * čvor - i gornji sloj, način grupiranja, raditi zajedno u većim dijelovima - na primjer, nastaviti procesirati A-čvorove za X milisekundi. (Neki to nazivaju "vremenski", iako ne znam).

Međutim, dopuštajući da se rad razbije na ovaj način, morate prenijeti stanje iz jednog okvira u drugi. Ako prekinete algoritam iteracije, morate spremiti cijelu državu podijeljenu između iteracija, kao i alat za praćenje za naknadnu iteraciju. To obično nije tako loše - dizajn klase “A * Pathfinder” je prilično očigledan, ali postoje i drugi slučajevi koji su manje ugodni. Ponekad ćete naići na duge izračune koji izvode različite vrste rada od okvira do okvira; objekt koji je zauzeo njegovo stanje može biti u velikom neredu polu-korisnih "lokalnih stanovnika", namijenjenih za prijenos podataka iz jednog okvira u drugi. A ako se bavite oskudnim procesom, često morate primijeniti mali krajnji stroj kako biste pratili kada uopće treba raditi.

Ne bi bilo uredno ako umjesto da izričito pratimo sve ovo stanje u nekoliko okvira, i umjesto da imate višedretvenost i upravljanje sinkronizacijom i zaključavanjem, itd., Možete jednostavno napisati svoju funkciju kao fragment jednog koda i označite određena mjesta na kojima bi funkcija trebala "pauzirati i nastaviti kasnije"

Jedinstvo - zajedno s nizom drugih medija i jezika - osigurava to u obliku Corouta.

Kako izgledaju? U Unityscriptu (Javascript):

 function LongComputation() { while(someCondition) {  // Pause here and carry on next frame yield; } } 

U C #:

 IEnumerator LongComputation() { while(someCondition) {  // Pause here and carry on next frame yield return null; } } 

Kako oni rade? Dopustite mi da kažem, brzo, da ne radim za Unity Technologies. Nisam vidio izvorni kod Unity. Nikad nisam vidjela crijeva Jedinstvenog motora. Međutim, ako su ga implementirali na način koji je radikalno drugačiji od onoga što ću opisati, onda ću biti jako iznenađen. Ako netko iz UT želi nazvati i razgovarati o tome kako to radi, onda će biti super.

Velike tipke su u verziji C #. Prvo, imajte na umu da je povratni tip za funkciju IEnumerator. I, drugo, imajte na umu da je jedna od izjava povratak. To znači da profitabilnost mora biti ključna riječ, a budući da je podrška za Unitys C # vanilija C # 3.5, ona mora biti ključna riječ C # 3.5 vanilije. Doista, ovdje je u MSDN - govoreći o nečemu što se zove "iteratorski blokovi". Što se događa?

Prvo, ovaj tip IEnumeratora. Tip IEnumerator djeluje kao pokazivač iznad niza, pružajući dva značajna elementa: Current, što je svojstvo koje vam daje element koji je sada u pokazivaču, a MoveNext () je funkcija koja se pomiče na sljedeći element u nizu. Budući da je IEnumerator sučelje, ne točno određuje kako se ti elementi implementiraju; MoveNext () može jednostavno dodati jedan toCurrent ili učitati novu vrijednost iz datoteke ili preuzeti sliku s interneta te je pohraniti i spremiti novi hash u Current ... ili čak napraviti jedan za prvi element u nizu i nešto sasvim drugo za drugi , Možete ga koristiti čak i za generiranje beskonačnog niza, ako želite. MoveNext () izračunava sljedeću vrijednost u nizu (vraća false ako nema više vrijednosti), a Current vraća vrijednost koju je izračunala.

Obično, ako želite implementirati sučelje, morate napisati klasu, elemente implementacije itd. Iterator blokovi su prikladan način za implementaciju IEnumeratora bez ikakvih komplikacija - samo slijedite nekoliko pravila, a implementaciju IEnumeratora automatski generira kompajler.

Iterator je regularna funkcija koja (a) vraća IEnumerator i (b) koristi ključnu riječ yield. Dakle, što zapravo donosi ključna riječ? On izjavljuje da je sljedeća vrijednost u nizu - ili da više nema vrijednosti. Točka u kojoj se kôd vraća povratnom povratku ili prekidu prekida je točka na kojoj bi se trebao zaustaviti IEnumerator.MoveNext (); vraćajući dohodak X uzrokuje da MoveNext () vrati true i Current kojem je X dodijeljen, dok break break poziva MoveNext () da se vrati false.

Ovo je trik. Nije važno kakve su stvarne vrijednosti koje vraća sekvenca. Možete ponovno pozvati MoveNext () i zanemariti Current; proračuni će i dalje biti izvedeni. Svaki put kada se pozove MoveNext (), vaš se iteratorski blok pokreće u sljedeći izraz izvještavanja, bez obzira na to koji izraz zapravo daje. Tako možete napisati nešto poput:

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

i ono što ste zapravo napisali je iteratorni blok koji generira dugi niz nultih vrijednosti, ali su nuspojave rada koje obavlja da bi ih izračunale značajne. Ovaj coroutine možete pokrenuti pomoću jednostavne petlje:

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

Ili, još korisnije, možete ga pomiješati s drugim poslom:

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

Sve je u vremenskoj liniji Kao što ste vidjeli, svaki povratni izvještaj o povratku mora sadržavati izraz (na primjer, null), tako da blok iteratora mora zapravo dodijeliti nešto IEnumerator.Current. Dugi niz nula nije baš od pomoći, ali je više zainteresiran za nuspojave. Arent mi?

Zapravo, nešto prikladno možemo učiniti s ovim izrazom. Što ako umjesto da se odreknemo nule i ignoriramo to, dali smo nešto što je naznačeno kad očekujemo da vi radite više? Često morate nastupiti na sljedećem kadru, naravno, ali ne uvijek: bit će mnogo puta kada želimo nastaviti igru ​​nakon što animacija ili zvuk završi igru, ili nakon određenog vremena. Oni, dok (playAnimation) vraćaju povratak null; Dizajni su pomalo zamorni, zar ne?

Unity deklarira osnovni tip YieldInstruction i daje nekoliko specifičnih izvedenih tipova koji ukazuju na određene tipove čekanja. Imate WaitForSeconds, koji nastavlja koroutin nakon određenog vremenskog razdoblja. Imate WaitForEndOfFrame koji nastavlja koroutinu na određenoj točki kasnije u istom okviru. Imate sam Coroutine tip, koji, kada coroutine A daje coroutine B, suspendira coroutine A dok koroutin B. ne završi.

Kako to izgleda u smislu vremena izvođenja? Kao što sam rekao, ne radim za Jedinstvo, tako da nikad nisam vidio njihov kod; ali mislim da bi moglo izgledati ovako:

 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  } unblockedCoroutines = shouldRunNextFrame; 

Nije teško zamisliti kako dodati dodatne podvrste YieldInstruction za obradu drugih slučajeva - na primjer, podrška za signal na razini motora može se dodati s podrškom za YieldInstruction WaitForSignal (SignalName). Dodavanjem više YieldInstructions, coroutini sami mogu postati izražajniji - povratak povratka novi WaitForSignal ("GameOver") je bolje pročitati (! Signals.HasFired ("GameOver")) povratak null, ako mene pitate, apsolutno bez obzira što činiti. ovo u motoru može biti brže nego u skripti.

Ima nekoliko očitih posljedica. Postoji nekoliko korisnih stvari o svemu što ljudi ponekad propuste, što sam mislio da bih trebao istaknuti.

Prvo, prinos povrata jednostavno daje izraz - bilo koji izraz - i YieldInstruction je uobičajeni tip. To znači da možete raditi sljedeće stvari:

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

Specifični nizovi vraćaju novi WaitForSeconds (), prinos povratak novi WaitForEndOfFrame (), itd., Su uobičajeni, ali oni zapravo nisu posebni oblici sami po sebi.

Drugo, budući da su te koroutine samo blokovi iteratora, možete ih sami ponavljati, ako želite, ne trebate motor da biste to učinili umjesto vas. Koristio sam ovo za dodavanje uvjeta prekida koroutini prije:

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

Treće, činjenica da možete dati na druge koroutine može vam omogućiti da implementirate vlastite YieldInstrukcije, iako ne na isti način kao da ih je motor implementirao. Na primjer:

 IEnumerator UntilTrueCoroutine(Func fn) { while(!fn()) yield return null; } Coroutine UntilTrue(Func fn) { return StartCoroutine(UntilTrueCoroutine(fn)); } IEnumerator SomeTask() {  yield return UntilTrue(() => _lives < 3);  } 

međutim, ne bih preporučio ovo - trošak pokretanja Coroutine-a je malo težak za mene.

Zaključak Nadamo se da će ovo objasniti nešto što se doista događa kada koristite Coroutine u jedinstvu. C # crator blokovi su mali groovy konstrukti, pa čak i ako ne koristite Unity, možda će vam biti korisno da ih koristite na isti način.

56
09 сент. Odgovor daje James McMahon rujan 09 2015-09-09 03:09 '15 u 3:09 2015-09-09 03:09

Prvi naslov u nastavku je izravan odgovor na pitanje. Ova dva zaglavlja su korisnija za svakodnevnog programera.

Možda bušenje informacija o izvedbi Coroutines naredbi

Coroutine su objašnjene u Wikipediji i drugdje. Ovdje samo dajem neke detalje s praktičnog stajališta. IEnumerator , yield , itd. C # funkcije koje se koriste u neku drugu svrhu u Jedinstvu.

Jednostavno rečeno, IEnumerator tvrdi da ima skup vrijednosti koje možete upitati jednu po jednu, poput List . U C #, funkcija s potpisom za vraćanje IEnumerator ne bi trebala stvarati i vraćati jedan, ali može dopustiti C # da osigura implicitni IEnumerator . Funkcija tada može pružiti sadržaj vraćenog IEnumerator u budućnosti na lijeni način koristeći yield return . Svaki put kada pozivatelj zatraži drugačiju vrijednost od implicitnog IEnumerator , funkcija obavlja yield return na sljedeći yield return , koji daje sljedeću vrijednost. Kao nusproizvod, funkcija se zaustavlja do sljedećeg zahtjeva.

U Jedinstvu, ne koristimo ih da bismo osigurali buduće vrijednosti, koristimo činjenicu da je funkcija obustavljena. Zbog tog iskorištavanja, u Unityu postoji mnogo stvari o koroutinima koje nemaju smisla (da IEnumerator ima nešto s nečim? Što je yield ? Zašto new WaitForSeconds(3) ? Itd.). Što se događa ispod haube, vrijednosti koje pružate putem IEnumeratora koristi StartCoroutine() da bi odlučio kada zatražiti sljedeću vrijednost, koja određuje kada će se vaš coroutine ponovno zaustaviti.

Vaša igra Jedinstvena pojedinačna nit (*)

border=0

Korotini nisu potoci. Postoji jedna glavna petlja Unity, a sve one funkcije koje pišete zovu se istim glavnim nitima po redu. To možete provjeriti stavljanjem while(true); na bilo koju od vaših funkcija ili koroutina. To će sve zamrznuti, čak i urednik Unity. To znači da sve radi u jednoj glavnoj niti. Ova veza , koju Kay spominje u svom prethodnom komentaru, također je veliki izvor.

(*) Unity poziva vaše funkcije iz jedne niti. Dakle, ako sami ne stvorite stream, kod koji ste napisali je jednostruki. Naravno, Unity koristi i druge teme, a možete i sami stvoriti niti ako želite.

Praktičan opis programa Coroutines za programere igara

U osnovi, kada pozovete StartCoroutine(MyCoroutine()) , to točno podsjeća na uobičajeni poziv na MyCoroutine() , prije prvog yield return X , gdje je X nešto kao null , new WaitForSeconds(3) , StartCoroutine(AnotherCoroutine()) , break , itd. To se događa kada se razlikuje od funkcije. Jedinstvo "obustavlja" ovu funkciju pravo u ovom retku yield return X , nastavlja raditi s drugim poslovanjem, a neki okviri prolaze, a opet, Unity nastavlja ovu funkciju odmah nakon te linije. Ona pamti vrijednosti za sve lokalne varijable u funkciji. Dakle, možete imati for petlju, koja ciklusa svake dvije sekunde, na primjer.

Kada Jedinstvo obnovi vašu koroutinu, ovisi o tome što je X bio u vašem yield return X Na primjer, ako ste koristili yield return new WaitForSeconds(3); , nastavlja se nakon 3 sekunde. Ako ste koristili yield return StartCoroutine(AnotherCoroutine()) , on se nastavlja nakon dovršetka AnotherCoroutine() , što vam omogućuje da ugradite ponašanje na vrijeme. Ako ste upravo koristili yield return null; , nastavlja se odmah u sljedećem okviru.

80
14 марта '13 в 18:08 2013-03-14 18:08 odgovor je dao Gazi Alankus 14. ožujka '13 u 6:08 2013-03-14 18:08

Ne može biti lakše:

Jedinstvo (i svi motori igre) temelje se na okvirima.

Čitava poanta, cijela točka Jednosti, je da se temelji na okvirima. Motor radi za vas "svaki okvir." (Animacija, prikazivanje objekta, fizika, itd.)

Vi svibanj pitati: "Oh, to je super. Što ako želim motor da učini nešto za mene u svakom okviru? Kako mogu dobiti motor da učini takav i takav u okviru?"

Odgovor je ...

To je upravo coroutine.

To je jednostavno.

I razmislite o tome ....

Znate funkciju "Osvježi". Vrlo jednostavno, sve što umetnete tamo izvršava se svaki okvir. To je doslovno ista stvar, bez razlike, od sintakse prinosa koroutina.

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

Nema razlike.

Fusnota: kao što svi bilježe, Unity jednostavno nema niti . "Okviri" u Jedinstvu ili u bilo kojem igraćem motoru nema nikakve veze s potocima.

Coroutines / yield je jednostavno pristup okvirima u jedinstvu. Jeste. (Doista, to je potpuno isto kao i funkcija Update () koju pruža Unity.) Sve što joj je potrebno je jednostavno.

6
09 февр. Odgovor daje Fattie 09. veljače. 2016-02-09 01:05 '16 u 1:05 2016-02-09 01:05

U posljednje će se vrijeme uklopiti u ovo, napisao poruku ovdje - http://eppz.eu/blog/understanding-ienumerator-inunity-3d/ - osvijetlio utrobu (s primjerima gustog koda), osnovno sučelje IEnumerator i kako koristi se za koroutine.

Korištenje brojača zbirki za ovu svrhu još mi se čini pomalo čudnim. To je inverzno za ono što su koderi. Točka nabrajanja je povratna vrijednost za svaki pristup, ali točka Coroutines je kod između povratne vrijednosti. Stvarna povratna vrijednost u ovom kontekstu nema smisla.

4
30 янв. Odgovor daje Geri 30. siječnja 2015-01-30 13:37 '15 u 13:37 2015-01-30 13:37

Ostala pitanja o oznakama ili Postavite pitanje