Aprende a crear juegos en HTML5 Canvas

lunes, 27 de febrero de 2012

Parte 7. Optimización para Javascript

Aunque el presente curso ha sido enseñado en el lenguaje Javascript, sus conocimientos son globales, por lo que adaptar lo aquí aprendido a un nuevo lenguaje, sería relativamente sencillo.

Sin embargo, si deseas proseguir tu camino por el camino del desarrollo de juegos para la web, es recomendable seguir las recomendaciones siguientes, para optimizar tus juegos en este lenguaje:

Encapsular el código en una función auto-ejecutable


Una de las características peculiares en Javascript, es que todos los scripts son globales y pueden interactuar entre si. A veces eso puede ser una ventaja, pero en consecuencia, puede llevar a efectos no deseados, como la redefinición de una variable con el mismo nombre.

Por ejemplo, si tu código usa una variable "x" para almacenar la posición de un objeto en tu juego, pero otro script usa otra variable con el mismo nombre "x" para una acción diferente, la variable eventualmente tendrá un valor distinto al planeado, y esto creará conflictos para uno de los dos códigos, si no ambos.

Para evitar esto, es importante encapsular los códigos de Javascript en una función auto-ejecutable, poniendo todo su contenido dentro de un código como el del ejemplo a continuación:
(function (window, undefined) {
    //...
}(window));
De esta forma, todas las variables y objetos dentro de nuestro código, serán exclusivamente locales, y no entrarán en conflicto con cualquier otra variable que quede de forma global. Nuestro código aun podrá usar variables globales si es necesario, pero los scripts externos no podrán modificar las variables dentro de nuestro código.

Notarás que la función recibe las variables "window" y "undefined", y envía al momento de construirse la variable "window". Aunque esto no paresca tener mucho sentido, es una medida de seguridad de Javascript que previene que funciones dentro de "window" sean reemplazadas posteriormente durante su ejecución, y asegura que "undefined" sea realmente un valor indefinido y no sea reemplazado por otros scripts.

Usar el modo estricto


Javascript ha evolucionado con el tiempo desde un lenguaje muy básico y permisible para la web, pero aun cuando va mejorando con el tiempo, es necesario que sea compatible con prácticas antiguas que han caído en desuso, y en ocasiones, puede ser incluso contraproducente para el desarrollo en aplicaciones modernas.

Los navegadores no tienen forma de saber si nosotros estamos usando un código con estas antiguas prácticas o no, pero nosotros podemos indicarle que estamos creando un código actual que no depende de estas, agregando al comienzo la siguiente línea:
    'use strict';
Al usar la versión estricta de Javascript, el navegador desactivará el funcionamiento de todas estas malas prácticas que se hacían antiguamente. Si por error, llegáramos a incluir una, esta será reportada en la Consola de Javascript como un error.

Recuerda que si tu código solo muestra una pantalla gris, debes verificar la Consola de Javascript, tal como aprendimos desde la lección 1.

Verificar si la imagen está cargada


A veces las imágenes grandes pueden tardar en cargar un momento, o hay ocasiones en las que por un error, la imagen simplemente no carga en lo absoluto. Sin embargo, aun sin una imagen, el juego será ejecutado, confrontando obstáculos invisibles.

La mejor forma de saber si una imagen ha cargado correctamente en Javascipt, es verificando su ancho. Si la imagen no ha cargado, esta regresará un valor de 0 o nulo.

Si la imagen no ha sido cargada, es una buena idea poner algo en su lugar que indique que hay una imagen sin dibujarse en ese lugar; de esta forma, el usuario sabrá que ahí está alguna especie de obstáculo, aunque no sepa precisamente de que clase de obstáculo se trate.

Para facilitar esta tarea, se puede agregar este método a la función "Rectangle", que dibuja la imagen en la posición indicada, y si la imagen no ha cargado, dibujará el contorno del rectángulo en su lugar:
        this.drawImage = function (ctx, img) {
            if (img == null) {
                window.console.warn('Missing parameters on function drawImage');
            } else {
                if (img.width) {
                    ctx.drawImage(img, this.x, this.y);
                } else {
                    ctx.strokeRect(this.x, this.y, this.width, this.height);
                }
            }
        };
