Desarrollo de videojuegos con HTML5 – Parte 7

Después de un tiempo de descanso, es hora de retomar este paso a paso para realizar nuestros juegos con HTML5 y JavaScript.

En esta oportunidad hablaremos sobre una de las características básicas de todo juego, las colisiones.

Detectar colisiones en un videojuego es una tarea común, no solo para poder saber si dos objetos del juego se tocan, como por ejemplo, una bala disparada por nuestro personaje impactando contra un enemigo, sino que además, la interacción con el puntero del ratón, o los dedos en una pantalla táctil para acceder a un determinado menú del juego también representa una colisión.

Existen muchos métodos para detectar que dos objetos se están tocando, cada uno de estos más eficientes que los otros en determinados casos. Podemos encontrar colisiones en forma de caja (La que usaremos para el ejemplo), por píxeles, verificando que solo píxeles de color de un objeto estéen tocando los píxeles del otro objeto, mediante proyección de líneas (Ray Casting), y muchos otros métodos.

La colisión por cajas, entonces, es una de las más simples y puede resultar efectiva si nuestro juego no cuenta con gran cantidad de elementos para detectar si están en colisión o no, ya que si bien no es dependiente de este tipo de colisión, se suele hacer un escaneo por todos los elementos del juego y verificar, uno a uno, si nuestro objeto están en contacto con otro.

Para entender la mecánica de esta forma de detectar colisiones, pensemos en nuestro objeto de juego al cual le dibujamos una caja que lo contenga. Esta caja poseerá tanto las coordenadas X e Y donde se sitúa, además de las dimensiones de ancho y alto. Luego, teniendo el objeto contra el cual probar la colisión, dibujaremos otra caja con datos similares. Cuando ambas cajas entran en contacto, entonces entendemos que existe una colisión.

Este tipo de colisiones no promete detectar correctamente la misma si las formas son irregulares.

Como vemos en la imagen, si las formas (Gráficos) son irregulares, al crearse una caja para contenerlo, puede quedar mucho espacio sin uso, pero que será tomado como válido para la colisión. Para mejorar este comportamiento, una colisión por píxel sería más adecuada, ya que la misma detectará solo aquellos píxeles de color y no los transparentes en nuestro sprite, por supuesto, realizar el cálculo para dicha detección resulta algo más costosa en cálculos computacionales.

Entonces, para detectar mediante la colisión por cajas necesitamos saber las coordenadas de los dos objetos además de sus dimensiones. Una excelente solución es la que nos propone Matthew Casperson con su clase Rectangle en JavaScript.

function Rectangle()
{
    this.left = 0;
    this.top = 0;
    this.width = 0;
    this.height = 0;

    this.startupRectangle = function(left, top, width, height)
    {
        this.left = left;
        this.top = top;
        this.width = width;
        this.height = height;
        return this;
    }

    this.intersects = function(other)
    {
        if (this.left + this.width < other.left)
            return false;
        if (this.top + this.height < other.top)
            return false;
        if (this.left > other.left + other.width)
            return false;
        if (this.top > other.top + other.height)
            return false;

        return true;
    }
}

La sección más importante está dentro de la función intersects, la cual toma los dos rectángulos y compara los mismos para saber que una parte del primero se encuentre en alguna parte del segundo.

Para este ejemplo, dibujaremos un rectángulo en el trayecto de nuestro personaje. Cuando el mismo toque dicho rectángulo, este dejará de avanzar.

contextBuffer.save();
	contextBuffer.fillStyle = "black";
	contextBuffer.fillRect(300, 10, 30, 40);
contextBuffer.restore();

A continuación del dibujado de nuestro personaje, dibujamos un rectángulo de color negro. Podemos notar en el código que estamos utilizado dos funciones, save y restore. Estas funciones sirven para guardar el estado del lienzo de dibujo en ese momento, poder realizar cualquier modificación, agregar elementos, y luego retornar el lienzo al estado anterior. De esta forma, el cambio de color mediante fillStyle no afectará otras funciones que usen este color para rellenar formas.

El bloque negro se interpone en el camino de nuestro personaje.

El siguiente paso, para detectar la colisión será verificar el estado de los dos objetos tras cada actualización del juego.

var boxBlock = new Rectangle();
boxBlock.startupRectangle(300, 10, 30, 40);
var boxYoda = new Rectangle();

