Jul 27, 2008

EnterFrame vs Timer (I)

Hace ya varios años, en una época en la que la web estaba poblada de páginas estáticas y todo el movimiento se reducía a los gif, un programa llamado FutureSplash consiguió sorprender a todos por su increíble capacidad para la animación.

Emulando el sistema de cinematografía, basado en proyectar fotogramas de forma rápida y sucesiva para lograr la sensación de movimiento, FutureSplash incorporaba una linea de tiempo en la que se podía dibujar frame a frame para reproducir luego la película.

Un año más tarde, ese revolucionario programa fue adquirido por Macromedia y nació Flash 1.0, para acabar evolucionando actualmente en Flash CS3, de la mano de Adobe. Y por sorprendente que parezca, nueve versiones después, la filosofía para generar animación se ha mantenido intacta.

Al margen de la animación tradicional en linea de tiempo, la otra forma de generar animación es mediante código, y en ActionScript sí que ha ido evolucionando el tema:

  • En Flash 5 se introdujo el archiconocido enterFrame.
  • En Flash MX nos obsequiaron con setInterval (lo recuerdo como una gran novedad).
  • Y ya en AS3, se ha apostado por la clase Timer como sucesora de setInterval.

Así pues, en Flash 5, para animar mediante código se utilizaba enterFrame, pero con la introducción de setInterval la comunidad empezó a dividirse entre partidarios y detractores de uno y otro sistema. Es fácil encontrar discusiones en blogs, foros y listas sobre este tema, y personalmente he perdido la cuenta de cuantas veces he debatido sobre ello.

En este artículo vamos a estudiar la clase Timer (setInterval está desaconsejada, pero la lógica se puede aplicar igualmente) y el evento enterFrame, con la esperanza de poder definir cuándo es mejor usar una u otra.

EnterFrame

Flash Player cuenta con un motor interno que ejecuta un frame tras otro de forma continua, incluso en películas que no tienen linea de tiempo o que están detenidas. El evento enterFrame está sincronizado con el framerate de la película y se lanza cada vez que el motor alcanza un nuevo frame.

Los frames se cuentan por segundos (fps), y desde el IDE admite el siguiente rango de valores: 0.01 a 120. En AS3 se puede establecer por primera vez el framerate vía código, permitiendo aumentar el valor hasta 1000 fps.

Cualquier objeto que descienda de DisplayObject se puede suscribir (y desuscribir) a un evento enterFrame, pero no se tiene ningún control sobre él, se genera de forma natural en cada película.

Timer

Como comentaba, la clase Timer es una novedad de AS3. Nos permite crear un objeto que periódicamente llama a una función, pero en vez de estar ligado a la velocidad de la película, lo hace definiendo un intervalo de tiempo. De esta manera podemos indicar que un objeto se mueva cada 5 milésimas, 5 segundos o 5 minutos.

A un objeto Timer, junto con la frecuencia, se le puede indicar cuantas veces se debe ejecutar (por ejemplo, llamar a una función 5 veces cada medio segundo), y además cuenta con métodos start, stop y reset para controlarlo y con eventos que indican cada vez que se dispara la función o cuándo se ha completado un ciclo de llamadas.

Una persona sin experiencia en ActionScript a la que se le plantearan los dos mecanismos elegiría utilizar la clase Timer, dada todas las ventajas que aporta sobre enterFrame. Pero para sacar conclusiones, antes hay que conocer los mecanismos internos que tiene Flash Player para actualizar la pantalla.

Actualizando la pantalla

Hace ya alguna semanas escribí un post titulado El sistema de renderizado del Flash Player. Ese post estaba pensado inicialmente para explicarlo en este apartado, pero debido a lo extenso que estaba quedando decidí hacer un artículo al margen. Sino lo has leído o no lo tienes fresco, es aconsejable imprescindible que le eches un vistazo antes de continuar.

Como recordatorio nos quedaremos con esta imagen:

Procesamiento de 1 frame a 1 fps con un evento EnterFrame

Al entrar en un frame se dispara el evento enterFrame, después se ejecuta el código del frame y se espera hasta que que llega la hora de renderizar la pantalla.

Un error muy común

Uno de los errores frecuentes que cometen muchos programadores (y no sólo novatos) es utilizar un Timer porque pueden poner valores muy bajos y hacer así más comprobaciones por segundo.

Por ejemplo, un clásico, utilizarlo para comprobar colisiones entre objetos:

[as3]
var t:Timer = new Timer(1); // 1 milisegundo
t.addEventListener(TimerEvent, comprobarColision);
t.start();