He preferido usar "strokeRect" en lugar de "fillRect", ya que envía con mayor certeza esa sensación de "Aquí va algo que falta". Puedes combinarlo con los "fillStyle" comentados en el código, cambiándolos por "strokeStyle", creando así un juego bastante funcional, aun en el peor de los casos, en que una imagen falle en cargar (O tarde demasiado en hacerlo).

Para dibujar las imágenes en la función "paint" con esta función personalizada, debes cambiarlas del formato:
    ctx.drawImage(iFood, food.x, food.y);
A la siguiente forma:
    food.drawImage(ctx, iFood);

Compatibilidad de audio


Sobre los sonidos, hay que resaltar un problema importante, pues actualmente, los navegadores Internet Explorer y Safari solo admiten formato mp3 y mp4 (m4a/aac), mientras que Firefox y Opera solo admiten formato ogg (oga). Chrome admite ambos formatos, así que no hay problema con él.

Por tanto, al agregar sonidos en nuestro juego, es posible que en algunos navegadores no se escuche (dependiendo el formato seleccionado).

Hay soluciones avanzadas que permiten descubrir las funciones del navegador del usuario y usar un archivo diferente dependiendo las mismas. La función a continuación nos da a conocer si debe ser usado un archivo de audio ogg o de formato diferente:
function canPlayOgg() {
    var aud = new Audio();
    if (aud.canPlayType('audio/ogg').replace(/no/, '')) {
        return true;
    } else {
        return false;
    }
}
Este código se implementa de la siguiente forma al asignar la fuente del audio:
    if (canPlayOgg()) {
        aEat.src="assets/chomp.oga";
    } else {
        aEat.src="assets/chomp.m4a";
}

Usar [] en lugar de "new Array()"


Una forma alterna de crear un nuevo arreglo en Javascript, es simplemente declararlo con un par de corchetes, de la siguiente forma:
    var body = [];
No solo es mas rápido de escribir y recordar, también resulta más veloz en tiempo de ejecución, por lo que se recomienda su uso en lugar de la forma tradicional.

Usar ~~ en lugar de "Math.floor()"


De forma similar que con los corchetes en lugar del nuevo arreglo, esta es una forma más corta y veloz de convertir un número decimal a un número entero. Por ejemplo, la función "random" sería modificada a la forma siguiente:
    function random(max) {
        return ~~(Math.random() * max);
    }
En realidad esta no es una forma alterna de llamar a "Math.floor", si no un truco que da la casualidad de ejecutar la tarea requerida en Javascript, en una forma más eficiente (Y también, más fácil de recordar).

Esta técnica forma parte de un conjunto de técnicas llamada "Operaciones a nivel de bits" (En ingles "Bitwise operation"). Estas operaciones manipulan los bits de un valor, es decir, los números binarios que lo conforman al nivel más básico del lenguaje computadora.

En realidad esto no importa mucho a nuestro conocimiento por ahora, pero diré en forma resumida, que el operador "~", hace una negación de los bits en un valor, es decir, convierte todos los 0s en 1s, y los 1s en 0s. Las operaciones a nivel de bits en Javascript solo pueden usar números enteros, por lo que los decimales son eliminados. Al hacer una doble negación "~~", terminamos con el valor entero de este número decimal, y como dije antes, en un tiempo de ejecución aun menor al "Math.floor()".

Usar !== en lugar de != al comparar


Y de igual forma, "===" en lugar de "==". A diferencia de muchos otros lenguajes, Javascript permite hacer comparaciones entre valores equivalentes que no sean estrictamente idénticos, esto permitió versatilidad durante mucho tiempo para el desarrollo web, pero ha causado conflictos cuando se requiere comparar dos valores a que sean exactamente iguales. Para resolver este conflicto, Javascript agregó dos nuevos comparadores para asegurar la estricta igualdad o diferencia de dos valores, agregando un caracter extra de igualdad al final de la comparación.

Para evitar posibles conflictos en el código, es recomendado utilizar siempre los comparadores estrictos al programar en Javascript. Si deseas conocer más sobre la equivalencia de valores en comparadores no estrictos, puedes ver la siguiente tabla: http://dorey.github.io/JavaScript-Equality-Table/
            if (dir === 0) {
                body[0].y -= 10;
            }

