¿Por qué no me estoy quedando atrapado en el circuito?

8

Soy nuevo en Unity. Estaba aprendiendo corutinas y escribí esto.

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

Cuando se presiona el botón, se llama a la rutina y entra en el ciclo 'while'. Cuando dejo el botón, se detiene la rutina. ¿No debería quedar atascado en el ciclo 'while' ya que es un ciclo infinito? ¿Por qué?

babybrain
fuente
Hace poco volví a Unity, noté que los métodos de entrada están tomando una cadena "Fire1", ¿es algo que puede configurar en el motor para permitir reasignaciones de teclas en lugar de escribir Keycode.Foo?
Mkalafut
1
Puede ser útil darse cuenta de que yieldefectivamente es la abreviatura de "Ceda el control a la persona que llama hasta que se solicite el siguiente elemento en el Enumerable".
3Dave
@Mkalafut que suena como algo que preguntar en una nueva publicación de Preguntas si no puede encontrar la respuesta en las páginas de documentación , tutoriales o sus propios experimentos de Unity .
DMGregory
No lo recomiendo StopAllCoroutines()en este caso. Está bien cuando solo usas una rutina, pero si alguna vez planeaste tener más de una, esto tendría efectos no deseados. En su lugar, debe usar StopCoroutine()y simplemente detener el que sea relevante en lugar de todos. ( StopAllCoroutines()sería útil, por ejemplo, al finalizar el nivel o cargar una nueva área, etc., pero no para cosas específicas como "Ya no estoy disparando".)
Darrel Hoffman

Respuestas:

14

La razón es la palabra claveyield que tiene un significado específico en C #.

Al encontrar las palabras, yield returnuna función en C # regresa, como cabría esperar.

El uso del rendimiento para definir un iterador elimina la necesidad de una clase extra explícita

[...]

Cuando se alcanza una declaración de rendimiento en el método iterador, se devuelve la expresión y se retiene la ubicación actual en el código. La ejecución se reinicia desde esa ubicación la próxima vez que se llama a la función de iterador.

Entonces no hay un bucle infinito. Hay una función / iterador que se puede llamar un número infinito de veces.

La función Unity StartCoroutine()hace que el marco Unity llame a la función / iterador una vez por cuadro.

La función Unity StopAllCoroutineshace que el marco Unity deje de llamar a la función / iterador.

Y al regresar WaitForSeconds(time)del iterador, el marco de Unity deja de llamar a la función / iterador time.


Un comentario confuso y un voto positivo igualmente confuso sobre ese comentario me animaron a profundizar en lo que yieldhace y no hace la palabra clave .

Si escribes esto:

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

En su lugar, también puede escribir esto:

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

De ello se deduce que la palabra clave yieldno está relacionada con subprocesos múltiples y absolutamente no llama System.Threading.Thread.Yield().

Peter
fuente
1
" On encountering the words yield return a function in C# returns". No, no lo hace. El texto que usted cita lo explica, al igual que 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.". Básicamente, "por favor detenme donde estoy y deja que alguien más corra por un tiempo".
Mawg dice que reinstale a Mónica el
2
@Mawg Agregué una segunda parte a la respuesta para abordar su inquietud.
Peter
Muchas gracias por aclarar (upvoted). Ciertamente aprendí algo nuevo hoy :-)
Mawg dice que reinstale a Mónica el
8

Cuando se levanta el botón de disparo, se ingresa el segundo if, y se ejecuta StopAllCoroutines. Esto significa que la Corutina en la que se está ejecutando el ciclo while finaliza, por lo que no hay más ciclos infinitos. La corutina es como un contenedor para que el código se ejecute.

Puedo recomendar el Manual de Unity y la API de Unity Scripting para comprender mejor qué son las corutinas y cuán poderosas pueden ser.

Este blog y la búsqueda en la publicación de YouTube también me ayudaron a usar mejor las corutinas.

David Weatherhead
fuente
3

Las corutinas son una bestia extraña. El rendimiento de rendimiento hace que el método suspenda la ejecución hasta que luego se escalone. Detrás de escena, podría verse más o menos así:

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 # (dado que el rendimiento devuelto es una característica nativa de C #), cuando llama a StartCoroutine, crea un FireContinuouslyDataobjeto y lo pasa al método. Según el valor de retorno, determina cuándo volver a llamarlo más tarde, simplemente almacenando el objeto FireContinuouslyData para pasarlo la próxima vez.

