c# - stopcoroutine - yield return unity
¿Cómo funciona realmente StartCoroutine/yield return pattern en Unity? (4)
No podría ser más simple:
La unidad (y todos los motores de juego) están basados en cuadros .
Todo el punto, toda la razón de ser de Unity, es que está basado en un marco. El motor hace cosas "cada cuadro" para usted. (Anima, representa objetos, hace física, etc.)
Podrías preguntar ... "Oh, eso es genial. ¿Qué sucede si quiero que el motor haga algo por mí en cada cuadro? ¿Cómo le digo al motor que haga tal y tal en un marco?"
La respuesta es ...
Para eso es exactamente una "corutina".
Es así de simple.
Y considera esto ...
Usted conoce la función "Actualizar". Simplemente, todo lo que pongas ahí se hace en cada fotograma . Es literalmente exactamente lo mismo, no hay diferencia en absoluto, a partir de la sintaxis de rendimiento de corrutina.
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;
}
No hay absolutamente ninguna diferencia.
Nota al pie: como todos han señalado, la Unidad simplemente no tiene hilos . Los "marcos" en Unity o en cualquier motor de juego no tienen absolutamente ninguna conexión con los hilos de ninguna manera.
Corutinas / rendimiento son simplemente la forma de acceder a los cuadros en Unity. Eso es. (Y de hecho, es absolutamente lo mismo que la función Update () provista por Unity.) Eso es todo, es así de simple.
Entiendo el principio de corutinas. Sé cómo hacer funcionar el patrón estándar de StartCoroutine
/ yield return
en C # en Unity, por ejemplo, invocar un método que devuelve IEnumerator
través de StartCoroutine
y en ese método hacer algo, yield return new WaitForSeconds(1);
esperar un segundo, luego hacer otra cosa.
Mi pregunta es: ¿qué está pasando realmente detrás de escena? ¿Qué hace realmente StartCoroutine
? ¿Qué IEnumerator
está devolviendo WaitForSeconds
? ¿Cómo devuelve StartCoroutine
control a la parte "algo más" del método llamado? ¿Cómo interactúa todo esto con el modelo de concurrencia de Unity (donde muchas cosas están sucediendo al mismo tiempo sin el uso de corutinas)?
El primer encabezado a continuación es una respuesta directa a la pregunta. Los dos títulos siguientes son más útiles para el programador de todos los días.
Posiblemente aburrido Detalles de implementación de Corutinas
Corutinas se explican en Wikipedia y en otros lugares. Aquí solo proporcionaré algunos detalles desde un punto de vista práctico. IEnumerator
, yield
, etc. son características de lenguaje C # que se usan para un propósito diferente en Unity.
En pocas palabras, un IEnumerator
afirma tener una colección de valores que puede solicitar uno por uno, como una List
. En C #, una función con una firma para devolver un IEnumerator
no tiene que crear y devolver una, pero puede dejar que C # proporcione un IEnumerator
implícito. La función puede proporcionar el contenido de ese IEnumerator
devuelto en el futuro de forma perezosa, a través de declaraciones de yield return
. Cada vez que la persona que realiza la llamada solicita otro valor de ese IEnumerator
implícito, la función se ejecuta hasta la siguiente declaración de yield return
, que proporciona el siguiente valor. Como subproducto de esto, la función se detiene hasta que se solicite el siguiente valor.
En Unity, no usamos estos para proporcionar valores futuros, aprovechamos el hecho de que la función se detiene. Debido a esta explotación, muchas cosas sobre corutinas en Unity no tienen sentido (¿ IEnumerator
tiene que ver IEnumerator
con nada? ¿Qué es yield
? ¿Por qué new WaitForSeconds(3)
? Etc.). Lo que ocurre "debajo del capó" es que los valores que usted proporciona a través del IEnumerator son utilizados por StartCoroutine()
para decidir cuándo solicitar el próximo valor, que determina cuándo se reanudará la corutina.
Su juego Unity tiene un solo subproceso (*)
Corutinas no son hilos. Hay un bucle principal de Unity y todas las funciones que escribe están siendo llamadas por el mismo hilo principal en orden. Puede verificar esto colocando un while(true);
en cualquiera de sus funciones o corutinas. Congelará todo, incluso el editor de Unity. Esto es evidencia de que todo se ejecuta en un hilo principal. Este enlace que Kay mencionó en su comentario anterior también es un gran recurso.
(*) Unity llama a tus funciones desde un hilo. Entonces, a menos que usted mismo cree un hilo, el código que escribió es de un solo hilo. Por supuesto, Unity emplea otros hilos y puede crear hilos usted mismo si lo desea.
Una descripción práctica de Corutinas para programadores de juegos
Básicamente, cuando llama a StartCoroutine(MyCoroutine())
, es exactamente como una llamada de función normal a MyCoroutine()
, hasta que el primer yield return X
, donde X
es algo así como null
, new WaitForSeconds(3)
, StartCoroutine(AnotherCoroutine())
, break
, etc. Esto es cuando comienza a diferir de una función. Unity "hace una pausa" que funciona justo en esa línea yield return X
, continúa con otros negocios y algunos fotogramas pasan, y cuando es tiempo de nuevo, Unity reanuda esa función justo después de esa línea. Recuerda los valores para todas las variables locales en la función. De esta forma, puede tener un bucle for
que se repite cada dos segundos, por ejemplo.
Cuando Unity reanudará su coroutine depende de lo que X
tenía en su yield return X
Por ejemplo, si utilizó el yield return new WaitForSeconds(3);
, se reanuda después de que hayan pasado 3 segundos. Si usó yield return StartCoroutine(AnotherCoroutine())
, se reanudará después de que AnotherCoroutine()
haya finalizado por completo, lo que le permite anidar comportamientos a tiempo. Si acaba de utilizar un yield return null;
, se reanuda justo en el siguiente cuadro.
Haber profundizado en esto últimamente, escribió una publicación aquí - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - que arrojó luz sobre las IEnumerator
internas (con ejemplos de código denso), la interfaz subyacente de IEnumerator
y cómo se usa para corutinas.
Usar enumeradores de recolección para este fin todavía me parece un poco extraño. Es el inverso de lo que los enumeradores consideran diseñado. El punto de los enumeradores es el valor devuelto en cada acceso, pero el punto de Corutinas es el código que se encuentra entre los valores devueltos. El valor real devuelto no tiene sentido en este contexto.
Las corutinas de Unity3D a menudo referenciadas en el enlace de detalles están muertas. Dado que se menciona en los comentarios y las respuestas, voy a publicar el contenido del artículo aquí. Este contenido proviene de este espejo .
Corutinas de Unity3D en detalle
Muchos procesos en juegos tienen lugar en el transcurso de múltiples marcos. Tienes procesos ''densos'', como pathfinding, que trabajan duro en cada fotograma pero se dividen en varios fotogramas para no afectar demasiado la velocidad de fotogramas. Tienes procesos ''dispersos'', como desencadenantes de juego, que no hacen la mayoría de los fotogramas, pero ocasionalmente se les pide que hagan un trabajo crítico. Y tienes varios procesos entre los dos.
Cada vez que crea un proceso que tendrá lugar en múltiples marcos, sin multihilo, debe encontrar la forma de dividir el trabajo en fragmentos que se pueden ejecutar uno por cuadro. Para cualquier algoritmo con un bucle central, es bastante obvio: un Pathfinder A *, por ejemplo, puede estructurarse de manera que mantenga sus listas de nodos de forma semipermanente, procesando solo un puñado de nodos de la lista abierta de cada fotograma, en lugar de intentarlo para hacer todo el trabajo de una vez. Hay que equilibrar la gestión de la latencia; después de todo, si bloquea su velocidad de fotogramas a 60 o 30 fotogramas por segundo, su proceso solo tomará 60 o 30 pasos por segundo, y eso podría provocar que el proceso simplemente tome demasiado tiempo en general. Un diseño ordenado podría ofrecer la unidad de trabajo más pequeña posible en un nivel, por ejemplo, procesar un solo nodo A *, y capas encima de una forma de agrupar el trabajo en trozos más grandes, por ejemplo, seguir procesando nodos A * durante X milisegundos. (Algunas personas lo llaman ''timeslicing'', aunque yo no).
Aún así, permitir que el trabajo se divida de esta manera significa que tiene que transferir el estado de un cuadro al siguiente. Si está rompiendo un algoritmo iterativo, debe conservar todo el estado compartido en todas las iteraciones, así como un medio para rastrear qué iteración se realizará a continuación. Por lo general, eso no es tan malo: el diseño de una "clase A * Pathfinder" es bastante obvio, pero también hay otros casos que son menos agradables. A veces enfrentará cálculos largos que realizan diferentes tipos de trabajo de cuadro a cuadro; el objeto que captura su estado puede terminar con un gran lío de "locales" semi-útiles, guardados para pasar datos de un cuadro al siguiente. Y si se trata de un proceso escaso, a menudo terminas teniendo que implementar una pequeña máquina de estado solo para rastrear cuando se debe hacer algún trabajo.
¿No sería estupendo si, en lugar de tener que rastrear explícitamente todo este estado a través de múltiples marcos, y en lugar de tener que multiprocesar y administrar la sincronización y el bloqueo, y así sucesivamente, simplemente pudieras escribir tu función como un solo trozo de código, y marcar lugares específicos donde la función debería ''pausar'' y continuar más adelante?
La unidad, junto con otros entornos e idiomas, proporciona esto en forma de Corutinas.
¿Como se ven? En "Unityscript" (Javascript):
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
Cª#:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
¿Cómo trabajan? Permítanme decir, rápidamente, que no trabajo para Unity Technologies. No he visto el código fuente de Unity. Nunca he visto las entrañas del motor de corrutina de Unity. Sin embargo, si lo han implementado de una manera radicalmente diferente de lo que voy a describir, entonces me sorprenderá bastante. Si alguien de UT quiere hablar y hablar sobre cómo funciona realmente, entonces sería genial.
Las grandes pistas están en la versión de C #. En primer lugar, tenga en cuenta que el tipo de retorno para la función es IEnumerator. Y en segundo lugar, tenga en cuenta que una de las declaraciones es el retorno de rendimiento. Esto significa que el rendimiento debe ser una palabra clave, y como el soporte C # de Unity es vainilla C # 3.5, debe ser una palabra clave vainilla C # 3.5. De hecho, aquí está en MSDN , hablando de algo llamado ''bloques de iteradores''. Entonces, ¿qué está pasando?
En primer lugar, está este tipo de IEnumerator. El tipo de IEnumerator actúa como un cursor sobre una secuencia, proporcionando dos miembros significativos: Current, que es una propiedad que le proporciona el elemento sobre el que se encuentra el cursor, y MoveNext (), una función que se mueve al siguiente elemento de la secuencia. Debido a que IEnumerator es una interfaz, no especifica exactamente cómo se implementan estos miembros; MoveNext () podría simplemente agregar uno a Actual, o podría cargar el nuevo valor de un archivo, o podría descargar una imagen de Internet y hash y almacenar el nuevo hash en Actual ... o incluso podría hacer una cosa para el primero elemento en la secuencia, y algo completamente diferente para el segundo. Incluso podrías usarlo para generar una secuencia infinita si así lo deseas. MoveNext () calcula el siguiente valor de la secuencia (devuelve falso si no hay más valores) y Current recupera el valor que calculó.
Por lo general, si desea implementar una interfaz, debe escribir una clase, implementar los miembros, etc. Los bloques Iterator son una forma conveniente de implementar IEnumerator sin todas esas molestias; usted simplemente sigue unas pocas reglas y el compilador genera automáticamente la implementación de IEnumerator.
Un bloque iterador es una función regular que (a) devuelve IEnumerator y (b) usa la palabra clave yield. Entonces, ¿qué hace la palabra clave yield en realidad? Declara cuál es el siguiente valor en la secuencia, o que no hay más valores. El punto en el que el código encuentra un retorno de rendimiento X o salto de rendimiento es el punto en el que IEnumerator.MoveNext () debe detenerse; un retorno de rendimiento X hace que MoveNext () devuelva verdadero y actual para que se le asigne el valor X, mientras que un salto de rendimiento hace que MoveNext () devuelva falso.
Ahora, aquí está el truco. No tiene que importar cuáles son los valores reales devueltos por la secuencia. Puede llamar a MoveNext () repetidamente e ignorar Current; los cálculos se seguirán realizando. Cada vez que se llama a MoveNext (), su bloque iterador corre a la siguiente instrucción ''yield'', independientemente de la expresión que ceda. Entonces puedes escribir algo como:
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;
}
y lo que has escrito realmente es un bloque de iteradores que genera una larga secuencia de valores nulos, pero lo que es significativo son los efectos secundarios del trabajo que hace para calcularlos. Puede ejecutar esta coroutine usando un ciclo simple como este:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
O, más útil, podrías mezclarlo con otro trabajo:
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press ''Escape'', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
Todo está en el tiempo Como ha visto, cada declaración de rendimiento debe proporcionar una expresión (como nulo) para que el bloque iterador tenga algo que asignar a IEnumerator.Current. Una larga secuencia de nulos no es exactamente útil, pero estamos más interesados en los efectos secundarios. ¿No es así?
Hay algo útil que podemos hacer con esa expresión, en realidad. ¿Qué pasaría si, en lugar de limitarnos a ceder e ignorarlo, obtuviéramos algo que indicara cuándo esperamos necesitar hacer más trabajo? A menudo tendremos que llevarnos directamente al siguiente fotograma, claro, pero no siempre: habrá muchas ocasiones en las que queremos continuar después de que una animación o sonido haya terminado de reproducirse, o después de que haya transcurrido un tiempo determinado. Aquellos mientras (playingAnimation) producen return null; las construcciones son un poco tediosas, ¿no crees?
Unity declara el tipo de base YieldInstruction y proporciona algunos tipos derivados concretos que indican determinados tipos de espera. Tienes WaitForSeconds, que reanuda la coroutine una vez transcurrido el tiempo designado. Tienes WaitForEndOfFrame, que reanuda la coroutine en un punto particular más adelante en el mismo marco. Usted tiene el tipo de Coroutine en sí mismo, que, cuando la corutina A produce corutina B, pausa la corutina A hasta que finalice la corutina B.
¿Cómo se ve esto desde el punto de vista del tiempo de ejecución? Como dije, no trabajo para Unity, así que nunca he visto su código; pero me imagino que podría verse un poco así:
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;
No es difícil imaginar cómo se podrían agregar más subtítulos de YieldInstruction para manejar otros casos; por ejemplo, se podría agregar soporte de nivel de motor para señales, con WaitForSignal ("SignalName") YieldInstruction soportándolo. Al agregar más YieldInstructions, las corutinas en sí mismas pueden volverse más expresivas: rendimiento de rendimiento nuevo WaitForSignal ("GameOver") es más fácil de leer que nunca (! Signals.HasFired ("GameOver")) produce un retorno nulo, si me preguntas, bastante aparte de el hecho de hacerlo en el motor podría ser más rápido que hacerlo en secuencia de comandos.
Un par de ramificaciones no obvias Hay un par de cosas útiles acerca de todo esto que la gente a veces extraña y que pensé que debería señalar.
En primer lugar, el retorno de rendimiento solo genera una expresión, cualquier expresión, y YieldInstruction es un tipo regular. Esto significa que puedes hacer cosas como:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
Las líneas específicas producen return new WaitForSeconds (), return return new WaitForEndOfFrame (), etc., son comunes, pero en realidad no son formularios especiales por derecho propio.
En segundo lugar, como estas corutinas son solo bloques de iteración, puede iterar sobre ellas usted mismo si lo desea; no es necesario que el motor lo haga por usted. Lo he usado para agregar condiciones de interrupción a una corutina antes:
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
En tercer lugar, el hecho de que pueda ceder en otras corutinas puede permitirle implementar sus propias YieldInstructions, aunque no tan eficientemente como si hubieran sido implementadas por el motor. Por ejemplo:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
sin embargo, realmente no recomendaría esto: el costo de comenzar una Corutina es un poco pesado para mi gusto.
Conclusión Espero que esto aclare un poco algo de lo que realmente está sucediendo cuando usa un Coroutine en Unity. Los bloques de iteradores de C # son una pequeña construcción maravillosa, e incluso si no estás usando Unity, quizás te resulte útil aprovecharlos de la misma manera.