El enlace a menudo detallado de Cority de Unity3D en detalle está muerto. Como se menciona en los comentarios y las respuestas, voy a publicar el contenido del artículo aquí. Este contenido proviene de este espejo .
Unity3D coroutines en detalle
Muchos procesos en los juegos tienen lugar en el transcurso de múltiples cuadros. Tienes procesos 'densos', como la búsqueda de rutas, que trabajan duro cada fotograma pero se dividen en varios fotogramas para no afectar demasiado la velocidad de fotogramas. Tienes procesos 'dispersos', como los desencadenantes del juego, que no hacen nada en la mayoría de los cuadros, pero ocasionalmente se les pide que hagan un trabajo crítico. Y tienes una variedad de procesos entre los dos.
Siempre que esté creando un proceso que tendrá lugar en varios cuadros, sin múltiples subprocesos, debe encontrar alguna forma de dividir el trabajo en fragmentos que se puedan ejecutar uno por cuadro. Para cualquier algoritmo con un bucle central, es bastante obvio: un pathfinder A *, por ejemplo, puede estructurarse de modo que mantenga sus listas de nodos de forma semipermanente, procesando solo un puñado de nodos de la lista abierta de cada cuadro, en lugar de intentar hacer todo el trabajo de una vez. Hay que hacer un balance para administrar la latencia: después de todo, si está bloqueando su velocidad de cuadros a 60 o 30 cuadros por segundo, entonces su proceso solo tomará 60 o 30 pasos por segundo, y eso podría hacer que el proceso solo tome demasiado largo en general Un diseño ordenado podría ofrecer la unidad de trabajo más pequeña posible en un nivel, p. Ej. procese un solo nodo A *, y coloque encima una forma de agrupar el trabajo en trozos más grandes, por ejemplo, siga procesando nodos A * durante X milisegundos. (Algunas personas llaman a esto "división de tiempo", aunque yo no).
Aún así, permitir que el trabajo se rompa de esta manera significa que tiene que transferir el estado de un cuadro a otro. Si está rompiendo un algoritmo iterativo, entonces debe preservar todo el estado compartido entre las iteraciones, así como un medio para rastrear qué iteración se realizará a continuación. Eso no suele ser tan malo: el diseño de una 'clase de pathfinder A *' es bastante obvio, pero también hay otros casos que son menos agradables. A veces se enfrentará a largos cálculos que realizan diferentes tipos de trabajo de cuadro a cuadro; el objeto que captura su estado puede terminar con un gran desorden de 'locales' semi-útiles, guardados para pasar datos de un cuadro a otro. Y si se trata de un proceso escaso, a menudo terminas teniendo que implementar una pequeña máquina de estado solo para rastrear cuándo se debe hacer el trabajo.
¿No sería genial si, en lugar de tener que rastrear explícitamente todo este estado a través de múltiples cuadros, y en lugar de tener que realizar múltiples subprocesos y administrar la sincronización y el bloqueo, etc., podría escribir su función como un solo fragmento de código, y ¿Marcar lugares particulares donde la función debería 'pausar' y continuar más adelante?
Unity, junto con otros entornos e idiomas, proporciona esto en forma de Coroutines.
¿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 agallas del motor de rutina de Unity. Sin embargo, si lo han implementado de una manera radicalmente diferente de lo que estoy a punto de describir, entonces me sorprenderé bastante. Si alguien de UT quiere intervenir y hablar sobre cómo funciona realmente, entonces sería genial.
Las grandes pistas están en la versión 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 rendimiento del rendimiento. Esto significa que el rendimiento debe ser una palabra clave, y como el soporte C # de Unity es vanilla C # 3.5, debe ser una palabra clave vanilla C # 3.5. De hecho, aquí está en MSDN , hablando de algo llamado 'bloques iteradores'. Entonces, ¿qué está pasando?
En primer lugar, está este tipo de IEnumerator. El tipo 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 actualmente el cursor, y MoveNext (), una función que se mueve al siguiente elemento de la secuencia. Como IEnumerator es una interfaz, no especifica exactamente cómo se implementan estos miembros; MoveNext () podría simplemente agregar uno a Current, o podría cargar el nuevo valor de un archivo, o podría descargar una imagen de Internet y hacer un hash y almacenar el nuevo hash en Current ... o incluso podría hacer una cosa por primera vez elemento en la secuencia, y algo completamente diferente para el segundo. Incluso podría usarlo para generar una secuencia infinita si así lo desea. MoveNext () calcula el siguiente valor en la secuencia (devuelve falso si no hay más valores),
Normalmente, si quisiera implementar una interfaz, tendría que escribir una clase, implementar los miembros, etc. Los bloques de iterador son una forma conveniente de implementar IEnumerator sin toda esa molestia: solo tiene que seguir algunas 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 de rendimiento. Entonces, ¿qué hace realmente la palabra clave de rendimiento? 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 un salto de rendimiento es el punto en el que IEnumerator.MoveNext () debe detenerse; un retorno de rendimiento X hace que MoveNext () devuelva verdadero y se asigne el valor X a Corriente, mientras que un límite 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 aún se realizarán. Cada vez que se llama a MoveNext (), su bloque iterador se ejecuta a la siguiente instrucción 'rendimiento', independientemente de la expresión que realmente produzca. 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 realmente ha escrito es un bloque iterador que genera una larga secuencia de valores nulos, pero lo importante son los efectos secundarios del trabajo que realiza para calcularlos. Puede ejecutar esta rutina usando un bucle simple como este:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
O, más útilmente, podría 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 has visto, cada declaración de rendimiento debe proporcionar una expresión (como nulo) para que el bloque iterador tenga algo que asignar realmente a IEnumerator.Current. Una larga secuencia de nulos no es exactamente útil, pero estamos más interesados en los efectos secundarios. ¿No estamos?
Hay algo útil que podemos hacer con esa expresión, en realidad. ¿Qué pasa si, en lugar de ceder nulo e ignorarlo, arrojamos algo que indica cuándo esperamos tener que hacer más trabajo? A menudo tendremos que continuar directamente en el 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 pasado una cantidad de tiempo particular. Aquellos mientras (playingAnimation) producen retorno nulo; las construcciones son un poco tediosas, ¿no te parece?
Unity declara el tipo base YieldInstruction y proporciona algunos tipos derivados concretos que indican tipos particulares de espera. Tienes WaitForSeconds, que reanuda la rutina después de que haya transcurrido el tiempo designado. Tienes WaitForEndOfFrame, que reanuda la rutina en un punto particular más adelante en el mismo marco. Tienes el tipo de Corutina en sí mismo, que, cuando la corutina A produce la corutina B, pausa la corutina A hasta que la corutina B haya terminado.
¿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 subtipos de YieldInstruction para manejar otros casos: se podría agregar soporte a nivel de motor para señales, por ejemplo, con un WaitForSignal ("SignalName") YieldInstruction que lo soporte. Al agregar más YieldInstructions, las corutinas mismas pueden volverse más expresivas: el retorno de rendimiento nuevo WaitForSignal ("GameOver") es más agradable de leer que mientras (! Signals.HasFired ("GameOver")) devuelve nulo, si me lo preguntas, aparte de el hecho de que hacerlo en el motor podría ser más rápido que hacerlo en script.
Un par de ramificaciones no obvias Hay un par de cosas útiles sobre todo esto que la gente a veces extraña y que pensé que debería señalar.
En primer lugar, el rendimiento de rendimiento solo produce una expresión, cualquier expresión, y YieldInstruction es un tipo regular. Esto significa que puede 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 devuelven nuevo WaitForSeconds (), devuelven nuevo WaitForEndOfFrame (), etc., son comunes, pero en realidad no son formas especiales por derecho propio.
En segundo lugar, debido a que estas corutinas son solo bloques iteradores, puede iterar sobre ellas usted mismo si lo desea; no es necesario que el motor lo haga por usted. He usado esto para agregar condiciones de interrupción a una rutina 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 instrucciones de rendimiento, aunque no de manera tan eficaz como si fueran 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 Coroutine es un poco pesado para mi gusto.
Conclusión Espero que esto aclare un poco algo de lo que realmente está sucediendo cuando usas una Coroutine in Unity. Los bloques iteradores de C # son una pequeña construcción maravillosa, e incluso si no está utilizando Unity, tal vez le resulte útil aprovecharlos de la misma manera.
IEnumerator
/IEnumerable
(o los equivalentes genéricos) y que contienen layield
palabra clave. Busque iteradores.