Perché non rimango bloccato nel circuito


8

Sono nuovo di Unity. Stavo imparando coroutine e ho scritto questo.

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

Quando si preme il pulsante, viene chiamato il coroutine ed entra nel ciclo 'while'. Quando lascio il pulsante, interrompe la routine. Non dovrebbe rimanere bloccato nel ciclo 'while' in quanto è un ciclo infinito? Perché?


Sono appena tornato a Unity da solo, noto che i metodi di input stanno prendendo in una stringa "Fire1", è qualcosa che puoi impostare nel motore per consentire la modifica dei tasti anziché la digitazione Keycode.Foo?
Mkalafut,

1
Potrebbe essere utile capire che yieldè effettivamente l'abbreviazione di "Controllo del rendimento per il chiamante fino a quando non viene richiesto l'elemento successivo nell'enumerabile".
3Daveva il

@Mkalafut suona come qualcosa da porre in un nuovo post di domande se non riesci a trovare la risposta nelle pagine della documentazione di Unity , nei tutorial o nei tuoi esperimenti.
DMGregory

Non lo consiglio StopAllCoroutines()in questo caso. Va bene quando usi sempre una sola routine, ma se hai mai pianificato di averne più di una, questo avrebbe effetti indesiderati. Invece dovresti usare StopCoroutine()e fermare quello che è rilevante invece di tutti loro. ( StopAllCoroutines()sarebbe utile ad esempio quando si termina il livello o si carica una nuova area, ecc., ma non per cose specifiche come "Non sto più sparando".)
Darrel Hoffman,

Risposte:


14

Il motivo è la parola chiaveyield che ha un significato specifico in C #.

Incontrando le parole yield returnritorna una funzione in C #, come ci si aspetterebbe.

L'uso di yield per definire un iteratore elimina la necessità di una classe aggiuntiva esplicita

[...]

Quando viene raggiunta un'istruzione return di rendimento nel metodo iteratore, viene restituita espressione e la posizione corrente nel codice viene mantenuta. L'esecuzione viene riavviata da quella posizione la volta successiva che viene chiamata la funzione iteratore.

Quindi non esiste un ciclo infinito. Esiste una funzione / iteratore che può essere chiamata un numero infinito di volte.

La funzione Unity StartCoroutine()consente al framework Unity di chiamare la funzione / iteratore una volta per frame.

La funzione StopAllCoroutinesUnity impedisce al framework Unity di chiamare la funzione / iteratore.

E il ritorno WaitForSeconds(time)dall'iteratore fa sospendere il framework Unity chiamando la funzione / iteratore time.


Un commento confuso e un voto altrettanto confuso su quel commento mi hanno incoraggiato ad approfondire ciò che la parola chiave yieldfa e non fa.

Se scrivi questo:

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

Puoi invece scrivere anche questo:

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

Ne consegue che la parola chiave yieldnon è correlata al multi-threading e non chiama assolutamenteSystem.Threading.Thread.Yield() .


