Aprende a crear juegos en HTML5 Canvas

lunes, 19 de noviembre de 2012

RequestAnimationFrame

En el pasado, para crear temporizadores en general, se utilizaba la función setTimeout. Esta función ha servido desde hace muchos años para toda clase de acciones por temporizador para las páginas web, sin embargo, no fue contemplada para animaciones, que requiere múltiples llamadas por segundo, consumiendo muchos recursos de nuestra computadora, aun si no estamos haciendo uso de la aplicación en cuestión.

Las compañias desarrolladoras de navegadores web han estado consciente de ello, y por tanto han ideado una mejor solución para esta tarea: la función requestAnimationFrame.

Esta función optimiza el uso de información, actualizándose de forma automática tan pronto el CPU le permite (Comúnmente, 60 cuadros por segundo en computadoras de escritorio), mejorando la capacidad del manejo de información en animaciones, consumiendo menos recursos, e incluso mandando a dormir el ciclo cuando la aplicación deja de tener enfoque, dando como resultado, un mejor manejo de las animaciones.

Para usar resquestAnimationFrame, tan solo debes llamarle como la primer línea de una función, enviando como primer parámetro la misma función que la ha mandado a llamar, para que le llame de regreso después del tiempo de intervalo, tal como se muestra a continuación:
function run(){
    window.requestAnimationFrame(run);
    act();
    paint(ctx);
}
requestAnimationFrame(run) equivaldría a llamar un setTimeout(run,17), pero de forma optimizada.

Soporte para navegadores antiguos.


En el tiempo que esta entrada está siendo escrita, requestAnimationFrame es una función relativamente nueva, por lo que los navegadores que no estén actualizados podrían no soportarla, o usar una función experimental no-estándar de ella. Para saber más sobre la versión cuando esta función fue implementada, puedes visitar http://caniuse.com/requestanimationframe, dónde muestran el avance de su soporte.

Para poder usar requestAnimationFrame en estos navegadores antiguos, existen muchas soluciones posibles. La más simple y popular, es agregar esta función a tu código:
window.requestAnimationFrame = (function () {
    return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 17);
        };
}());
Esta función personalizada creará una función requestAnimationFrame con la mejor alternativa posible. Primero intentará utilizar la función estándar. Si falla, intentará cargar la versión de webkit, que es soportada por versiones antiguas de Safari y de Google Chrome, así como algunos navegadores móviles. Si falla, intentará cargar la versión de mozilla, que es la que usan las versiones viejas de Firefox. Finalmente, si no existe soporte para ninguna versión, estándar o experimental, como en el caso de Opera con Presto y las versiones antiguas de Internet Explorer, se creará una llamada clásica a un setTimeout a 17 milisegundos, que es aproximadamente la taza de actualización de requestAnimationFrame.

De esta forma, podremos comenzar a usar requestAnimationFrame en el desarrollo de nuestro juegos.

Midiendo los cuadros por segundo.


Para comprobar el rendimiento real de requestAnimationFrame en nuestro dispositivo, calcularemos los cuadros por segundo (FPS) de nuestro lienzo. Para hacer esto, usaremos el código de Parte 2. Animando el canvas. Empecemos creando cuatro variables, cuyos propósitos iré explicando conforme avancemos en la explicación:
var lastUpdate = 0,
    FPS = 0,
    frames = 0,
    acumDelta = 0;
Insertaremos el código a continuación dentro de la función run, justo después de llamar a requestAnimationFrame. Para empezar, calcularemos la delta del tiempo, esto es, el tiempo que ha pasado desde la última vez que el ciclo fue ejecutado en milisegundos, y lo dividiremos entre 1000 para convertirlo en segundos:
    var now = Date.now(),
        deltaTime = (now - lastUpdate) / 1000;
    if (deltaTime > 1) {
        deltaTime = 0;
    }
    lastUpdate = now;
En la primer línea, almacenamos el resultado de la función en una variable. Este valor es la cantidad de milisegundos desde el 1 enero de 1970, 00:00:00 UTC (Un estándar).