Debido a que sabemos de antemano la posición del rectángulo negro, no es necesario actualizarlo cada vez que el juego se modifica, por lo que escribiremos el código anterior fuera del bucle principal del juego. Finalmente, en el bucle, comprobaremos la posición actual del personaje y su intersección con el bloque.

boxYoda.startupRectangle(x, 10, 42, 39);
if (!boxYoda.intersects(boxBlock)) {
	x++;
}

Solo si el personaje no está en colisión con el bloque, podrá avanzar un píxel.

Como decíamos, este tipo de comprobación suele ser costosa, pero eficiente para darnos una solución rápida para nuestros juegos.

Desarrollo de videojuegos con HTML5 – Parte 6

En la cuarta entrega nos quedó, al final del mismo, un tema pendiente cuando aprendimos a crear objetos animados.

Cuando ejecutábamos el juego pudimos notar que nuestro personaje modificaba sus gráficos de forma muy rápida no pudiéndose apreciar la animación.

Este comportamiento se debe, en principio, a que el juego está disparando más de 30 cuadros de redibujado por segundo y tras cada cuadro nuestro código cambia el gráfico a mostrar, por lo que en vez de tener una animación algo más apreciable, simplemente se ven algunos píxeles que se cambian de lugar, lo que resulta en una animación pobre, incluso para dos cuadros de animación.

Por lo tanto tendremos que encontrar una forma para disparar el cambio de cuadro en nuestro personaje cada cierta cantidad de cuadros dibujados en pantalla.

Una solución rápida podría ser contar cuantos cuadros se han dibujado y luego cambiar al siguiente gráfico de nuestro personaje. Si bien es una alternativa, esto puede causar que una variación en la cantidad de cuadros posibles a ser dibujados en pantalla haga que nuestra animación se dispare más veces por segundo, o menos, dependiendo de cuál sea esta variación, lo que nos daría una sensación de que el juego a veces se acelera y a veces se hace más lento.

Otra posibilidad, y es la que veremos aquí, es la de contar tiempo entre cuadros. Esto es, contabilizar cuanto tiempo ha pasado entre un cuadro y el otro, y luego de llegar a determinada suma, realizar el cambio. Esto nos garantizará que el cambio sea constante en el tiempo, incluso si contamos con problemas en la cantidad de cuadros dibujados por segundo. Por supuesto, esta técnica no puede ser considerada como infalible, ya que a baja performance del equipo que ejecute el juego, también veremos ciertos problemas, pero salvaremos esto en equipos con mayor capacidad.

Para esto necesitaremos algunas variables para calcular el tiempo actual y la diferencia en relación al cuadro actual.