1
" On encountering the words yield return a function in C# returns". No non lo fa. Il testo che citi lo spiega, così come 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.". In sostanza, "` per favore, mettimi in pausa dove sono e lascia che qualcun altro corra per un po '".
Mawg dice di ripristinare Monica il

2
@Mawg Ho aggiunto una seconda parte alla risposta per rispondere alle tue preoccupazioni.
Peter,

Grazie mille per il chiarimento (votato). Oggi ho sicuramente imparato qualcosa di nuovo :-)
Mawg dice di ripristinare Monica l'

8

Quando il pulsante di fuoco viene sollevato, viene immessa la seconda istruzione if e viene eseguito StopAllCoroutines. Ciò significa che la Coroutine in cui è in esecuzione il ciclo while è terminata, quindi non esiste più un ciclo infinito. La coroutine è come un contenitore in cui eseguire il codice.

Posso raccomandare il Manuale di Unity e l' API di Unity Scripting per avere una migliore comprensione di ciò che le coroutine sono e quanto potenti possono essere.

Anche questo blog e la ricerca di post su YouTube mi sono stati utili per utilizzare meglio le coroutine.


3

Le coroutine sono una strana bestia. Il rendimento del rendimento fa sospendere l'esecuzione del metodo fino a quando non viene successivamente modificato. Dietro le quinte, potrebbe assomigliare a questo:

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

E interno a Unity / C # (poiché il rendimento restituito è una funzione nativa c #), quando si chiama StartCoroutine, crea un FireContinuouslyDataoggetto e lo passa al metodo. In base al valore restituito, determina quando richiamarlo più tardi, semplicemente memorizzando l'oggetto FireContinuouslyData per passarlo alla volta successiva.

Se hai mai fatto una rottura della resa, potrebbe semplicemente impostare internamente data.shouldBreak = truee quindi Unity semplicemente eliminerebbe i dati e non li pianificherebbe di nuovo.

E se ci fossero dei dati che dovevano essere salvati tra le esecuzioni, sarebbero anche memorizzati nei dati per dopo.

Un esempio di come Unity / C # potrebbe implementare la funzionalità 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);
    }
}

1

Un'altra risposta menziona che stai interrompendo le co-routine quando "Fire1"è attivo - questo è completamente corretto, nella misura in cui la coroutine non continua a creare un'istanza di GameObjects dopo la prima pressione di "Fire1".

Nel tuo caso, tuttavia, questo codice non rimarrà "bloccato" in un ciclo infinito, che sembra quello che stai cercando una risposta, ad esempio il while(true) {}ciclo, anche se non lo hai fermato esternamente.

Non si bloccherà ma la tua routine non finirà (senza chiamare StopCoroutine()o StopAllCoroutines()). Questo perché le coroutine Unity danno il controllo al proprio chiamante. yielding è diverso da returning:

  • una returndichiarazione cesserà l'esecuzione di una funzione, anche se non v'è più il codice seguente è
  • una yielddichiarazione sarà sospendere la funzione, a partire dalla riga successiva dopo yieldquando ripreso.

Di solito, le coroutine verranno riprese per ogni fotogramma ma stai anche restituendo un WaitForSecondsoggetto.

La riga yield return new WaitForSeconds(fireTime)si traduce approssimativamente come "ora sospendimi e non tornare fino a quando non fireTimesono passati secondi".

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

A meno che non sia stato arrestato, si tratta di una procedura di routine che, una volta avviata, eseguirà l'intero ciclo una volta al fireTimesecondo.


1

Una semplice spiegazione: sotto l'unità Unity sta iterando su una raccolta (di istruzioni o null di Yield o qualsiasi altra cosa yield return) usando IEnumeratorciò che la tua funzione restituisce.

Dato che usi la yieldparola chiave, il tuo metodo è un iteratore . Non è la cosa di Unity, è una funzione del linguaggio C #. Come funziona?

È pigro e non genera tutta la raccolta in una sola volta (e la raccolta può essere infinita e impossibile da generare in una sola volta). Gli elementi della raccolta vengono generati secondo necessità. La tua funzione restituisce un iteratore con cui Unity può funzionare. Chiama il suo MoveNextmetodo per generare un nuovo elemento e Currentproprietà per accedervi.

Quindi il tuo ciclo non è infinito, esegue un po 'di codice, restituisce un elemento e restituisce il controllo a Unity, quindi non è bloccato e può fare altro lavoro come gestire l'input per fermare la routine.


0

Pensa a come foreachfunziona:

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

Il controllo sull'iterazione è sul chiamante: se interrompi l'iterazione (qui con break), è tutto.

La yieldparola chiave è un modo semplice per rendere un enumerabile in C #. Il nome suggerisce: yield returnrestituisce il controllo al chiamante (in questo caso, il nostro foreach); è il chiamante che decide quando passare all'elemento successivo. Quindi puoi creare un metodo come questo:

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

Sembra ingenuo che funzionerà per sempre; ma in realtà dipende interamente dal chiamante. Puoi fare qualcosa del genere:

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

Questo può essere un po 'confuso se non sei abituato a questo concetto, ma spero sia anche ovvio che questa è una proprietà molto utile. Era il modo più semplice per ottenere il controllo del chiamante e quando il chiamante decide di eseguire il follow-up, può semplicemente fare il passo successivo (se Unity fosse realizzato oggi, probabilmente avrebbe usato awaitinvece di yield; ma awaitnon esisteva poi).

Tutto ciò di cui hai bisogno per implementare le tue coroutine (inutile dirlo, le coroutine più stupide più semplici) è questo:

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

Per aggiungere WaitForSecondsun'implementazione molto semplice , hai solo bisogno di qualcosa del genere:

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

E il codice corrispondente nel nostro ciclo principale:

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

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

Ta-da - è tutto ciò di cui ha bisogno un semplice sistema coroutine. E cedendo il controllo al chiamante, il chiamante può decidere su qualsiasi numero di cose; potrebbero avere una tabella degli eventi ordinata piuttosto che scorrere tutte le coroutine su ogni frame; potrebbero avere priorità o dipendenze. Consente un'implementazione molto semplice del multi-tasking cooperativo. E guarda quanto è semplice, grazie a yield:)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.