Mengapa saya tidak terjebak dalam lingkaran

8

Saya baru mengenal Unity. Saya belajar coroutine dan saya menulis ini.

private void Fire()
{
    if(Input.GetButtonDown("Fire1"))
    {
        StartCoroutine(FireContinuously());
    }
    if(Input.GetButtonUp("Fire1"))
    {
        StopAllCoroutines();
    }
}

IEnumerator FireContinuously()
{
    while(true)
    {
        GameObject laser = Instantiate(LaserPrefab, transform.position, Quaternion.identity) as GameObject;
        laser.GetComponent<Rigidbody2D>().velocity = new Vector2(0, 10f);
        yield return new WaitForSeconds(firetime);
    }
}

Ketika tombol ditekan coroutine dipanggil dan memasuki loop 'while'. Ketika saya meninggalkan tombol itu menghentikan coroutine. Tidakkah seharusnya macet di loop 'while' karena itu adalah loop tak terbatas? Mengapa?

otak bayi
sumber
Saya baru saja kembali ke Unity sendiri, saya perhatikan bahwa metode input mengambil string "Fire1", apakah itu sesuatu yang dapat Anda atur di mesin untuk memungkinkan remapping kunci daripada mengetik Keycode.Foo?
Mkalafut
1
Mungkin membantu untuk menyadari bahwa itu yieldadalah kependekan dari "Kontrol hasil kepada penelepon sampai item berikutnya dalam Enumerable diminta."
3Dave
@Mkalafut yang kedengarannya seperti sesuatu untuk ditanyakan dalam posting Pertanyaan baru jika Anda tidak dapat menemukan jawabannya di halaman dokumentasi Unity , tutorial, atau eksperimen Anda sendiri.
DMGregory
Saya tidak merekomendasikan StopAllCoroutines()dalam hal ini. Tidak apa-apa ketika Anda hanya menggunakan satu coroutine, tetapi jika Anda pernah berencana untuk memiliki lebih dari satu, ini akan memiliki efek yang tidak diinginkan. Alih-alih, Anda harus menggunakan StopCoroutine()dan menghentikan saja yang relevan alih-alih semuanya. ( StopAllCoroutines()akan berguna misalnya ketika mengakhiri level atau memuat area baru, dll., tetapi tidak untuk hal-hal spesifik seperti "Saya tidak lagi menembak".)
Darrel Hoffman

Jawaban:

14

Alasannya adalah kata kunciyield yang memiliki makna khusus dalam C #.

Saat menemukan kata-kata yield returnfungsi dalam C # kembali, seperti yang diharapkan.

Menggunakan hasil untuk mendefinisikan iterator menghilangkan kebutuhan untuk kelas tambahan eksplisit

[...]

Ketika pernyataan pengembalian hasil dicapai dalam metode iterator, ekspresi dikembalikan, dan lokasi saat ini dalam kode dipertahankan. Eksekusi dimulai kembali dari lokasi itu saat berikutnya fungsi iterator dipanggil.

Jadi tidak ada loop tanpa batas. Ada fungsi / iterator yang bisa disebut tak terhingga kali.

Fungsi Unity StartCoroutine()membuat kerangka Unity memanggil fungsi / iterator sekali per frame.

Fungsi Unity StopAllCoroutinesmembuat kerangka Unity berhenti memanggil fungsi / iterator.

Dan kembali WaitForSeconds(time)dari iterator membuat kerangka Unity menunda memanggil fungsi / iterator time.


Komentar yang membingungkan dan suara yang sama-sama membingungkan pada komentar itu mendorong saya untuk menguraikan lebih lanjut tentang apa yang yielddilakukan dan tidak dilakukan kata kunci .

Jika Anda menulis ini:

IEnumerable<int> Count()
{
   int i = 0;
   yield return i++;
}

Anda bisa juga menulis ini:

IEnumerator<int> Count() {
    return new CountEnumerator ();
}
class CountEnumerator : IEnumerator<int> {
    int i = 0;
    bool IEnumerator<int>.MoveNext() { i++; return true; }
    int IEnumerator<int>.Current { get { return i; }
    void IEnumerator<int>.Reset() { throw new NotSupportedException(); }
}

Oleh karena itu kata kunci yieldtidak terkait dengan multi-threading dan sama sekali tidak menelepon System.Threading.Thread.Yield().

Peter
sumber
1
" On encountering the words yield return a function in C# returns". Tidak. Teks yang Anda kutip menjelaskannya, seperti halnya Wikipedia - " In computer science, yield is an action that occurs in a computer program during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority.". Pada dasarnya, "` tolong hentikan saya di mana saya berada dan biarkan orang lain lari sebentar ".
Mawg mengatakan mengembalikan Monica
2
@ Mawg Saya menambahkan bagian kedua pada jawaban untuk mengatasi masalah Anda.
Peter
Terima kasih banyak untuk klarifikasi (terunggah). Saya tentu belajar sesuatu yang baru hari ini :-)
Mawg mengatakan mengembalikan Monica
8

