Post on 04-Feb-2016
description
Corutinas en Unity
1
Corutinas en Unity3D: Update Me Please!
Comúnmente cuando estamos aprendiendo a programar en Unity3D, la función
Update() resulta ser la función mas cargada de código. Puesto que es invocada cada
frame del juego, allí podemos manipular acciones en determinados tiempos y manejar
eventos de acuerdo a los estados de juego. Sin embargo, esta no es siempre la manera
optimizada de abordar estas situaciones, y casi siempre es preferible (en términos de
performance, por ejemplo para mobile iOS y Android) usar las Coroutines, o en
español, Corutinas.
PROBLEMA
Supongamos por ejemplo que en determinada escena nuestro personaje principal abre
una puerta y que se desee programar que esta se cierre después de un determinado
tiempo. Una manera de abordar esta tarea podría ser el uso de una zona trigger cerca de
la puerta de manera que cuando el personaje salga de esta zona, invoquemos nuestra
función de cierre de puerta. Infelizmente, esta solución a pesar de que puede funcionar
bien en muchos casos, no es lo que queríamos exactamente: Cerrar la puerta después de
un “determinado tiempo”; dado que no podemos estar seguros cuanto tiempo demorará
el personaje en salir de la zona de trigger.
Una manera de solucionar nuestro problema de tiempos usando la función Update() , es
usar una variable de estado “puertaAbierta” que nos permite saber si la puerta esta
abierta en un determinado instante de tiempo, a la vez de usar una variable “tPAbierta”
para contar cuanto tiempo lleva abierta la puerta en cualquier momento del juego, de
manera que si esta variable supera el tiempo de cierre que nosotros especifiquemos, la
puerta se cierre:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool puertaAbierta = false; float tPAbierta; float tiempoDeCierre = 5f; //tiempo que queremos //que dure la puerta abierta void AbrirPuerta(){ /*... codigo de abrir puerta aqui... */ puertaAbierta = true; tPAbierta=0; //comenzamos a contar el tiempo //desde la abertura de la puerta. }
void Update(){ if(puertaAbierta){ tPAbierta+= Time.deltaTime; //contamos tiempo desde abertura if(tPAbierta > tiempoDeCierre){//cerramos /*... codigo de cerrar puerta aqui ...*/ puertaAbierta = false; } } }
Corutinas en Unity
2
Esta solución funciona, pero no es la mejor. Observemos que incluso cuando estemos en
cualquier otro momento de la escena que no tenga nada que ver con la puerta, nuestro
código aun estará evaluando la veracidad de la sentencia if(puertaAbierta){…} en cada
frame.
USANDO LAS CORUTINAS
Para obtener una solucion mas limpia y optimizada debemos usar corutinas. Las
corutinas en Unity3D son funciones especiales con las siguientes funcionalidades y
características:
Permiten que creemos una función que ejecuta acciones (bloques de código) en
sucesión, y especialmente con la posibilidad de hacer pausas entre cada acción.
Permiten que creemos una función que se invoque automáticamente de
diferentes maneras: 1. cada determinada cantidad de segundos; 2. en cada
fixedTime (similar a FixedUpdate); 3. al final de cada frame de TODAS las
cámaras que estén haciendo render en la escena.
Pueden ser pausadas y reanudadas por medio de sentencias “yield”. Puesto que
la podemos interrumpir cuando queramos, es ideal para situaciones que
involucren tiempos como nuestro caso de la puerta.
Se ejecutan asincronamente en el hilo de ejecución principal (main thread).
HELLO WORLD EN CORUTINAS
El siguiente código seria el equivalente a el hello world usando corutinas:
1
2
3
4
5
6
7
8
9
10
IEnumerator helloWorldCorutina(){ /* Imprima HELLO */ Debug.Log("HELLO");
/*retorne, y reanude a partir de aqui dentro de 2 segundos */ yield return new WaitForSeconds(2f);
/* Imprima WORLD */ Debug.Log("WORLD"); }
ENTENDIENDO LAS CORUTINAS
Podemos entender las corutinas intuitivamente de la siguiente manera: Es una función
normal que contiene una o muchas sentencias yield. Cuando una de estas sentencias es
alcanzada, nuestro código se suspende hasta que esta sentencia se cumpla. Cuando se
cumple, nuestro código continua normalmente, justo donde se había interrumpido.
Actualizaciones normales de Coroutines (corrutinas) son ejecutadas después del return
que hace la función Update. Una coroutine es una función que puede suspender su
ejecución (yield) hasta que la YieldInstruction finalice. Diferentes usos de Coroutines:
Corutinas en Unity
3
yield La coroutine va a continuar después de que todas las funciones Update hayan sido llamadas en el siguiente frame.
yield WaitForSeconds Continúa después de un retraso de un tiempo específico, después de que todas las funciones Update hayan sido llamadas para el frame
yield WaitForFixedUpdate Continua después de que todos los FixedUpdate hayan sido llamadas en todos los scripts.
yield WWW Continúa después de que una descarga WWW haya sido completada yield StartCoroutine Encadenada la coroutine, y va a esperar para que la coroutine
MyFunc haya sido completado primero.
Existen múltiples sentencias yields en Unity3D, estas son:
1
2
3
4
5
6
7
8
9
10
11
12
13
/*Interrumpa esta corutina, y reanude solo al final de render de todas las camaras presentes en la escena.*/ yield return new WaitForEndOfFrame();
/* Interrumpa esta corutina, y reanude solo en el proximo Fixed
frame.*/ yield return new WaitForFixedUpdate();
/*Interrumpa esta corutina, y reanude solo despues de "segs" cantidad de segundos.*/ yield return new WaitForSeconds(segs); /*Interrumpa y NO reanude nunca mas esta corutina*/ yield break;
En C# una corutina debe ser declarada como tipo IEnumerator, y además debe devolver
al menos una vez una sentencia yield. Consideremos la siguiente corutina:
1
2
3
4
5
6
7
8
9
IEnumerator corutinaEjemplo(){ while(true){ /*retorne, y reanude a partir de aqui dentro de 5 segundos */ yield return new WaitForSeconds(5f);
/*Codigo de algo que necesito actualizar cada 5 segundos */ /* ..... */ } }
Corutinas en Unity
4
Como mencione anteriormente, las sentencias yield son sentencias especiales de
interrupción de corutinas, si no las usáramos (suponiendo se pudiera compilar una
corutina sin ellas) el bucle while de este ejemplo quedaría ocupando el hilo principal
de ejecucion eternamente, y obtendríamos un bucle infinito sin permitir que otras tareas
se ejecuten en este hilo. Lo especial de las corutinas es precisamente esto, mediante las
sentencias yield podemos liberar al hilo de ejecución principal para que se ocupe de
otras tareas, y después reanudar nuestra corutina de acuerdo a como lo especifique la
sentencia yield.
Es asi que se podria deducir erróneamente que las corutinas son como código que corre
en paralelo a otras tareas. Y digo erróneamente porque ESTO NO ES VERDAD y en
realidad este paralelismo no ocurre: todo se ejecuta en el hilo principal, pero en orden
diferente, alternado las tareas de las corutinas con el resto de tareas. Para verificar la
veracidad de esto ultimo, hagan el intento de correr la siguiente corutina, y se darán
cuenta que el editor de unity colapsa:
1
2
3
4
5
6
IEnumerator corutinaColapsa(){ yield return new WaitForSeconds(1f); while(true){ Debug.Log("HELLO WORLD"); } }
COMO INVOCAR UNA CORUTINA
A diferencia de las funciones normales, para que una corutina empiece su ejecución
usando C#, utilizamos el método (heredado de MonoBehaviour)
StartCoroutine(miCorutina()). Por ejemplo para invocar que nuestra corutina hello
world helloWorldCorutina() arranque, podríamos usar:
1
2
3
void Start(){ StartCoroutine(helloWorldCorutina()); }
Finalmente para terminar, damos la solución del problema de cierre de puerta propuesto
anteriormente, ahora usando nuestras alabadas corutinas, con las cuales resulta ser
sorprendentemente una tarea simplificada:
1
2
3
4
5
IEnumerator AbriryCerrarPuerta(){ /*... codigo de abrir puerta ...*/ yield return new WaitForSeconds(tiempoDeCierre); /*... codigo de cerrar puerta ...*/ }
Corutinas en Unity
5
La moraleja es: Si necesitas que algo se chequee constantemente, no en todos los
frames, sino en solo durante un periodo determinado de tiempo; o si necesitas
sincronizar tareas de acuerdo tiempos exactos: usa corutinas y no cargues de código a la
función Update()!
UNITY 3D– StartCoroutine
En este post vamos a ver cómo se utiliza la función de UNITY3D StartCoroutine.
yield
yield es una palabra reservada que indica al compilador que en la función en la que
aparece es un iterador. yield se utiliza con la palabra reservada return que devuelve un
objeto al iterador. También se utiliza con la palabra reservada break para indicar que el
iterador se ha finalizado la iteración. Para profundizar más, os recomiendo echar un
vistazo a la documentación.
Vamos a ver un ejemplo para que quede un poco más claro.
1: static void Main()
2: {
3: foreach (int value in ComputePower(2, 30))
4: {
5: Console.Write(value);
6: Console.Write(" ");
7: }
8: Console.WriteLine();
9: }
10:
11: public static IEnumerable<int> ComputePower(int number, int
exponent)
12: {
13: int exponentNum = 0;
14: int numberResult = 1;
15:
16: while (exponentNum < exponent)
17: {
18: numberResult *= number;
19: exponentNum++;
20:
21: yield return numberResult;
22: }
23: }
La función ComputePower nos devuelve todos los valores que hay de number elevado
desde 1 hasta exponent, al utilizar yield, podemos llamar a esta función desde dentro de
un bucle foreach.
¿Esto para qué nos sirve dentro de Unity3D?
En Unity3D nos puede ser util cuando usamos la función StartCoroutine, ya que esta
función lo que hace es ejecutar un método cuyo retorno es un IEnumerator.
Corutinas en Unity
6 StartCoroutine
En primer lugar vamos a ver que es una coroutine:
Una coroutine es una función que se ejecuta parcialmente, presuponiendo que se
cumplen las condiciones adecuadas, y se continuará en el futuro hasta que esta finalice.
Hay que tener en cuenta que una coroutine no se ejecuta de forma asíncrona.
Unity procesa las coroutine en cada frame del juego, después del Update y antes del
LateUpdate, esto es así en la mayoría de los casos, salvo algunas circunstancias
especiales. Aquí os pongo un diagrama con el ciclo de vida en Unity3D, para una
explicación detallada, no dudéis en echar un ojo aquí.
Con la función StartCoroutine, lanzamos esta función. La sintaxis es
StartCoroutine(“Nombre de función”);.
Cuando en una coroutine se llega a un yield, la ejecución de dicha función se detiene
hasta el siguiente frame en la mayoría de los casos, aunque también es posible detener le
ejecución por un lapso de tiempo si usamos la función WaitForSeconds.
Esto podría usarse para mostrar un efecto a lo largo del tiempo, o si en el juego tenemos
que hacer algún tipo de validación recurrente pero que no es necesaria que se haga en
cada frame.
En la propia documentación de Unity3D puedes encontrar una muy buena explicación
sobre las coroutines.
Coroutines
Cuando usted llama a una función, esta se ejecuta en su totalidad antes de retornar. Esto
significa efectivamente que cualquier acción tomando lugar en una función debe
suceder en una sola actualización de frame (cuadro); un llamado a una función no puede
ser usado para contener una animación procedimental o una secuencia de eventos en el
tiempo. Como un ejemplo, considere la tarea de reducir gradualmente el valor alfa
(alpha) de un objeto (opacidad) hasta que se convierta completamente invisible.
void Fade() {
for (float f = 1f; f >= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
}
}
Tal como está, la función Fade (Desvanecer) no tendrá el efecto que usted espera. Para
que el desvanecimiento sea visible, el alfa debe ser reducido sobre una secuencia de
frames (cuadros) para mostrar los valores intermedios siendo renderizados. Sin
embargo, la función se ejecutará en su totalidad en una sola actualización de cuadro.
Los valores intermedios nunca se verán y el objeto desaparecerá instantáneamente.
Corutinas en Unity
7
Es posible manejar situaciones como estas agregando código a la función Update que
ejecuta el desvanecer en una base cuadro-a-cuadro (frame-by-frame). Sin embargo, es
usualmente mas conveniente usar una coroutine (corrutina) para este tipo de tarea.
Una coroutine (corrutina) es como una función que tiene la habilidad de pausar su
ejecución y retornar el control a Unity para luego continuar donde dejó en el siguiente
frame. En C#, una coroutine es declarada así:-
IEnumerator Fade() {
for (float f = 1f; f >= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return null;
}
}
Es es esencialmente una función declarada con un tipo de retorno de IEnumerator y con
una instrucción de retorno yield (yield return) incluida en algún lugar de su cuerpo. La
linea de retorno yield es el punto en el cual la ejecución se pausará y reanudará en el
siguiente frame (cuadro). Para establecer una coroutine en ejecución, usted necesita usar
la función StartCoroutine
void Update() {
if (Input.GetKeyDown("f")) {
StartCoroutine("Fade");
}
}
Por defecto, una coroutine es reanudada en el frame después de que haga yield, pero
también es posible introducir un tiempo de retardo usando WaitForSeconds:-
IEnumerator Fade() {
for (float f = 1f; f >= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return new WaitForSeconds(.1f);
}
}
Esto puede ser usado como una forma de propagar un efecto sobre un periodo de tiempo
pero también es una útil optimización. Muchas tareas en un juego necesitan ser llevadas
a cabo periódicamente y la forma mas obvia de hacer esto es incluirles en la función
Update. Sin embargo, esta función usualmente será llamada muchas veces por segundo.
Cuando una tarea no requiere ser repetida tan frecuentemente, le puede poner en una
coroutine para obtener una actualización regularmente pero no en cada frame. Un
ejemplo de esto puede ser una alarma que alerta al usuario si un enemigo está cerca. El
código podría verse algo como esto:-
Corutinas en Unity
8 function ProximityCheck() {
for (int i = 0; i < enemies.Length; i++) {
if (Vector3.Distance(transform.position,
enemies[i].transform.position) < dangerDistance) {
return true;
}
}
return false;
}
Si hay muchos enemigos, entonces llamar esta función cada cuadro podría introducir
una sobrecarga significativa. Sin embargo, podría usar una coroutine para llamarle cada
décima de segundo:-
IEnumerator DoCheck() {
for(;;) {
ProximityCheck;
yield return new WaitForSeconds(.1f);
}
}
Esto reducirá en gran cantidad el número de comprobaciones llevados a cabo sin ningún
efecto notable en la experiencia de juego.