Comparar con "undefined" en lugar de "null" para valores indefinidos


Al comienzo de este artículo se habló sobre una variable de valor indefinido, pero no se detalló al respecto. En la mayoría de los lenguajes, todos los objetos se inicializan con un valor nulo, y las variables suelen tener el mínimo valor predefinido, sin embargo, en Javascript todas las variables son inicializadas literalmente con un valor "indefinido".

Como se había comentado antes, en Javascript no existía una palabra reservada para representar este valor indefinido, pero por estándar se ha utilizado la variable con valor indefinido "undefined". Cuando se realiza una comparación estricta en Javascript, este es el valor que tienen por defecto las variables sin definir, por ejemplo, al no asignarlas cuando se llama una función; por ello es que se debe hacer esta comparación con "undefined" en lugar de utilizar "null", que sería el valor predeterminado en otros lenguajes.
        if (img === undefined) {
            window.console.warn('Missing parameters on function drawImage');
        }

Usar prototipos en las pseudo-clases.


Se aprendió que JavaScript no tiene clases como tal, a diferencia de otros lenguajes, pero se pueden usar funciones que trabajen de forma similar a las clases, agregando en su interior las variables y funciones necesarias para que los objetos creados a partir de estas, funcionen de la forma deseada.

Sin embargo, el método aprendido de incluir las funciones en el interior de la pseudo-clase (Que es la forma estándar en que funcionan la mayoría de los lenguajes), no es el mas óptimo en JavaScript, pues cada nuevo objeto crea una copia completa de todo su contenido en la memoria RAM, incluyendo las funciones que en esencia repiten la misma información sin necesidad, y pueden saturar la memoria en caso de trabajar con miles de objetos, como suele ocurrir en los videojuegos.

JavaScript trabaja realmente en base a prototipos en lugar de clases, y usar esta forma no copia de nuevo la información en la memoria RAM, permitiendo el uso de múltiples objetos con un impacto menor en la misma. Usar prototipos puede ser un poco confuso para quienes ya hayan trabajado con otros lenguajes, ya que agregar funciones a un prototipo es diferente a como se hace con clases.

Una forma de hacerlo es crear cada función directamente en el prototipo de la función principal, tal como se muestra a continuación para el prototipo de la función "Rectangle":
    function Rectangle(x, y, width, height) {
        this.x = (x === undefined) ? 0 : x;
        this.y = (y === undefined) ? 0 : y;
        this.width = (width === undefined) ? 0 : width;
        this.height = (height === undefined) ? this.width : height;
    }
    
    Rectangle.prototype.intersects = function (rect) {
        if (rect === undefined) {
            window.console.warn('Missing parameters on function intersects');
        } else {
            return (this.x < rect.x + rect.width &&
                this.x + this.width > rect.x &&
                this.y < rect.y + rect.height &&
                this.y + this.height > rect.y);
        }
    };

    Rectangle.prototype.fill = function (ctx) {
        if (ctx === undefined) {
            window.console.warn('Missing parameters on function fill');
        } else {
            ctx.fillRect(this.x, this.y, this.width, this.height);
        }
    };

    Rectangle.prototype.drawImage = function (ctx, img) {
        if (img === undefined) {
            window.console.warn('Missing parameters on function drawImage');
        } else {
            if (img.width) {
                ctx.drawImage(img, this.x, this.y);
            } else {
                ctx.strokeRect(this.x, this.y, this.width, this.height);
            }
        }
    };
La otra forma es sobrescribir por completo el prototipo con un nuevo objeto que contenga todas las funciones y propiedades deseadas. Si se usa este método hay que reasignar su constructor, o de lo contrario quedará indefinido al sobrescribir dicho prototipo:
    function Rectangle(x, y, width, height) {
        this.x = (x === undefined) ? 0 : x;
        this.y = (y === undefined) ? 0 : y;
        this.width = (width === undefined) ? 0 : width;
        this.height = (height === undefined) ? this.width : height;
    }
    
    Rectangle.prototype = {
        constructor: Rectangle,
        
        intersects: function (rect) {
            if (rect === undefined) {
                window.console.warn('Missing parameters on function intersects');
            } else {
                return (this.x < rect.x + rect.width &&
                    this.x + this.width > rect.x &&
                    this.y < rect.y + rect.height &&
                    this.y + this.height > rect.y);
            }
        },
        
        fill: function (ctx) {
            if (ctx === undefined) {
                window.console.warn('Missing parameters on function fill');
            } else {
                ctx.fillRect(this.x, this.y, this.width, this.height);
            }
        },
        
        drawImage: function (ctx, img) {
            if (img === undefined) {
                window.console.warn('Missing parameters on function drawImage');
            } else {
                if (img.width) {
                    ctx.drawImage(img, this.x, this.y);
                } else {
                    ctx.strokeRect(this.x, this.y, this.width, this.height);
                }
            }
        }
    };

