Coroutine Unity3D yang sering dirujuk dalam tautan detail sudah mati. Karena itu disebutkan dalam komentar dan jawaban saya akan memposting konten artikel di sini. Konten ini berasal dari cermin ini .
Unity3D coroutine secara rinci
Banyak proses dalam game berlangsung selama beberapa frame. Anda punya proses 'padat', seperti merintis jalan, yang bekerja keras setiap frame tetapi terbelah di beberapa frame sehingga tidak terlalu mempengaruhi framerate. Anda memiliki proses 'jarang', seperti pemicu gameplay, yang tidak melakukan apa pun pada sebagian besar frame, tetapi kadang-kadang diminta untuk melakukan pekerjaan penting. Dan Anda punya berbagai macam proses di antara keduanya.
Setiap kali Anda membuat proses yang akan berlangsung di beberapa frame - tanpa multithreading - Anda perlu menemukan beberapa cara untuk memecah pekerjaan menjadi potongan-potongan yang dapat dijalankan satu per frame. Untuk algoritme apa pun dengan loop pusat, cukup jelas: pathfinder A *, misalnya, dapat disusun sedemikian rupa sehingga mempertahankan daftar simpulnya secara semi-permanen, hanya memproses beberapa node dari daftar terbuka setiap frame, alih-alih mencoba untuk melakukan semua pekerjaan sekaligus. Ada beberapa penyeimbangan yang harus dilakukan untuk mengelola latensi - setelah semua, jika Anda mengunci framerate Anda pada 60 atau 30 frame per detik, maka proses Anda hanya akan mengambil 60 atau 30 langkah per detik, dan itu mungkin menyebabkan proses hanya mengambil keseluruhan terlalu lama. Desain yang rapi mungkin menawarkan unit kerja sekecil mungkin di satu tingkat - mis proses satu simpul * A - dan lapisan di atas cara pengelompokan bekerja bersama menjadi potongan yang lebih besar - misalnya tetap memproses simpul A * untuk X milidetik. (Beberapa orang menyebut ini 'timeslicing', walaupun saya tidak).
Namun, membiarkan pekerjaan dibubarkan dengan cara ini berarti Anda harus mentransfer status dari satu frame ke frame berikutnya. Jika Anda memecah algoritma iteratif ke atas, maka Anda harus mempertahankan semua status yang dibagikan di seluruh iterasi, serta cara melacak mana iterasi yang akan dilakukan selanjutnya. Itu biasanya tidak terlalu buruk - desain 'kelas pathfinder A *' cukup jelas - tetapi ada kasus lain juga yang kurang menyenangkan. Terkadang Anda akan menghadapi perhitungan panjang yang melakukan berbagai jenis pekerjaan dari bingkai ke bingkai; objek yang menangkap keadaan mereka dapat berakhir dengan kekacauan besar 'penduduk setempat' yang semi-berguna, yang disimpan untuk meneruskan data dari satu frame ke frame berikutnya. Dan jika Anda berurusan dengan proses yang jarang, Anda seringkali harus menerapkan mesin negara kecil hanya untuk melacak ketika pekerjaan harus dilakukan sama sekali.
Bukankah lebih rapi jika, daripada harus secara eksplisit melacak semua keadaan ini di beberapa frame, dan bukannya harus multithread dan mengelola sinkronisasi dan penguncian dan sebagainya, Anda bisa menulis fungsi Anda sebagai satu potongan kode, dan tandai tempat-tempat tertentu di mana fungsi harus 'berhenti' dan melanjutkan di lain waktu?
Persatuan - bersama dengan sejumlah lingkungan dan bahasa lain - menyediakan ini dalam bentuk Coroutine.
Bagaimana penampilan mereka? Dalam "Unityscript" (Javascript):
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
Dalam C #:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
Bagaimana mereka bekerja? Biar saya katakan, cepat, bahwa saya tidak bekerja untuk Unity Technologies. Saya belum melihat kode sumber Unity. Saya belum pernah melihat nyali mesin coroutine Unity. Namun, jika mereka telah mengimplementasikannya dengan cara yang sangat berbeda dari apa yang akan saya jelaskan, maka saya akan cukup terkejut. Jika ada orang dari UT yang ingin berpura-pura dan berbicara tentang cara kerjanya, maka itu akan sangat bagus.
Petunjuk besar ada di versi C #. Pertama, perhatikan bahwa tipe pengembalian untuk fungsi adalah IEnumerator. Dan kedua, perhatikan bahwa salah satu pernyataannya adalah imbal hasil. Ini berarti bahwa hasil harus berupa kata kunci, dan karena dukungan C # Unity adalah vanilla C # 3.5, itu harus kata kunci vanilla C # 3.5. Memang, ini dia di MSDN - berbicara tentang sesuatu yang disebut 'blok iterator.' Jadi apa yang terjadi?
Pertama, ada tipe IEnumerator ini. Tipe IEnumerator bertindak seperti kursor di atas suatu urutan, menyediakan dua anggota signifikan: Arus, yang merupakan properti yang memberi Anda elemen yang kursornya selesai, dan MoveNext (), sebuah fungsi yang bergerak ke elemen berikutnya dalam urutan. Karena IEnumerator adalah antarmuka, itu tidak menentukan secara tepat bagaimana anggota ini diimplementasikan; MoveNext () bisa saja menambahkan satu keCurrent, atau bisa memuat nilai baru dari file, atau bisa mengunduh gambar dari Internet dan hash dan menyimpan hash baru di Current ... atau bahkan dapat melakukan satu hal untuk yang pertama elemen dalam urutan, dan sesuatu yang sama sekali berbeda untuk yang kedua. Anda bahkan dapat menggunakannya untuk menghasilkan urutan yang tidak terbatas jika diinginkan. MoveNext () menghitung nilai selanjutnya dalam urutan (menghasilkan false jika tidak ada lagi nilai),
Biasanya, jika Anda ingin mengimplementasikan antarmuka, Anda harus menulis sebuah kelas, mengimplementasikan anggota, dan sebagainya. Blok Iterator adalah cara yang mudah untuk mengimplementasikan IEnumerator tanpa semua kerumitan - Anda cukup mengikuti beberapa aturan, dan implementasi IEnumerator dihasilkan secara otomatis oleh kompiler.
Blok iterator adalah fungsi reguler yang (a) mengembalikan IEnumerator, dan (b) menggunakan kata kunci hasil. Jadi apa yang sebenarnya dilakukan kata kunci hasil? Ini menyatakan apa nilai selanjutnya dalam urutan itu - atau bahwa tidak ada lagi nilai. Titik di mana kode menemukan imbal hasil X atau hasil istirahat adalah titik di mana IEnumerator.MoveNext () harus berhenti; imbal hasil X menyebabkan MoveNext () mengembalikan true andCurrent untuk diberi nilai X, sedangkan imbal hasil menyebabkan MoveNext () mengembalikan false.
Sekarang, inilah triknya. Tidak masalah apa nilai aktual yang dikembalikan oleh urutan. Anda dapat memanggil MoveNext () berulang-ulang, dan mengabaikan Current; perhitungan akan tetap dilakukan. Setiap kali MoveNext () dipanggil, blok iterator Anda berjalan ke pernyataan 'hasil' berikutnya, terlepas dari ekspresi apa yang sebenarnya dihasilkannya. Jadi, Anda dapat menulis sesuatu seperti:
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;
}
dan apa yang sebenarnya Anda tulis adalah blok iterator yang menghasilkan urutan panjang dari nilai nol, tetapi yang penting adalah efek samping dari pekerjaan yang dilakukan untuk menghitungnya. Anda dapat menjalankan coroutine ini menggunakan loop sederhana seperti ini:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
Atau, lebih bermanfaat, Anda dapat mencampurnya dengan pekerjaan lain:
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
Semuanya ada dalam pengaturan waktu Seperti yang Anda lihat, setiap pernyataan pengembalian hasil harus memberikan ekspresi (seperti nol) sehingga blok iterator memiliki sesuatu untuk benar-benar ditetapkan ke IEnumerator.Current. Urutan nol yang panjang tidak terlalu berguna, tetapi kami lebih tertarik pada efek sampingnya. Bukan kita?
Sebenarnya ada sesuatu yang berguna yang bisa kita lakukan dengan ungkapan itu. Bagaimana jika, alih-alih hanya menghasilkan nol dan mengabaikannya, kami menghasilkan sesuatu yang ditunjukkan ketika kami berharap perlu melakukan lebih banyak pekerjaan? Seringkali kita perlu membawa langsung pada frame berikutnya, tentu saja, tetapi tidak selalu: akan ada banyak waktu di mana kita ingin melanjutkan setelah animasi atau suara selesai diputar, atau setelah sejumlah waktu tertentu telah berlalu. Sementara itu (playingAnimation) menghasilkan pengembalian nol; konstruksinya agak membosankan, bukan begitu?
Unity mendeklarasikan tipe dasar YieldInstruction, dan menyediakan beberapa tipe turunan konkret yang mengindikasikan jenis tunggu tertentu. Anda mendapatkan WaitForSeconds, yang melanjutkan coroutine setelah jumlah waktu yang ditentukan telah berlalu. Anda punya WaitForEndOfFame, yang melanjutkan coroutine pada titik tertentu di frame yang sama. Anda memiliki tipe Coroutine itu sendiri, yang, ketika coroutine A menghasilkan coroutine B, menjeda coroutine A sampai setelah coroutine B selesai.
Seperti apa tampilan ini dari sudut pandang runtime? Seperti yang saya katakan, saya tidak bekerja untuk Unity, jadi saya belum pernah melihat kode mereka; tapi saya membayangkan itu akan terlihat sedikit seperti ini:
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;
Tidak sulit membayangkan berapa banyak subtipe YieldInstruction yang dapat ditambahkan untuk menangani kasus lain - dukungan level mesin untuk sinyal, misalnya, dapat ditambahkan, dengan Instruksi YieldInstruksi WaitForSignal ("SignalName") yang mendukungnya. Dengan menambahkan lebih banyak YieldInstructions, coroutine itu sendiri dapat menjadi lebih ekspresif - imbal hasil baru WaitForSignal ("GameOver") lebih bagus untuk dibaca daripada itu (! Sinyal. MemilikiFired ("GameOver")) menghasilkan pengembalian nol, jika Anda bertanya kepada saya, terlepas dari fakta bahwa melakukannya di mesin bisa lebih cepat daripada melakukannya di skrip.
Beberapa konsekuensi yang tidak jelas. Ada beberapa hal berguna tentang semua ini yang kadang-kadang orang lewatkan, yang saya pikir harus saya tunjukkan.
Pertama, pengembalian hasil hanya menghasilkan ekspresi - ekspresi apa pun - dan YieldInstruction adalah tipe reguler. Ini berarti Anda dapat melakukan hal-hal seperti:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
Baris spesifik menghasilkan return WaitForSeconds baru (), menghasilkan return WaitForEndOfFrame baru (), dll, adalah umum, tetapi mereka sebenarnya bukan bentuk khusus dalam hak mereka sendiri.
Kedua, karena coroutine ini hanya blok iterator, Anda dapat beralih sendiri jika Anda mau - Anda tidak perlu memiliki mesin melakukannya untuk Anda. Saya telah menggunakan ini untuk menambahkan kondisi interupsi ke coroutine sebelumnya:
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
Ketiga, fakta bahwa Anda dapat menghasilkan pada coroutine lain dapat semacam memungkinkan Anda untuk menerapkan Instruksi Produksi Anda sendiri, meskipun tidak berkinerja seolah-olah mereka dilaksanakan oleh mesin. Sebagai contoh:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
Namun, saya tidak akan benar-benar merekomendasikan hal ini - biaya memulai Coroutine sedikit berat untuk seleraku.
Kesimpulan Saya harap ini menjelaskan sedikit dari apa yang sebenarnya terjadi ketika Anda menggunakan Coroutine in Unity. Blok iterator C # adalah konstruk kecil yang asyik, dan bahkan jika Anda tidak menggunakan Unity, mungkin Anda akan merasa berguna untuk memanfaatkannya dengan cara yang sama.
IEnumerator
/IEnumerable
(atau setara dengan generik) dan yang mengandungyield
kata kunci. Cari iterator.