Si alguna vez hiciste un corte de rendimiento, podría establecerse internamente data.shouldBreak = truey luego Unity simplemente tiraría los datos y no volvería a programarlos.

Y si hubiera datos que debían guardarse entre ejecuciones, también se almacenarían en los datos para más adelante.

Un ejemplo de cómo Unity / C # podría implementar la funcionalidad de rutina:

//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
fuente
1

Otra respuesta menciona que está deteniendo las co-rutinas cuando "Fire1"está activo; esto es completamente correcto, en la medida en que la rutina no continúa creando instancias de GameObjects después de la primera pulsación de "Fire1".

Sin embargo, en su caso, este código no se 'quedará atascado' en un bucle infinito, que es lo que parece que está buscando una respuesta, es decir, el while(true) {}bucle, incluso si no lo detuvo externamente.

No se atascará, pero su rutina no terminará (sin llamar StopCoroutine()o StopAllCoroutines()) tampoco. Esto se debe a que las corutinas de Unity ceden el control a su interlocutor. yielding es diferente a returning:

  • una returndeclaración dejará de ejecutar una función, incluso si hay más código siguiéndola
  • una yieldinstrucción pausará la función, comenzando en la siguiente línea después de yieldcuando se reanude.

Por lo general, las rutinas se reanudarán en cada cuadro, pero también devolverá un WaitForSecondsobjeto.

La línea yield return new WaitForSeconds(fireTime)se traduce aproximadamente como "ahora suspenderme, y no volver hasta que fireTimehayan pasado los segundos".

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 menos que se detenga, esta es una rutina que, una vez iniciada, realizará todo el ciclo una vez cada fireTimesegundo.

Joe
fuente
1

Una explicación simple: debajo del capó, Unity está iterando sobre una colección (de YieldInstruction s o nulls o lo que sea que usted yield returnuse) usando la IEnumeratorfunción que devuelve su función.

Como usa la yieldpalabra clave, su método es un iterador . No es lo de Unity, es una característica del lenguaje C #. ¿Como funciona?

Es vago y no genera toda la colección a la vez (y la colección puede ser infinita e imposible de generar a la vez). Los elementos de la colección se generan según sea necesario. Su función devuelve un iterador para que Unity trabaje. Llama a su MoveNextmétodo para generar un nuevo elemento y Currentpropiedad para acceder a él.

Por lo tanto, su bucle no es interminable, ejecuta un código, devuelve un elemento y devuelve el control a Unity para que no se atasque y pueda hacer otro trabajo, como manejar su entrada para detener la rutina.

trollingchar
fuente
0

Piensa en cómo foreachfunciona un :

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

El control sobre la iteración está en la persona que llama; si detiene la iteración (aquí con break), eso es todo.

La yieldpalabra clave es una forma simple de hacer un enumerable en C #. El nombre sugiere esto: yield returndevuelve el control a la persona que llama (en este caso, nuestra foreach); Es la persona que llama quien decide cuándo continuar con el siguiente elemento. Entonces puedes hacer un método como este:

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

Parece ingenuo que se ejecutará para siempre; pero en realidad, depende completamente de la persona que llama. Puedes hacer algo como esto:

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

Esto puede ser un poco confuso si no estás acostumbrado a este concepto, pero espero que también sea obvio que esta es una propiedad muy útil. Era la forma más sencilla en la que podía ceder el control a su interlocutor, y cuando la persona que llama decide hacer un seguimiento, solo puede hacer el siguiente paso (si Unity se creó hoy, probablemente lo usaría en awaitlugar de yield, pero awaitno existía) entonces).

Todo lo que necesita para implementar sus propias rutinas (no hace falta decir que las más simples y más estúpidas) es esto:

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

Para agregar una WaitForSecondsimplementación muy simple , solo necesita algo como esto:

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

Y el código correspondiente en nuestro bucle principal:

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

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

Ta-da: eso es todo lo que necesita un sistema de rutina simple. Y al ceder el control a la persona que llama, la persona que llama puede decidir cualquier cantidad de cosas; podrían tener una tabla de eventos ordenada en lugar de iterar a través de todas las rutinas en cada cuadro; pueden tener prioridades o dependencias. Permite una implementación muy simple de la multitarea cooperativa. Y mira lo simple que es esto, gracias a yield:)

Luaan
fuente