Optimizando tu juego en JavaScript para Firefox OS

7 agosto, 2013 23:03 por

Este artículo fue originalmente publicado en el blog de Mozilla Hacks. Traducción por Ángel Fernando Quiroz.

Cuando desarrollas en un procesador quad core con 16 gigabytes de RAM, fácilmente puedes olvidar considerar cómo será el desempeño en un dispositivo móvil. Este artículo detalla algunas mejores prácticas y cosas a considerar para portar un juego a Firefox OS o algún hardware objetivo similar.

Aprovechando lo mejor de 256 Mb RAM/800 Mhz CPU

Hay muchas áreas de interés a tener en cuenta mientras se desarrolla un juego. Cuando tu objetivo es dibujar 60 veces por segundo, una recolección de basura en la memoria e intentos de dibujo ineficientes comienzan a interponerse en tu camino. Empecemos con lo básico…

No optimizar en exceso

Esto puede sonar un poco contradictorio tratándose de un artículo sobre optimización de juegos pero, la optimización es el último paso y debe de realizarse sobre un código completo y funcional. Aunque no está de más tener estos consejos y trucos presentes, no vas a poder saber si serán necesarios hasta que hayas terminado de crear el juego y lo hayas probado en un dispositivo.

Optimizar el dibujado

Dibujar en 2D con canvas en HTML5 es el cuello de botella más grande en la mayoría de juegos JavaScript, ya que todo el resto de actualizaciones son generalmente cambios de álgebra que no afectan al DOM. Las operaciones con canvas están aceleradas por hardware, con lo que puedes tener algo de espacio extra para respirar.

Usar renderizado de píxeles enteros

El renderizado de subpixeles ocurre cuando se renderizan objetos en un canvas sin valores enteros.

ctx.drawImage(myImage, 0.3, 0.5)

Esto hace que el navegador haga cálculos extras para crear el efecto anti-aliasing. Para evitar eso, asegúrate de redondear todas las coordenadas usadas en las llamadas a drawImage usando Math.floor o como leerás más adelante en el artículo, operadores bitwise.
jsPerf – drawImage con píxeles enteros.

Dibujo en caché en un canvas fuera de pantalla

Si te encuentras con operaciones de dibujo complejas en cada frame, considera crear un canvas fuera de pantalla: dibuja una vez (o cada vez que cambie) en el canvas fuera de pantalla y luego en cada frame dibuja ese canvas de fuera de pantalla.

myEntity.offscreenCanvas = document.createElement(“canvas”);
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext(“2d”);
 
myEntity.render(myEntity.offscreenContext);

Utilizar moz-opaque en la etiqueta canvas (en Firefox solamente)

Si tu juego usa canvas y no necesita ser transparente, asigna el atributo moz-opaque en la etiqueta canvas. Esta información puede ser usada internamente para optimizar el renderizado.

<canvas id="mycanvas" moz-opaque></canvas>

Descripción detallada en Bug 430906 – Agregar el atributo moz-opaque en canvas.

Escalar canvas usando transformaciones CSS3

Las transformaciones CSS3 son más rápidas usando la GPU. El mejor caso es no escalar el canvas o tener un canvas más pequeño y ampliarlo, en vez de tener un canvas más grande y reducirlo. Para Firefox OS, la resolución objetivo es 480 x 320 px.

var scaleX = canvas.width / window.innerWidth;
var scaleY = canvas.height / window.innerHeight;
 
var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);
 
stage.style.transformOrigin = "0 0"; //scale from top left
stage.style.transform = "scale(" + scaleToFit + ")";

Ver el ejemplo funcionando en este jsFiddler.

Renderizado al vecino más cercano para escalar el pixel-art

Como en el último punto, si tu juego utiliza pixel-art, debes usar una de las siguientes técnicas cuando escales el canvas. El algoritmo de redimensionado por defecto crea un efecto borroso y arruina la belleza de los píxeles.