function comprobarColision(e:TimerEvent):void{
if (objetoA.colisiona(objetoB)){
objetoA.cambiaDireccion();
objetoB.cambiaDireccion();
}
}[/as3]

Con la necesidad de saber inmediatamente cuándo los dos objetos se están tocando, lo comprobamos mil veces por segundo.

Este pensamiento conlleva dos grandes problemas:

  1. Ninguna máquina te va a dar un rendimiento suficiente con un valor tan bajo.
  2. Aunque la máquina lo diera, sería inútil, ya que el renderizado de pantalla depende del framerate.

Para mayor claridad, vamos a estudiar estos dos problemas en apartados independientes.

Problema 1

Un objeto Timer, a pesar de funcionar independientemente del framerate, depende en todo momento de la velocidad de procesamiento de la máquina, y es por eso que no es capaz de garantizar el valor asignado.

Por ejemplo, pongamos un caso perfecto en que tenemos puesto durante 10 segundos un Timer a 1 milisegundo: realizaría 10.000 llamadas a una función. Si esta película la ponemos en un ordenador que esté ocupado por otros programas, quizá sólo sea capaz de realizar 6.000 llamadas en el mismo tiempo.

Como no podemos garantizar el rendimiento, es importante no forzar la cpu con valores muy bajos. He aquí una tabla de valores en milisegundos y su correspondencia en fps:

Valor del Timer Framerate
1 ms 1.000 fps
10 ms 100 fps
20 ms 50 fps
30 ms 33’3 fps
50 ms 20 fps
100 ms 10 fps

Como podemos ver, valores por debajo de 10 ms son prohibitivos. Cuanto mayor sea el número de milisegundos mejor podremos garantizar el equilibrio de nuestra película en distintos ordenadores.

Problema 2

Imaginemos un hipotético caso en que nuestra máquina es capaz de procesar a 1 milisegundo y simulemos el ejemplo en que dos bolas colisionan:

Película a 1 fps y comprobación de colisión mediante Timer a 1 milisegundo

Los objetos están ligados al motor central del swf, así que en cada frame se procesa el código y se actualiza la pantalla. Por otro lado, de forma independiente, con un Timer vamos comprobando si hay colisión. Es muy fácil ver que estamos haciendo miles de comprobaciones inútiles. E incluso en el frame 3, dónde por fin el Timer detecta la colisión, aunque a nivel de código se haya procesado en el milisegundo 1, no va a tener representación visual hasta el milisegundo 1000 (después del ciclo de renderizado).

El mismo código coordinado con enterFrame nos hubiera dado 1 colisión de 4 comprobaciones, en vez de 1 de 4000.

Siempre que se ejecute código que tenga una representación por pantalla, si lo hacemos en un enterFrame estamos asegurando que tenemos una relación 1 a 1 entre la ejecución y la visualización en un frame.

Nota: para ejemplificar el problema de los valores bajos en un Timer, he escogido la situación más exagerada: 1 fps y 1 milisegundo. De todas formas, con otros valores más reales, como 24 fps y 10-20 milisegundos, la cantidad de recursos desperdiciados sigue siendo muy grande.

Solucionando los problemas

Ya hemos visto que para solucionar el problema 1 no debemos utilizar valores que fuercen excesivamente la máquina: hay que valorar la cantidad de código que se procesa en el listener del Timer, durante cuánto tiempo, si la película ya está consumiendo recursos…

Para el segundo punto, hay una solución que ya comenté en el artículo del render: el método UpdateAfterEvent, que se puede invocar en las clases MouseEvent, KeyboardEvent y TimerEvent, y que fuerza al player a actualizar la pantalla. Su uso sería así:

[as3]
var t:Timer = new Timer(30);
t.addEventListener(TimerEvent, moverBola);
t.start();

function moverBola(e:TimerEvent):void{
bola.x += 0.5;
e.updateAfterEvent();
}
[/as3]

Hay que notar que no sirve de nada utilizar este método si los objetos se siguen moviendo con el framerate de la película. En el ejemplo de la colisión, lo único que conseguiríamos es hacer las mil comprobaciones por segundo y refrescar la pantalla otras mil veces, pero si las bolas se mueven a 1fps lo único que conseguimos es empeorar todavía más el rendimiento de la película.

En cambio, si procesamos el código de movimiento en el listener del Timer y lo acompañamos con updateAfterEvent, tendremos un movimiento muy fluido.

Pero como suele pasar, una ventaja en un lado conlleva una desventaja en otro. En este caso, la fluidez del Timer más el update significa aumentar el número de ciclos de refresco de pantalla así como el número de ejecuciones del código, por lo que hay que usarlo prudentemente.

Flash Player funciona con framerate