Ketika tombol api diangkat, pernyataan kedua dimasukkan, dan StopAllCoroutines dijalankan. Ini berarti bahwa Coroutine yang menjalankan loop sementara berakhir, sehingga tidak ada lagi loop tak terbatas. Coroutine seperti wadah untuk mengeksekusi kode.

Saya dapat merekomendasikan Unity Manual dan Unity Scripting API untuk lebih memahami apa itu coroutine dan seberapa kuat mereka.

Blog ini dan mencari posting youtube juga membantu saya menggunakan coroutine dengan lebih baik.

David Weatherhead
sumber
3

Coroutine adalah binatang aneh. Imbal hasil menyebabkan metode untuk menunda eksekusi sampai nanti melangkah. Di belakang layar, mungkin terlihat seperti ini:

class FireContinuouslyData {
    int state;
    bool shouldBreak;
}

object FireContinuously(FireContinuouslyData data) {
    switch (data.state) {
        case 0:
            goto State_0;
    }
    while (true) {
        GameObject laser = ...;
        laser.GetComponent...
        //the next three lines handle the yield return
        data.state = 0;
        return new WaitForSeconds(fireTime);
        State_0:
    }
}

Dan internal ke Unity / C # (karena hasil pengembalian adalah fitur c # asli), ketika Anda memanggil StartCoroutine, itu menciptakan FireContinuouslyDataobjek, dan meneruskannya ke metode. Berdasarkan nilai kembali, ia menentukan kapan harus memanggilnya lagi nanti, cukup menyimpan objek FireContinuouslyData untuk meneruskannya di waktu berikutnya.

Jika Anda pernah melakukan penghentian panen, secara internal bisa saja diatur data.shouldBreak = truedan kemudian Unity hanya akan membuang data dan tidak menjadwalkannya lagi.

Dan jika ada data yang perlu disimpan di antara eksekusi, itu juga akan disimpan dalam data untuk nanti.

Contoh bagaimana Unity / C # dapat mengimplementasikan fungsionalitas coroutine:

//Internal to Unity/C#

class Coroutine {
    Action<object> method;
    object data;
}

Coroutine StartCoroutine(IEnumerator enumerator) {
    object data = CreateDataForEnumerator(method); //Very internal to C#
    Action<object> method = GetMethodForEnumerator(enumerator); //Also very internal to C#
    Coroutine coroutine = new Coroutine(method, data);
    RunCoroutine(coroutine);
    return coroutine;
}

//Called whenever this coroutine is scheduled to run
void RunCoroutine(Coroutine coroutine) {
    object yieldInstruction = coroutine.method(coroutine.data);
    if (!data.shouldBreak) {
        //Put this coroutine into a collection of coroutines to run later, by calling RunCoroutine on it again
        ScheduleForLater(yieldInstruction, coroutine);
    }
}
Ed Marty
sumber
1

Jawaban lain menyebutkan bahwa Anda menghentikan co-rutin ketika "Fire1"sudah habis - ini benar, sejauh mengapa coroutine tidak melanjutkan instantiating GameObjects setelah tekan pertama dari "Fire1".

Namun dalam kasus Anda, kode ini tidak akan menjadi 'macet' dalam loop tak terbatas, yang sepertinya Anda cari jawabannya - yaitu while(true) {}loop, bahkan jika Anda tidak menghentikannya secara eksternal.

Itu tidak akan macet tetapi coroutine Anda tidak akan berakhir (tanpa menelepon StopCoroutine()atau StopAllCoroutines()). Ini karena Unity coroutine memberikan kendali kepada penelepon mereka. yieldberbeda dengan returning:

  • sebuah returnpernyataan akan menghentikan eksekusi suatu fungsi, bahkan jika ada lebih banyak kode yang mengikutinya
  • sebuah yieldpernyataan akan menjeda fungsi, mulai dari baris berikutnya setelah yieldketika dilanjutkan.

Biasanya, coroutine akan dilanjutkan setiap frame tetapi Anda juga mengembalikan WaitForSecondsobjek.

Baris tersebut secara yield return new WaitForSeconds(fireTime)kasar diterjemahkan sebagai "sekarang tunda saya, dan jangan kembali sampai fireTimedetik berlalu".

IEnumerator FireContinuously()
{
    // When started, this coroutine enters the below while loop...
    while(true)
    {
        // It does some things... (Infinite coroutine code goes here)

        // Then it yields control back to it's caller and pauses...
        yield return new WaitForSeconds(fireTime);
        // The next time it is called , it resumes here...
        // It finds the end of a loop, so will re-evaluate the loop condition...
        // Which passes, so control is returned to the top of the loop.
    }
}