En la segunda línea, calculamos la delta del tiempo restando el ahora, al tiempo que teníamos almacenado. Para comprender esto mejor, hay que notar antes que en la cuarta línea, la variable lastUpdate obtiene el valor de now. Ahora sí, comprendemos que al siguiente ciclo, now-lastUpdate nos dará la delta del tiempo.

Sin embargo, en el primer ciclo, lastUpdate tiene un valor de cero, por lo que restar now-lastUpdate nos haría un valor enorme que causaría efectos no deseados. Por ello, en la tercer línea, si el tiempo delta es mayor a un segundo, su valor será descartado. Esto también sirve para prevenir valores no deseados en caso que el usuario salga de la pestaña actual por un momento, o la computadora se cuelgue por algunos segundos.

Después de comprender esta compleja pero simple sección, pasemos a lo que sigue. En cada ciclo sumaremos en uno los cuadros, y sumaremos la delta del tiempo a acumDelta:
    frames += 1;
    acumDelta += deltaTime;
Posteriormente, averiguaremos si ha pasado ya un segundo, preguntando si acumDelta es mayor a 1; de ser así, asignaremos la cantidad de cuadros pasados a nuestra variable FPS, y haremos un reset a las variables frames y acumDelta:
    if (acumDelta > 1) {
        FPS = frames;
        frames = 0;
        acumDelta -= 1;
    }
De esta forma, hemos obtenido los cuadros por segundo de nuestro juego. Ahora solo nos falta imprimirlos en pantalla:
    ctx.fillText('FPS: ' + FPS, 10, 10);
La posible desventaja de usar requestAnimationFrame, es que tiene una tasa de actualización muy pequeña, que nuestros juegos se harán muy lentos en dispositivos de bajo rendimiento como computadoras antiguas y dispositivos móviles. Para prevenir esto, se debe usar un método para regularizar el tiempo, los cuales hay muchos, pero esos los veremos en la siguiente entrega.

Código Final

[Canvas not supported by your browser]
/* RequestAnimationFrame */
'use strict';
var canvas = null,
    ctx = null,
    lastUpdate = 0,
    FPS = 0,
    frames = 0,
    acumDelta = 0,
    x = 50,
    y = 50;

window.requestAnimationFrame = (function () {
    return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 17);
        };
}());

function paint(ctx) {
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = '#0f0';
    ctx.fillRect(x, y, 10, 10);

    ctx.fillStyle = '#fff';
    ctx.fillText('FPS: ' + FPS, 10, 10);
}

function act() {
    x += 2;
    if (x > canvas.width) {
        x = 0;
    }
}

function run() {
    window.requestAnimationFrame(run);

    var now = Date.now(),
        deltaTime = (now - lastUpdate) / 1000;
    if (deltaTime > 1) {
        deltaTime = 0;
    }
    lastUpdate = now;

    frames += 1;
    acumDelta += deltaTime;
    if (acumDelta > 1) {
        FPS = frames;
        frames = 0;
        acumDelta -= 1;
    }

    act();
    paint(ctx);
}

function init() {
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
    run();
}

window.addEventListener('load', init, false);

6 comentarios:

  1. Muy bien explicado, como en todas las entradas de tu blog, muchas gracias por compartir el conocimiento

    ResponderEliminar
  2. Y yo que crei que nunca terminaria de entender esto. Gracias!

    ResponderEliminar
  3. Acabo de descubrir esto y es simplemente genial :D

    ResponderEliminar
  4. Genial, pero en este if:

    if (acumDelta > 1) {
    FPS = frames;
    frames = 0;
    acumDelta -= 1;
    }

    creo que seria mejor si hicieras acumDelta igual a 0 y no restarle 1 ya que el programa podría entrar a este if con acumDelta>1 y luego no entraría exactamente cada segundo.

    ResponderEliminar
    Respuestas
    1. De hecho, por el contrario, está formula ayuda a evitar el sobreflujo de milisegundos. De hacerlo con tu técnica, si en la acumulación llega a 1.05 segundos, 1.12 segundos, etc, estos milisegundos se perderán si se convierte a cero, creando un desfase que eventualmente se convertirá en un retraso de segundos.

      De la forma en que se hace en este ejemplo, esos milisegundos se quedan acumulados en la variable, llegando así a compensarse cuando el siguiente segundo termina.

      Eliminar