A estas alturas parece obvio decirlo, pero Flash Player funciona bajo el compás que marca el framerate. Por ello, hay que tener en cuenta que si contamos con animación en linea de tiempo, sincronizar animación con código en un enterFrame es automático, pero hacerlo con un Timer es imposible.

Si tenemos una película que corre a 50 fps, una animación en el timeline de 500 frames y un enterFrame activado, en el caso ideal se recorren los 500 frames en 10 segundos lanzado 500 eventos enterFrame. Si el framerate no fuera real (ver apartado “Framerate real” en el artículo anterior) y bajara por ejemplo a 25 fps, la película tardaría 20 segundos en ejecutarse, pero en el frame 228 se lanzaría el evento 228, y en 446 el 446, con lo que siempre tendremos sincronizada nuestra animación con nuestro código.

Sin embargo, debido a la naturaleza del Timer, aunque podamos limitar el número de eventos a 500 repeticiones y calcular el tiempo para que coincida con los 10 segundos que tardará el timeline, en cuanto la máquina no dé el valor exacto (que virtualmente es siempre), nuestro frame número 50 no corresponderá con el evento TIMER número 50.

Internet está poblada de películas Flash (animaciones, juegos, sites…) que no funcionan bien en todas las máquinas debido a que los programadores no tuvieron en cuenta este punto tan básico.

La dependencia del Timer

Unos apartados más arriba comentaba que el Timer funciona al margen del framerate, pero… ¿es esto cierto?. Para comprobarlo podemos crear una película a 120 fps y copiar el siguiente código en el primer frame:

[as3]// Inicializar contador
var cont:int = 0;

// Crear Bola
var bola:Sprite = new Sprite();
bola.graphics.beginFill(0xFF00FF);
bola.graphics.drawCircle(0,0,10);
bola.graphics.endFill();
bola.y = 100;
addChild(bola);

// Timer
var t:Timer = new Timer(50, 100);
t.addEventListener(TimerEvent.TIMER, mover);
t.addEventListener(TimerEvent.TIMER_COMPLETE, fin);
t.start();

// Funciones
function mover(e:TimerEvent):void{
cont++;
bola.x ++;
}
function fin(e:TimerEvent):void{
trace(“Posición x:”, bola.x);
}[/as3]

Este código crea una bola en el escenario y mediante un Timer la mueve 1 pixel hacia la derecha cada 50 milisegundos un total de 100 veces. En otras palabras, pasados 5 segundos (100×50) la bola se habrá movido 100 pixels. Si ejecutamos veremos que realmente sucede así. ¿Pero qué sucede si en el mismo código cambiamos el framerate a 1 fps?

Al ejecutar la película comprobamos 3 cosas:

  1. La bola continúa desplazándose 100 pixels (correcto)
  2. Debido al bajo framerate y que el renderizado ocurre cada segundo, el movimiento no es suave, sino que se visualiza a saltos (correcto)
  3. La bola tarda bastante más de 5 segundos en recorrer los 100 pixels (¿?)

Las dos primeras sentencias son lógicas, pero la tercera no debería serlo. Aunque por pantalla no se reflejen los cambios, el Timer debería procesarse en 5 segundos, ¿no? Pues no, porque incluso los Timers tienen dependencia del framerate. La limitación consiste en que un Timer sólo puede dispararse como máximo 10 veces por cada ciclo de renderizado. Si nos fijamos en esta segunda prueba, la bola ha realizado exactamente 10 saltos antes de completar el recorrido, debido a que una película a 1fps puede alcanzar como máximo los 100 ms.

Sabiendo esto podríamos concluir con otra tabla entre el valor del framerate y la dependencia que crea en el Timer:

Framerate Valor máximo del Timer
1 fps 100 ms (= 10 veces por segundo)
10 fps 10 ms (= 100 veces por segundo)
100 fps 1 ms (= 1000 veces por segundo)

Así pues, los valores que vimos en la primera tabla, realmente están supeditados a esta norma. Otra razón de peso para no poner valores muy bajos en el Timer.

El consumo de memoria

No hay que olvidar que con cada objeto Timer que se crea, se está reservando memoria, al contrario que con enterFrame, que no crea un nuevo objeto, sino que guarda una tabla de objetos suscritos.

Si creamos una clase ObjetoAnimado cuyo motor principal es la clase Timer, y luego creamos mil instancias de ObjetoAnimado, generaremos un consumo de memoria muy elevado, por lo que hay que tener cuidado cuando se trabaje con muchas instancias de la clase Timer.

Nota: Esto puede evitarse centralizando el código. En la segunda parte de este artículo se tratara este recurso.

Timer negatifo, nunca positifo