var lastFrame = 0;
var counter = 0;
function dibujar() {
var thisFrame = new Date().getTime();
var delta = (thisFrame - lastFrame) / 1000;
lastFrame = thisFrame;
...

Cada vez que un nuevo cuadro es dibujado se calcula el tiempo en milisegundos (Podría ser cualquier otra unidad) en el cual se dibujó el cuadro anterior y el tiempo actual. Esto nos dará un valor delta (Diferencia en el cambio entre el valor anterior y el actual) con el que podremos realizar nuestros cálculos.

counter += delta;
if (counter > 0.4) {
walkFrame = (walkFrame + 1) % 2;
counter = 0;
}

Sumando este valor delta a un contador iremos acumulando las diferencias. Cuando la misma llegue a un valor determinado, entonces será el momento de realizar el cambio del gráfico a mostrar, logrando una animación más agradable.

En Frameworks avanzados este valor delta suele ir encapsulado con otras propiedades, pero siempre podremos encontrarlo ya que es vital no solo para este tipo de cálculos, sino que resulta también de utilidad en cálculos de trayectorias, proyectiles, colisiones y otros.

 

Desarrollo de videojuegos con HTML5 – Parte 5

En la tercer entrega de estos post, cuando aprendimos a mover elementos por la superficie del juego, comentábamos sobre técnicas que nos ayudarían a poder dibujar una mayor cantidad de objetos en pantalla y de forma más rápida.

Si podemos colocar más gráficos en pantalla y de forma más rápida, podremos también, utilizar el tiempo ganado para realizar cálculos adicionales que enriquezcan nuestro video juego.

Esta técnica la habíamos denominado de buffer doble (Double Buffer), técnica bastante simple, pero eficiente.

La técnica de doble buffer consiste, en pocas palabras, en tener una copia de la memoria de video, eso que vamos a mostrar en pantalla, pero en memoria, realizar todos los cambios en esta (Mover gráficos, realizar animaciones, etc.) y luego, con todo esto resuelto, volcar de una sola vez este modelo de memoria a la pantalla.

Tengamos en cuenta que el dibujar un solo pixel en pantalla es una tarea costosa a nivel de procesos de datos. Ya que no es simplemente determinar el punto o coordenada donde queramos depositar este píxel, si no que el mismo debe viajar desde la computadora hasta la pantalla para transformarse en un punto de luz que solo el monitor puede representar. Por lo tanto, para lograr esta tarea, es necesario que la información sea sincronizada, manipulada y procesada por diferentes dispositivos, desde el CPU pasando por la tarjeta de video y hasta el monitor.

Si nuestro código simplemente dibuja cada elemento directamente en el canvas, esto hace que cada una de las acciones de dibujado sean enviadas tras terminar con ellas, por lo que todo el trabajo de sincronización y cómputos será necesario tras cada uno de los envíos.

Imaginemos que tenemos 10 personajes en nuestro juego, aunque intentemos crear una cantidad de cuadros por segundo, dentro de cada cuadro estaríamos enviando 10 veces (1 por cada personaje) información hasta el monitor, por lo que todo ese tiempo de procesado nos jugaría en contra a la hora de conseguir más cuadros por segundo, o de necesitar dibujar más elementos o simplemente detectar colisiones o cualquier otro tipo de cálculo.

Por lo tanto, si dibujamos estos 10 elementos en memoria, y luego volcamos todos esos píxeles en una sola pasada al monitor, solo tendríamos un proceso de sincronización y demás acciones.

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var canvasBuffer = document.createElement("canvas");
canvasBuffer.width = canvas.width;
canvasBuffer.height = canvas.height;
var contextBuffer = canvasBuffer.getContext("2d");

Lo primero, entonces, será crear nuestro buffer en memoria. Como vemos en el código anterior, simplemente se crea un elemento canvas en memoria, se le asigna el mismo ancho y alto que el objeto canvas original y finalmente se toma un contexto de dibujo. El siguiente paso, por lo tanto, es dibujar sobre este contexto.

function dibujar() {
walkFrame = (walkFrame + 1) % 2;
contextBuffer.fillRect(0, 0, canvasBuffer.width, canvasBuffer.height);
contextBuffer.drawImage(yoda,
walkFrame * 42, 1, 42, 39,
x, 10, 42, 39);
x++;
context.drawImage(canvasBuffer, 0, 0);
}

Aquí vemos que todo el dibujado es realizado sobre contextBuffer, para luego tomar el objeto canvas en memoria y utilizarlo como si fuese una imagen completa, copiando todos sus píxeles al canvas destino.

Existen muchas otras técnicas para optimizar el rendimiento en video juegos, esta, posiblemente, una de las más antiguas que aun hoy se utiliza.

Desarrollo de videojuegos con HTML5 – Parte 4

Esta es la cuarta entrega de post para aprender a desarrollar videojuegos con HTML5 y JavaScript. Los anteriores puedes verlos aquí, aquí y aquí.

En esta entrega crearemos animaciones para los objetos del juego. Siendo que, salvo algunos ítems o partes de la interfaz del juego suelen ser los únicos estáticos, todos los demás elementos cuentan con por lo menos algún efecto de animación que va más allá del simple hecho de desplazar el objeto por la pantalla.

En los juegos de dos dimensiones, como el que estamos creando, es necesario contar con una serie de gráficos que simulen la animación cuadro a cuadro. Si buscamos un parámetro de referencia, este es el de los dibujos animados. En el caso de los dibujos animados, cada cuadro de película requiere se dibujado, y cada movimiento de un protagonista necesita de cada uno de los dibujos que le dan vida.

Cada cuadro del caminar es dibujado

De la misma forma, para obtener una animación en un video juego es necesario dibujar cada uno de estos cuadros en cada uno de los objetos. Luego, se usarán estos cuadros para que, en cada frame de animación del juego, se represente uno de los gráficos que forman parte de la animación del personaje.

Mediante el uso de alguna herramienta de diseño gráfico, entonces, podemos crear esta serie de gráficos para animar a nuestro personaje.

Yoda solo moverá los pies ligeramente para dar la sensación de animación

Como vemos en la imagen, se traza una cuadrícula que dividirá cada cuadro de la animación. Esta cuadrícula, los píxeles de separación entre imagen e imagen, son importantes debido a que de esta forma será más simple, a nivel de programación, poder obtener cada uno de los cuadros de esta tira de gráficos, también llamada Sprite Sheet.

Si tenemos en cuenta que cada cuadro de nuestro personaje posee un ancho de 42 píxeles, la cuadrícula será dividida por este tamaño dejando un espacio de 42 píxeles para cada animación.

Con los gráficos en su lugar solo nos queda poder intercambiar, entre cada cuadro de dibujado, el sprite que corresponda de la Sprite Sheet y así obtener el efecto de caminar deseado.

yoda.src = "yodaspritesheet.png";
var x = 0;
var walkFrame = 0;

function dibujar() {
walkFrame = (walkFrame + 1) % 2;
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(yoda,
walkFrame * 42, 1, 42, 39,
x, 10, 42, 39);
  x++;
}

Hemos cambiado algunas líneas desde el ejemplo anterior. La más notable es la referente al dibujado de la imagen en el canvas. La función drawImage, ahora posee nuevos parámetros los que nos ayudan a poder elegir la sección origen de nuestra Sprite Sheet y el destino en el canvas. La nueva definición de drawImage es la siguiente:

drawImage(Imagen, X origen, Y origen, ancho origen, alto origen, X destino, Y destino, ancho destino, alto destino)

Esto quiere decir que podemos especificar un rectángulo en la imagen origen, tomar dicho rectángulo y dibujar solo esa parte en el destino, esto es, nuestro canvas. En el caso del ancho y alto del destino, si variamos estos valores, la imagen cambiará su tamaño de forma automática. Esto resulta una buena forma de escalar gráficos sin tener que realizar cálculos matemáticos complejos.

La segunda línea llamativa es la que especifica el cuadro a dibujar:

walkFrame = (walkFrame + 1) % 2;

Esta es una forma simple de recorrer todos los cuadros de la animación. Teniendo el cuadro actual, podemos obtener el siguiente valor de cuadro a mostrar o volver el contador a cero. La formula es como sigue:

[FRAME ACTUAL] = ([FRAME ACTUAL] + [SIGUIENTE FRAME]) % [CANTIDAD DE FRAMES EN LA ANIMACION]

Si ejecutamos el código veremos que la animación se realiza de forma muy rápida. Esto se debe a que contamos con pocos cuadros de animación por la cantidad de cuadros por segundo con los cuales se refresca toda la pantalla.

En la próxima entrega hablaremos del cómo sincronizar estos casos para que, teniendo menos cuadros de animación podamos tener una animación fluida.

Desarrollo de videojuegos con HTML5 – Parte 2

En la anterior entrega creamos un lienzo de dibujo mediante el tag canvas. Este tag representaba un conjunto de píxeles y contaba con funciones para dibujar elementos sobre este. Finalmente escribimos algo de texto para probar su funcionamiento.

Pero, en general, los videojuegos se caracterizan por mostrar imágenes para representar cada uno de los elementos del juego y reservamos el texto para mensajes con los que el usuario podrá conocer más sobre los acontecimientos e historia del juego. Entonces, es momento de que dibujemos nuestra primer imagen sobre el lienzo.

Dibujar imágenes es una tarea relativamente sencilla, primero, debemos contar con la imagen en cuestión, para luego colocarla en un eje de coordenadas específico. Y para hacer esto existen diferentes técnicas, por ejemplo, podríamos utilizar una imagen declarada en el DOM mediante el tag <src..>, luego leer dicha imagen desde nuestro código y dibujarla sobre el lienzo. Por otro lado, para este tipo de desarrollos es preferible, para contar con algo más de control y reducir el código, realizar las mismas acciones, todas, desde las líneas de código.

<script>
var yoda = new Image();
yoda.src = "yoda.jpg";
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.drawImage(yoda, 10, 10);
</script>

La función drawImage nos permite tomar una imagen y dibujarla sobre el lienzo en unas coordenadas específicas. En nuestro ejemplo, la imagen se dibujará a 10 píxeles en el eje X y 10 píxeles en el eje Y.

Curiosamente, si ejecutamos el código veremos que no se muestra ninguna imagen.

El código se ejecuta más rápido que la carga de la imagen.

Esto se debe a que la carga de la imagen en memoria se realiza después de que el código de dibujado se ejecuta, por lo tanto, el objeto imagen aun se encuentra vacio.

Para esto deberemos realizar algunas modificaciones al código para asegurarnos que solo se dibuje la imagen luego de que nos hayamos asegurado de que se haya cargado correctamente.

var yoda = new Image();
yoda.onload = function () {
context.drawImage(yoda, 10, 10);
};
yoda.src = "yoda.png";

El evento onload de la imagen nos avisará cuando el objeto haya terminado con la carga. Es en este momento cuando deberemos realizar el dibujado.

Una vez cargada la imagen, se muestra en el canvas

Es importante que tengamos en cuenta el formato de nuestras imágenes. Podemos usar cualquiera de los tres formatos más populares de Internet (Gif, Jpg y Png). Cada uno tiene sus ventajas y desventajas. El formato Png, para nuestro caso, es el que posee un balance entre pros y contras. Pudiendo soportar gran cantidad de colores, una buena compresión sin perder calidad y lo más importante, transparencias. Nos ocuparemos de esto en la siguiente entrega.

Desarrollo de videojuegos con HTML5 – Parte 1

HTML5

Hace unos meses atrás realizábamos un Code&Beer donde hablábamos del cómo desarrollar videojuegos con HTML5 y JavaScript, aunque no detallábamos, paso a paso, como crear un videojuego.

En esta serie de post, por lo tanto, iremos adentrándonos en el mundo del desarrollo de videojuegos, hasta tener un juego completamente funcional usando, para esto, HTML5 y JavaScript. Continue reading

Kinetica Code&Beer #4: Juegos con HTML5

Siguiendo con los eventos de Code&Beer en las oficinas, ayer se habló sobre desarrollo de video juegos con HTML5 y JavaScript.

Enfoque

Iniciamos con una introducción al concepto de desarrollo de video juegos, donde hablamos de los siguientes puntos:

  • Tag <canvas> como lienzo de dibujo en HTML5
  • Bucle principal de un juego
  • Sprites y Sprite Sheets
  • Double buffering para optimización de performance

Los puntos anteriores sirvieron como puntapié de las actividades mínimas necesarias que involucra la creación de un video juego.

El objetivo

Mediante la utilización de un micro framework existente y algunos sprites, nos pusimos en la tarea de crear un juego simple, con una base común, pero aplicando cada uno de los asistentes sus propias variantes.

 

 

El micro framework

Este micro framework, con solo 88 líneas de JavaScript, fue el encargado de manejar la lógica principal del juego. Proveer el lienzo de dibujo a los distintos objetos del juego, manejar el bucle de dibujo e interactuar con el mecanismo de double buffering.

Un poco de la estructura del framework a continuación:

this.init = function (canvasName) {
canvas = document.getElementById(canvasName);
context = canvas.getContext(“2d”);

setInterval(runGame, 1000 / 30);
}

function runGame() {
    …
    backBuffer.fillStyle = “rgb(255,255,255)”;
    backBuffer.fillRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < gameObjects.length; i++) {
        gameObjects[i].update(delta);
        if (gameObjects[i].draw && gameObjects[i].visible()) {
            …
        }
    }
    context.drawImage(backBufferCanvas, 0, 0);
};

Los objetos del juego

Cada entidad en el juego fue representada por un objeto que manejaría el micro framework. Para respetar la estructura y funcionamiento, cada objeto necesitaría 4 funciones (Init, Visible, Update y Draw).

function Cavernicola() {
var self = this;
var cavernicola = new Image();
cavernicola.src = “prota.png”;


this.init = function() {
//Inicializamos el objeto

}

this.visible = function() {
return true;
}

this.update = function(delta) {
//Lógica de actualización del objeto

}

this.draw = function(context) {
//Se dibuja el objeto en el lienzo de dibujo

}
}

Pueden descargar el código desde esta dirección.