Seguir los lineamientos de JSLint


JSLint fue creado como una herramienta para código de calidad para Javascript, no solo verifica si el código ha sido escrito correctamente, si no que además muestra una serie de recomendaciones para que el código sea legible y sencillo de comprender, lo cual es de mucha ayuda en proyectos colaborativos, por lo que resulta una buena práctica seguir estos lineamientos desde el comienzo. Herramientas de desarrollo web como Brackets ya lo incluyen por defecto al revisar código JavaScript.

Hay ciertas recomendaciones activadas por defecto que pueden ser personalizadas agregando comentarios especiales al comienzo del código. Para que nuestro código se muestre válido, tendremos que agregar estas dos líneas al comienzo:
/*jslint bitwise:true, es5: true */
La primer condición es activar las operaciones a nivel de bits (bitwise), ya que JSLint previene de su uso dado que muy poca gente comprende como funcionan y pueden escribir líneas de código con un propósito distinto al que imaginaban. Nosotros aprendimos poco arriba para que es el comando a nivel de bits que usamos y su razón, por lo que no necesitamos que nos advierta más de los riesgos de usarlo.

La segunda condición es activar "EcmaScript5" (es5). Usualmente JSLint advierte que redefinir "undefined" es posiblemente un error, y eso es cierto, pero en nuestro caso lo hacemos precisamente para prevenir errores por casos donde se llegue a dar esta situación. Esta práctica se hizo popular opr seguridad a partir de EcmaScript5 (En el que se basa JavaScript 1.5), por tanto, al especificar que esta es la versión que estamos usando, nos dejará de lanzar esta advertencia, suponiendo que le estamos redefiniendo por la razón correcta.

Conclusión


Con estos consejos, tus códigos en Javascript se ejecutarán de forma más segura y veloz en futuros proyectos. Como ejercicio, aplica todo lo aquí aprendido al juego que acabamos de desarrollar sin ver el código final. Úsalo solo para compararlo con tu resultado. Si tienes dudas, puedes consultarme en los comentarios.

¡Mucha suerte! ¡Y felices códigos!


6 comentarios:

  1. Consejos muy útiles la verdad, el modo estricto te lo he visto muchas veces en el código pero no sabía para qué valía. Lo que voy a empezar a usar es lo de la imagen y el audio que los veo bastante importante saberlo, por lo demás ya tenía nociones de ello y es bueno recordarlos.

    Conclusión, una gran ayuda Karl ;) !!

    Pd: Debería hacer el W3C un estándar de audio web.

    ResponderEliminar
    Respuestas
    1. De hecho, la W3C había propuesto como estándar el uso de OGA para audio y OGV para video, pero Apple y Microsoft nunca hicieron caso de este estándar, y al cabo de unos años dijo "¡Ya! ¡Hagan lo que quieran!" Y quitó el estándar de los documentos oficiales.

      Eliminar
    2. Pues es una pena. De Apple sabemos que siempre sigue su propio camino con su propia tecnología pero Microsoft podría haber echo un esfuerzo...quizás en un futuro más lejano...

      Eliminar
  2. Estos tutoriales son geniales, Gracias a los autores

    ResponderEliminar
  3. Estos tutoriales son geniales, Gracias a los autores

    ResponderEliminar
  4. He intentado seguir varios tutoriales, pero el tuyo es estupendo, ojala que sigas incrementando tus tutoriales, profundizando cada vez más, porque los videojuegos que uno puede encontrar en intenet, la verdad que no se sabe ni como le hacen para hacer tan buenos juegos.

    Gracias nuevamente!

    ResponderEliminar