Kecuali berhenti, ini adalah coroutine yang, setelah dimulai, akan melakukan seluruh loop sekali setiap fireTimedetik.

Joe
sumber
1

Penjelasan sederhana: di bawah tenda Unity mengulangi koleksi (dari YieldInstruction s atau nulls atau apa pun yang Anda yield return) menggunakan IEnumeratorfungsi yang dikembalikan oleh fungsi Anda.

Karena Anda menggunakan yieldkata kunci, metode Anda adalah iterator . Ini bukan hal Unity, ini fitur bahasa C #. Bagaimana cara kerjanya?

Itu malas dan tidak menghasilkan semua koleksi sekaligus (dan koleksi mungkin tak terbatas dan tidak mungkin dihasilkan sekaligus). Elemen-elemen koleksi dihasilkan sesuai kebutuhan. Fungsi Anda mengembalikan iterator untuk digunakan Unity. Ia memanggil MoveNextmetodenya untuk menghasilkan elemen dan Currentproperti baru untuk mengaksesnya.

Jadi loop Anda tidak terbatas, ia menjalankan beberapa kode, mengembalikan elemen dan mengembalikan kontrol ke Unity sehingga tidak macet dan dapat melakukan pekerjaan lain seperti menangani input Anda untuk menghentikan coroutine.

trollingchar
sumber
0

Pikirkan cara foreachkerjanya:

foreach (var number in Enumerable.Range(1, 1000000))
{
  if (number > 10) break;
}

Kontrol atas iterasi ada pada penelepon - jika Anda menghentikan iterasi (di sini dengan break), itu saja.

Kata yieldkunci adalah cara sederhana untuk membuat enumerable dalam C #. Nama mengisyaratkan hal ini - yield returnmenghasilkan kontrol kembali ke penelepon (dalam hal ini, kami foreach); peneleponlah yang memutuskan kapan untuk melanjutkan ke item berikutnya. Jadi Anda bisa membuat metode seperti ini:

IEnumerable<int> ToInfinity()
{
  var i = 0;
  while (true) yield return i++;
}

Ini secara naif sepertinya akan berjalan selamanya; tetapi pada kenyataannya, itu sepenuhnya tergantung pada penelepon. Anda dapat melakukan sesuatu seperti ini:

var range = ToInfinity().Take(10).ToArray();

Ini bisa sedikit membingungkan jika Anda tidak terbiasa dengan konsep ini, tapi saya harap itu juga jelas bahwa ini adalah properti yang sangat berguna. Itu adalah cara paling sederhana bahwa Anda dapat memberikan kendali kepada penelepon Anda, dan ketika penelepon memutuskan untuk menindaklanjuti, itu hanya dapat melakukan langkah berikutnya (jika Unity dibuat hari ini, itu mungkin akan menggunakan awaitalih-alih yield; tetapi awaittidak ada kembali kemudian).

Yang Anda butuhkan untuk mengimplementasikan coroutine Anda sendiri (tidak perlu dikatakan, coroutine paling bodoh yang paling sederhana) adalah ini:

List<IEnumerable> continuations = new List<IEnumerable>();

void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);

void MainLoop()
{
  while (GameIsRunning)
  {
    foreach (var continuation in continuations.ToArray())
    {
      if (!continuation.MoveNext()) continuations.Remove(continuation);
    }

    foreach (var gameObject in updateableGameObjects)
    {
      gameObject.Update();
    }
  }
}

Untuk menambahkan WaitForSecondsimplementasi yang sangat sederhana , Anda hanya perlu sesuatu seperti ini:

interface IDelayedCoroutine
{
  bool ShouldMove();
}

class Waiter: IDelayedCoroutine
{
  private readonly TimeSpan time;
  private readonly DateTime start;

  public Waiter(TimeSpan time)
  {
    this.start = DateTime.Now;
    this.time = time;
  }

  public bool ShouldMove() => start + time > DateTime.Now;
}

Dan kode yang sesuai di loop utama kami:

foreach (var continuation in continuations.ToArray())
{
  if (continuation.Current is IDelayedCoroutine dc)
  {
    if (!dc.ShouldMove()) continue;
  }

  if (!continuation.MoveNext()) continuations.Remove(continuation);
}

Ta-da - itu saja kebutuhan sistem coroutine sederhana. Dan dengan memberikan kendali kepada penelepon, penelepon dapat memutuskan sejumlah hal; mereka mungkin memiliki tabel acara yang diurutkan daripada iterasi melalui semua coroutine pada setiap frame; mereka mungkin memiliki prioritas, atau ketergantungan. Hal ini memungkinkan untuk implementasi multi-tasking koperasi yang sangat sederhana. Dan lihat betapa sederhananya ini, terima kasih yield:)

Luaan
sumber