canvas {
  image-rendering: crisp-edges;
  image-rendering: -moz-crisp-edges;
  image-rendering: -webkit-optimize-contrast;
  -ms-interpolation-mode: nearest-neighbor;
}

o

var context = canvas.getContext(‘2d’);
context.webkitImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;

Más documentación disponible en image-rendering en MDN.

CSS para imágenes grandes de fondo

Si como en la mayoría de juegos tienes una imagen estática de fondo, usa un elemento DIV plano con una propiedad CSS para el fondo y posiciónalo debajo del canvas. Esto evitará dibujar una imagen grande en el canvas a cada momento.

Múltiples canvas por capa

De manera similar al punto anterior, podrías darte cuenta de que tienes algunos elementos que se mueven y cambian frecuentemente, mientras que otros (como la IU) nunca cambian. Una optimización en esta situación consiste en crear capas, usando múltiples elementos canvas.

Por ejemplo puedes crear una capa para la interfaz gráfica que se sitúe por encima de todo y se dibuje solamente durante el ingreso de datos del usuario. Puedes crear una capa para el juego en donde existan elementos que cambien frecuentemente y una capa de fondo para elementos que cambien poco.

<div id="stage">
<canvas id="ui-layer" width="480" height="320"></canvas>
<canvas id="game-layer" width="480" height="320"></canvas>
<canvas id="background-layer" width="480" height="320"></canvas>
</div>
 
<style>
#stage {
width: 480px;
height: 320px;
position: relative;
border: 2px solid black
}
 
canvas { position: absolute; }
#ui-layer { z-index: 3 }
#game-layer { z-index: 2 }
#background-layer { z-index: 1 }
</style>

No escalar imágenes en drawImage

Mantén en caché varios tamaños de tus imágenes en un canvas fuera de pantalla cuando carguen en lugar de escalar constantemente en drawImage.

jsPerf – Desempeño escalando drawImage en canvas.

Si es posible, crea tu propia librería de física ya que librerías como Box2D no funcionan bien en dispositivos Firefox OS de baja gama.

Cuando el soporte asm.js aterrice en Firefox OS, las librerías compiladas para Emscripten pueden tomar ventaja de un rendimiento casi nativo. Lee más en esta evaluación de Box2D.

Uso de WebGL en lugar de Context 2D

Es más fácil de decir que de hacer, pero si dejas que la GPU se encargue de todos los pesados gráficos, la CPU quedará liberada para trabajar mejor. Aunque WebGL es 3D, puedes usarlo para dibujar superficies 2D. Existen algunas librerías cuyo objetivo es extraer los contextos de dibujo.

Minimizar la recolección de basura en memoria

JavaScript puede echarnos a perder en lo que se refiere a la gestión de memoria. Generalmente no debemos preocuparnos sobre la pérdida de memoria o de una asignación conservadora de la misma. Pero si hemos asignado demasiado y recolección de basura en memoria sucediera en medio de un frame, esto podría ocupar un tiempo valioso y resultar en un descenso visible en FPS.

Conjunto de objetos y clases comunes

Para minimizar la cantidad de objetos que están siendo limpiados durante la recolección de elementos no utilizados, puedes usar un conjunto preinicializado de objetos y reutilizarlos, en lugar de crear nuevos objetos todo el tiempo.

CÓDIGO DE EJEMPLO DE UN CONJUNTO GENÉRICO DE OBJETOS:

var objectPool = [];
var marker = 0;
var poolSize = 0;
 
//cualquier objeto JavaScript anterior
function commonObject () { }
 
commonObject.create = function () {
    if (marker >= poolSize) {
        commonObject.expandPool(poolSize * 2);
    }
 
    var obj = objectPool[marker++];
    obj.index = marker - 1;
    obj.constructor.apply(obj, arguments);
    return obj;
}
 