Leído lo leído, va a resultar que la clase Timer sólo da problemas, y tampoco es así. El hecho de que esta clase funcione en relación al tiempo la hace especialmente útil, por ejemplo, realizando tareas cada ciertos periodos, ya que en caso de hacerse en un enterFrame resultarían repetitivas.

En el caso de la animación, la gran ventaja del Timer es que puede actuar independientemente sobre distintos objetivos, dándonos mucha más libertad que con enterFrame, en el cuál la velocidad es fija para toda la película (aunque en AS3 se pueda variar en tiempo de ejecución, sigue afectando a todos los elementos). Además, combinándolos con updateAfterEvent() se puede conseguir animaciones más fluidas.

Para sacarle el máximo provecho, sólo hay que entender su naturaleza y sus limitaciones.

Resumiendo

Como programadores, es un deber optimizar al máximo nuestros desarrollos, y en cualquier lenguaje visual, optimizar el código es un arte. A pesar de que año tras año la velocidad de procesamiento de los ordenadores aumenta, la memoria disponible cada vez es mayor, la AVM (ActionScript Virtual Machine) es más potente, etc. es frecuente ver swf que agotan los recursos del Player.

En ActionScript, un buena compresión del evento enterFrame y la clase Timer (combinado con el sistema de renderizado del Player), nos abre una gran puerta a desarrollar mejores programas. Con este artículo, espero haber puesto las bases de esa compresión, y allanar el camino para saber cuándo se debe utilizar uno u otro.

Dejo pendiente un segundo artículo en esta serie donde explicaré casos reales de uso, y en función de mi experiencia, mostraré cuál es la mejor manera de abordar el problema.

Información del artículo

Post publicado el 27 de July de 2008 a las 18:00 por llops

Categorias: Artículos

Etiquetas: , , , , , , ,

Comparte

1 trackback

8 comentarios

  • Yo soy mas de enterFrame que de Timer (setInterval), pero en algunas ocasiones enterfFrame no es la solución aconsejable… todas aquellas situaciones donde se quiera algo basado en tiempo, como un reloj, un contador de un juego que te da, digamos 20secs de tiempo, etc.. en esas situaciones es mejor un setInterval/Timer y desvincularse del enterFrame. Hacer un reloj que tire de enterFrame y que calcule la hora o segundos basandose contadores * fps es cagarla si la precisión está en las especificaciones del proyecto.

    Gran articulo por otro lado.

  • Yo solo necesité hacer algo de ese tipo de precisión y opte por hacer un getTimer y restar los ms para obtener el tiempo… no se si es correcto o no… pero bueno XD

    Espero con ansias el segundo artículo ;)

  • Eliseo

    Salvo para crear eventos que deban ocurrir mucho más lentos que los fps, creo que la opción más correcta es usar EnterFrame. Eso no significa que los movimientos deban depender de los fps. Podemos hacer que dependan del tiempo transcurrido guardando el tiempo pasado entre un EnterFrame y otro. De ese modo, cuando bajen los fps reales, veremos peor la animación, pero no más lenta (aunque supongo que es de lo que trata la segunda parte del artículo)

  • Vaya cacho de artículos que te curras!!
    Pero el segundo artículo déjalo para después de verano. Descansa y ves a la playa. ;)

    Saludos!!

  • Pues ya adelanto que el 99% de la veces, mi motor central es un enterFrame, y como apuntáis, para soluciones muy precisas el cálculo de tiempo es la clave.

    #4 > Juan, no sufras que estoy bastante morenito! Eso sí, te voy a hacer caso y voy a cerrar el blog por vacaciones :)

    Saludos!

  • Cool, пригодится.

  • Ken

    Interesante el articulo, ya habia oido rumores sobre este tipo de situacion. Sin embargo con este articulo se revela la verdadera naturaleza del Timer que por lo visto no es tan “cronometrico”, y el consumo de memoria es algo que alarma mucho. Ohh … gracias por la magnifica aclaracion, y pensar que ya perdi la cuenta de cuantos Timers(1) he utilizado XD

  • Fabian

    Muy informativo el artículo, me encuentro desarrollando una aplicación en Flash, para facebook, y realmente es importante que tengamos presente no abusar del uso excesivo de la memoria, es claro que cuando se trabaja con Timers se puede llegar a obtener resultados no esperados y hacer que nuestras películas se muestren más lentas, en comparación con EnterFrame, digamos que en cierta medida, cada uno es utili para cierto tipo de cosas, sin menos cabar la reputación de los Timers, EnterFrame es una buena opción para los motores de juegos, en cuanto a los Timers, traten de hacer uso razonable de ellos y usenlos en ocasiones necesarias.

    Muy buen artículo… XD