Controlando el requestAnimationFrame en pixi.js con pixi-animationloop

Cuando creamos un juego o una animación usando el canvas de HTML5, sea en 2dContext o usando WebGL, tenemos varías opciones para crear el loop de animación, entre las que se encuentran setInterval, y requestAnimationFrame.

Durante mucho tiempo setInterval fue la opción más usada, definíamos el tiempo que debía durar el frame y listo, pero no era lo más optimo, así que los navegadores empezaron a incorporar requestAnimationFrame, su función es similar al setInterval, salvo que aquí no podemos controlar el tiempo entre frames, sino que es el propio navegador el que se encarga de ejecutar el loop de la forma más óptima pudiendo ser esta diferente dependiendo del dispositivo que se esté usando.

Veamos un ejemplo con setInterval:

var myActor = new Actor();
var frameTime = 1000/60; //60fps
var interval = setInterval(animate, frameTime);

function animate(){
    //loop de animación
    myActor.x += 1; //Movemos el actor 1px a la derecha a cada frame
}

Mismo ejemplo usando requestAnimationFrame:

var myActor = new Actor();

function animate(){
    requestAnimationFrame(animate); //Ejecutamos RAF dentro del loop
    myActor.x += 1;
}

animate(); //Iniciamos el loop

Ambos ejemplos hacen lo mismo, mueven un actor hacia la derecha 1px en cada frame, la diferencia reside en la forma en la que se gestiona el loop, mientras que el loop con setInterval se va a ejecutar 60 veces por segundo, de manera obligatoria, tal y como hemos declarado, el segundo loop puede variar la velocidad de ejecución en base a muchos otros factores.

Centremonos entonces, en requestAnimationFrame, que es, actualmente (salvo exepciones) el que se debe usar para crear animaciones.

Teniendo en cuenta que el RAF (requestAnimationFrame) puede que no se ejecute a velocidades constantes, y nuestro actor se mueve 1px a cada frame, tenemos un problema. Imagina ejecutar nuestro ejemplo en dos dispositivos diferentes, a la misma vez, veremos que la posición de nuestro actor empieza siendo la misma en ambos, pero a medida que transcurre el tiempo muy posibilemente varien, y uno de los actores se encuentre por delante del otro. Esto se debe a que quizás uno de los dispositivos ha ejecutado el loop a 55fps y el otro a 60, lo que a 1px por fps equivale a una diferencia de 5px entre ambos actores. Es un ejemplo un poco pobre, pero creo que se entiende el problema.

La solución pasa por calcular el delta time, que es basicamente el tiempo que ha pasado desde la última ejecucción del loop. Y cambiar el enfoque de nuestro movimiento de 1px por frame a 1px por segundo (1px o 100, o los que sean). De esta forma será más preciso el movimiento, y ambos actores avanzarán de forma identica independientemente de la velocidad del loop.

Veamos un ejemplo del calculo del delta time:

var myActor = new Actor();
var time = 0; //tiempo total
var lastTime = 0; //tiempo total en el último update
var last = 0; //Date.now() en el ultimo update
var delta = 0; //tiempo delta

function animate(){
    requestAnimationFrame(animate); //Ejecutamos RAF dentro del loop

    var now = Date.now();
    time += (now-last)/1000;
    delta = time-lastTime;
    lastTime = time;
    last = now;

    myActor += 1*delta;

    console.log(delta, time);
}

last = Date.now(); //Indicamos el tiempo actual antes de ejecutar el primer fps
animate();

Gracias a este simple calculo el movimiento de nuestro actor será mucho más preciso que al principio, y este pequeño snippet nos sirve de manera universal tanto si usamos el elemento canvas directamente, o si usamos cualquier otro renderer como pueden ser three.js o pixi.js.

Concretamente para pixi.js (renderer que suelo usar asiduamente), he creado un pequeño modulo que hará estos calculos y algunas cosillas más, como parar el loop y recuperarlo si la pestaña del navegador pierde el foco, limitar el tiempo máximo del frame por si el dispositivo de congela y de esta manera evitar saltos bruscos en la animación, etc...

El modulo se llama pixi-animationloop y se encuentra publicado en Github.

Hay varias formas de usarlo; la de toda la vida, bajar el script del directorio build/pixi-animationloop.js y añadirlo en tu index.html justo debajo del script de PIXI. O si usas browserify o Webpack, tan solo tienes que hacer:

npm install pixi-animationloop

El modulo añadirá una nueva clase a pixi, y solo tenemos crear la instancia pasandole el renderer de pixi, y ejecutar el metodo start:

var renderer = new PIXI.autoDetectRenderer(800,600);
document.body.appendChild(renderer.view);

var animationLoop = new PIXI.AnimationLoop(renderer);
animationLoop.start();

Tan fácil como eso, y ya tenemos nuestro loop funcionando, con todos los calculos de tiempo accesibles desde la variable animationLoop. Por ejemplo: .time nos indicaría el tiempo total que ha pasado "dentro" de la animación desde el primer frame, .realTime nos indicaría el tiempo real (sin contar posibles paradas, etc...), .speed nos permitiría alterar la velocidad a la que pasa el tiempo, útil para efectos de tiempo bala o similares, en .delta tendríamos el calculo del delta time en segundo y .deltaMS sería el mismo valor pero en milisegundos.

Si conoces pixi.js, te habrás fijado en que no he creado un contenedor para usarlo como Stage, esto es porque el modulo ya lo crea internamente, pero si quieres puedes pasarle tu propio PIXI.Container como segundo parametro y quedaría referido en animtionLoop.stage.

Además, el modulo emite diferentes eventos que puedes escuchar para ejecutar tus propias instrucciones, como start, stop, prerender, postrender y visibilitychange. Muy útil por ejemplo para añadir un método step/update usando prerender o postrender:

function update(){
    //Metodo update, this es animationLoop.
    myActor += 1 * this.delta;
}

animationLoop.on('prerender', update);

Si quieres saber más sobre la gestión de eventos te recomiendo ir a la documentación de node.js sobre estos: https://nodejs.org/api/events.html

Para terminar dejo el link al modulo en github. Te recomiendo que eches una vistazo rápido a la documentación para entender todo lo que puedes hacer, como parar el loop, eventos, velocidad máxima, etcetera.

Link a pixi-animationloop: https://github.com/Nazariglez/pixi-animationloop