//colocar el nuevo objeto en el conjunto de objetos
commonObject.expandPool = function (newSize) {
    for (var i = 0; i < newSize - poolSize; ++i) {
        objectPool.push(new commonObject());
    }
 
    poolSize = newSize;
}
 
//intercambiarlo con el último objeto disponible
commonObject.prototype.destroy = function () {
    marker--;
    var end = objectPool[marker];
    var endIndex = end.index;
 
    objectPool[marker] = this;
    objectPool[this.index] = end;
 
    end.index = this.index;
    this.index = endIndex;
}
 
//hacer esto tan grande como tú pienses que necesitas
commonObject.expandPool(1000);

Evitar que los métodos internos creen basura en memoria

Hay varios métodos JavaScript que crean nuevos objetos en vez de modificar los existentes. Esto incluye: Array.slice, Array.aplice, Function.bind.

LEE MÁS SOBRE LA RECOLECCIÓN DE BASURA EN MEMORIA

Evita llamadas frecuentes a localStorage

LocalStorage usa lectura y escritura de archivos y bloquea el hilo principal para recuperar y guardar los datos. Utiliza un objeto en memoria para almacenar en caché los valores de localStorage e incluso guarda contenidos escritos para cuando el jugador no se encuentre en el medio del juego.

EJEMPLO DE CÓDIGO DE UN OBJETO DE ALMACENAMIENTO ABSTRACTO:

var Storage = {
    _cache: {},
 
    get: function (key) {
        if (!this._cache[key]) {
            this._cache[key] = localStorage[key];
        }
 
        return this._cache[key];
    },
 
    set: function (key, value) {
        this._cache[key] = value;
    },
 
    sync: function () {
        for (var key in this._cache) {
            if (this._cache.hasOwnProperty(key)) {
                localStorage[key] = this._cache[key];
            }
        }
    }
}

La API de localStorage asíncrona con IndexedDB

IndexedDB es una API asíncrono para almacenamiento de datos en el cliente, pero puede ser excesiva para datos pequeños y simples. La librería de Gaia, que sirve para hacer asíncrona la API de localStorage a través de IndexedDB, está disponible en Github: async_storage.js.

Miscelánea de micro-optimizaciones

A veces cuando has agotado todas tus opciones y no consigues que vaya más rápido, puedes probar alguna de las siguientes micro-optimizaciones. Sin embargo ten en cuenta que estas solo marcan una diferencia dentro de un uso pesado y cuando cada milisegundo cuenta. Búscalas en tus operaciones cíclicas más críticas

Usa x | 0 en vez de Math.floor

Arrays limpios con .length = 0 para evitar crear un nuevo Array

  • Sacrifica algo de tiempo en la CPU para evitar la creación de basura en memoria.

Utiliza if .. else en vez de switch

Date.now en vez de (+ new Date)

Utiliza TypeArrays para decimales flotantes o enteros (por ejemplo, vectores y matrices)

Conclusiones

Desarrollar para dispositivos móviles y un hardware que no sea tan exigente es un ejercicio bueno y creativo ¡Y esperamos que te quieras asegurar de que tus juegos funcionan bien en todas las plataformas!

The following two tabs change content below.
Colaborando con Mozilla desde el 2002, convencido de las bondades del código abierto, una web libre y universal.

Compartir artículo:

  • ¡Participa!

    Firefox Flicks »
    Crear un vídeo sobre Firefox ahora tiene premio.
    Firefox Affiliates »
    Agrega botones de Firefox en tu sitio web y comparte tu amor por Mozilla Firefox.
    Armada alucinante »
    Ayuda a otros usuarios en Twitter.
    Desafío para desarrolladores »
    Envía tu demo y gana muchos premios.
    Colabora con la comunidad »
    En Mozilla lo importante son las personas. Descubre cómo puedes colaborar.

    Boletín Firefox

    Suscríbete al boletín de novedades de Firefox.

  • Descargas

    Descarga los programas de Mozilla.

    Lo más visto

    cc-by-sa