Cómo crear un juego HTML5 usando Haxe

HTML5 HTML5 gaming rocket wallpaper por Christian Heilmann. CC BY. Imagen con calidad y tamaño reducidos.

En este tutorial explicaremos cómo crear un juego HTML5 usando el lenguaje de programación Haxe. Haxe es un toolkit que permite desarrollar aplicaciones web, aplicaciones de escritorio y videojuegos que pueden ser exportados a las diferentes plataformas que existen hoy en día.

Con Haxe es posible programar un juego y exportarlo a Windows, OS X, Linux, iPhone, Android, HTML5 y otras plataformas, con la misma base de código. De igual manera, es posible convertir el código Haxe en código Python, JavaScript, C# o C++ gracias a sus potente compilador.

Instalando Haxe

Para instalar Haxe en nuestra computadora, necesitamos ir a la página de descargas de Haxe y obtener el instalador de la versión más reciente para nuestro sistema operativo:

Página de descargas de _Haxe_

Al ejecutarlo, el instalador copiará los archivos y ejecutables de Haxe en el directorio /usr en sistemas Linux o /usr/local en sistemas OS X. Al instalar Haxe, tenemos acceso a dos ejecutables: un compilador llamado haxe y un administrador de librerías llamado haxelib.

También tenemos a nuestra disposición, un compilador y maquina virtual llamada neko con la que podemos probar nuestros proyectos antes de exportarlos a HTML5 u otra plataforma.

A continuación debemos configurar la utilidad haxelib que nos va a permitir instalar los paquetes de Haxe necesarios para este tutorial. haxelib es similar en su funcionamiento a utilidades como pip en Python o NPM en Node.js. Para esto, ejecutamos el siguiente comando en una terminal:

$ haxelib setup

Este comando nos preguntará por la ubicación en donde queremos que se almacenen los paquetes de Haxe que instalemos en nuestra computadora. La ubicación por defecto es /usr/lib/haxe/lib. En sistemas OS X, la ubicación por defecto no es accesible, así que debe ser cambiada.

El juego que vamos a desarrollar en este tutorial es el famoso Tic Tac Toe o Tres en Línea. Para hacerlo realidad, vamos a necesitar de una librería para desarrollar juegos llamada HaxePunk.

Instalando HaxePunk

HaxePunk es una librería Haxe para desarrollo de juegos cuyo diseño esta basado en la famosa librería para ActionScript3 llamada FlashPunk. HaxePunk incorpora manejo de colisiones, sistema de partículas, atlas de texturas, reproducción de audio y control por teclado, mouse y pantallas táctiles.

Con HaxePunk es posible desarrollar juegos para casi todas las plataformas existentes, desde consolas hasta teléfonos móviles. Para instalar HaxePunk, abrimos una terminal y ejecutamos los siguientes comandos:

$ haxelib install HaxePunk
$ haxelib run HaxePunk setup

Estos comandos instalarán las librerías OpenFL, Lime y HXCPP, las cuales son dependencias de HaxePunk. También nos preguntará si queremos instalar la utilidad lime, a lo que responderemos afirmativamente.

Creando el proyecto

El código fuente explicado en este artículo se puede descargar desde Github o Bitbucket.

Ahora que tenemos todas las herramientas listas, vamos a comenzar el desarrollo del juego. En el juego de Tres en Línea se enfrentan dos jugadores en una partida, usando un tablero de juego con seis casillas ordenadas en una matriz de 3x3. Para ganar, cada jugador debe hacer una línea de círculos o de cruces según sea el símbolo que escogío cada jugador.

Para comenzar, vamos a crear un nuevo proyecto HaxePunk. Para esto, ejecutamos el siguiente comando en una terminal:

$ haxelib run HaxePunk new tictactoe_haxepunk

El comando va a generar un directorio con los siguientes archivos:

tictactoe_haxepunk/
  assets/
    audio/
      README.md
    font/
      README.md
    graphics/
      README.md
    HaxePunk.svg
  project.xml
  src/
    MainScene.hx
    Main.hx

En el directorio assets/ se almacenan los archivos de audio, las imágenes y las fuentes que vamos a usar en el juego. El archivo project.xml es un archivo de configuración de HaxePunk, que no vamos a modificar en este tutorial. Finalmente, dentro del directorio src/ se coloca el código fuente de nuestro juego.

Para comprobar que todo esta funcionando correctamente, ejecutamos el siguiente comando:

$ lime test neko -debug

Debemos ver algo como esto:

Probando HaxePunk modo debug

Nuestro juego va a tener tres escenas principales: el inicio, el tablero de juego y el final.

Creando la escena de inicio

La escena de inicio va a mostrarle al jugador el titulo y una breve descripción de nuestro juego. Un juego en HaxePunk se compone de varias escenas y cada escena se compone de una o varias entidades que interactúan entre si.

Para implementar la escena de inicio, vamos a crear el archivo StartScene.hx dentro del directorio src/. En este archivo vamos a crear la escena StartScene de la siguiente forma:

import com.haxepunk.Entity;
import com.haxepunk.HXP;
import com.haxepunk.Scene;
import com.haxepunk.graphics.Text;
import com.haxepunk.utils.Input;
import com.haxepunk.utils.Key;
import flash.text.TextFormatAlign;

import BoardScene;

class StartScene extends Scene
{
    private var title:Entity;
    private var description:Entity;

    public function new()
    {
        super();
    }

    public override function begin()
    {
        var textOptions = {size: 50, align: TextFormatAlign.CENTER};
        var text = new Text("Tic Tac Toe", 0, 0, 640, 0, textOptions);
        title = addGraphic(text, 0, 0, 50);

        var desc_text = "Circle goes first, then is cross' turn.\n\n" +
            "Make three circles or crosses in a row for you to win.\n\n\n" +
            "Press SPACE to continue...";
        var textOptions = {size: 24, wordWrap: true};
        var text = new Text(desc_text, 0, 0, 600, 0, textOptions);
        description = addGraphic(text, 0, 20, 150);
    }
}

Como podemos ver, la clase StartScene hereda la funcionalidad de la clase Scene de HaxePunk. En el constructor new() llamamos a la función super() para indicarle a Haxe que incluya el código presente en el constructor de la clase padre.

La escena contiene dos entidades title y description, que se van a encargar de mostrar texto solamente. La entidad title va a mostrar el título del juego y la entidad description va a mostrar la descripción.

La función begin() se usa para inicializar las entidades que vamos a utilizar en la escena. Para crear el texto, usamos la clase Text de HaxePunk, cuyo constructor acepta seis parámetros:

  • El texto que se va a mostrar
  • La posición en la pantalla sobre el eje X
  • La posición en la pantalla sobre el eje Y
  • La anchura del texto (si el valor es cero, la anchura se ajustará al tamaño del texto)
  • La altura del texto (si el valor es cero, la altura se ajustará al tamaño del texto)
  • Opciones de formato (tipo de fuente, tamaño, color, entre otras)

El sistema de coordenadas de HaxePunk asume que la posición de origen (x=0, y=0), corresponde a la esquina superior izquierda de la pantalla.

Por último, la función addGraphic() agrega las entidades de texto a una estructura de datos que HaxePunk va a escanear, más o menos, de 30 a 60 veces por segundo para dibujarlas en pantalla. Esto significa que nuestro juego va a tener una tasa de refresco, o FPS, de entre 30 a 60 cuadros por segundo.

Las posiciones sobre el eje X aumentan de izquierda a derecha, mientras que las posiciones sobre el eje Y aumentan de arriba hacia abajo.

Ahora debemos modificar el archivo Main.hx para decirle a HaxePunk que la escena StartScene es la primera que se va a ejecutar:

import com.haxepunk.Engine;
import com.haxepunk.HXP;

import StartScene;

class Main extends Engine
{

    override public function init()
    {
#if debug
        HXP.console.enable();
#end
        HXP.scene = new StartScene();
    }

    public static function main() { new Main(); }

}

Para probar la escena de inicio ejecutamos el siguiente comando una vez más:

$ lime test neko -debug

Debemos ver algo cómo esto:

Juego HaxePunk inicio

Creando el tablero de juego

El tablero de juego se va a componer de once entidades:

  • Nueve entidades de texto para las casillas donde pondremos un circulo o una cruz.
  • Una entidad para mostrar el texto que indica en que turno estamos.
  • Una entidad que mostrará una imagen de fondo con las lineas que visualmente separan las casillas.

La imagen de fondo se puede descargar aquí y debe copiarse al directorio assets/graphics/ del proyecto. Para implementar el tablero de juego, vamos a crear el archivo BoardScene.hx dentro de src/:

import com.haxepunk.Entity;
import com.haxepunk.Scene;
import com.haxepunk.graphics.Stamp;
import com.haxepunk.graphics.Text;
import com.haxepunk.utils.Input;
import flash.text.TextFormatAlign;

class BoardScene extends Scene
{
    public static inline var BOARD_SIZE:Int = 3;

    private var turnLabel:Entity;
    private var slots:Array<Array<Entity>>;
    private var background:Entity;
    private var turn:String;
    private var hasWon:Bool;
    private var slotsFilled:Int;

    public function new()
    {
        super();
    }

    public override function begin()
    {
        var textOptions = {size: 50, align: TextFormatAlign.CENTER};
        var text = new Text("", 0, 0, 640, 0, textOptions);
        turnLabel = addGraphic(text, 0, 0, 50);

        var xOffset = 170;
        var yOffset = 120;
        var width = 100;
        var height = 100;

        slots = new Array<Array<Entity>>();
        for (i in 0...3)
        {
            var row = new Array<Entity>();
            for (j in 0...3)
            {
                var x = xOffset + (j * width);
                var y = yOffset + (i * height);

                var textOptions = {size: 50, align: TextFormatAlign.CENTER};
                var text = new Text("", 0, 25, width, 0, textOptions);

                var entity = addGraphic(text, 0, x, y);
                entity.setHitbox(100, 100);
                row.push(entity);
            }
            slots.push(row);
        }

        background = addGraphic(new Stamp("graphics/background.png"), 1);

        turn = "O";
        hasWon = false;
        slotsFilled = 0;
    }
}

En esta clase estamos definiendo dos entidades: turnLabel para mostrar un texto indicando el turno en el que estamos y background que mostrara las líneas divisorias.

También estamos definiendo una variable turn que almacenara un círculo o una cruz y va a servir para llenar las casillas, una variable hasWon que nos va a indicar si hay un ganador o no y una variable slotsFilled que nos va a indicar el numero de casillas que ya están llenas.

Por último, estamos definiendo al inicio de la clase, la constante BOARD_SIZE que va a almacenar el numero de casillas que el tablero de juego tiene por cada lado.

Para almacenar las casillas vamos a usar una matriz bi-dimensional de entidades definida por la variable slots. Una matriz bi-dimensional es simplemente una lista de listas de entidades.

En la función begin() estamos construyendo la matriz usando dos instrucciones for() anidadas. En el for() interno agregamos cada una de las casillas de una determinada fila. En el for() externo estamos agregando cada fila resultante a la matriz slots.

Como se puede apreciar, para cada casilla se está definiendo un hitbox mediante la función setHitbox(). El hitbox es simplemente un rectángulo que define los limites de cada entidad y se utiliza para calcular las colisiones. En este caso, el hitbox es de 100x100 pixeles. Aparte de lo comentado, el resto del código de esta escena es similar al de la escena de inicio.

Ahora necesitamos agregar la función update() a la clase StartScene en el archivo StartScene.hx para poder mostrar el tablero de juego cuando el jugador presiona la tecla ESPACIO:

class StartScene extends Scene
{
    public override function update()
    {
        if (Input.check(Key.SPACE)) {
            HXP.scene = new BoardScene();
        }
    }
}

El único requisito necesario para mostrar la nueva escena es re-asignar la escena que queremos mostrar a la variable HXP.scene. Es lo mismo que hicimos en el archivo Main.hx para mostrar la escena de inicio. En este caso, la escena solo es re-asignada cuando la tecla ESPACIO es presionada.

Todo esto sucede en la función update(), la cual se ejecuta cada vez que se refresca la pantalla, es decir, de 30 a 60 veces por segundo. Por esta razón, es aquí donde debemos incluir el código que tenga que ver con el manejo de los dispositivos que usamos para controlar el juego (teclado, ratón, controles de juego).

Para probar el tablero de juego ejecutamos el siguiente comando una vez más:

$ lime test neko -debug

Cuando presionemos la tecla ESPACIO, estando en la escena de inicio, debemos ver algo como esto:

Juego HaxePunk tablero de juego

Ahora queremos que cuando se haga clic en cada una de las casillas del tablero de juego, aparezca un circulo o una cruz dependiendo del turno en el que estemos. Para hacer esto posible, vamos a incluir la función update() en la clase BoardScene en el archivo BoardScene.hx:

class BoardScene extends Scene
{
    public override function update()
    {
        var wasTurnChanged = false;

        if (Input.mousePressed && !hasWon)
        {
            var mouseX = Input.mouseX;
            var mouseY = Input.mouseY;

            for (i in 0...BOARD_SIZE)
            {
                for (j in 0...BOARD_SIZE)
                {
                    var entity = slots[i][j];
                    var text = cast(entity.graphic, Text);

                    var hasCollided = entity.collidePoint(
                        entity.x, entity.y, mouseX, mouseY);

                    if (hasCollided && text.text == "")
                    {
                        text.text = turn;
                        wasTurnChanged = true;
                        slotsFilled += 1;
                    }
                }
            }
        }

        if (wasTurnChanged)
        {
            if (turn == "O") turn = "X" else turn = "O";
        }

        var text = cast(turnLabel.graphic, Text);
        text.text = "It's \"" + turn + "\" turn";
    }
}

El primer condicional if () dentro de la función update() esta constantemente revisando si el botón izquierdo del ratón ha sido presionado.

Si es así y si la partida aún no tiene un vencedor, obtenemos las coordenadas del punto en la pantalla donde se hizo clic. Mediante el uso de bucles for() anidados y de la función collidePoint(), se revisan cada una de las casillas para determinar sobre cual de ellas se hizo clic.

La función collidePoint() acepta cuatro parámetros, que en este caso son:

  • La posición X de la entidad.
  • La posición Y de la entidad.
  • La posición X del cursor al hacer clic.
  • La posición Y del cursor al hacer clic.

El resultado de la función se guardará en la variable hasCollided, y será verdadero si el punto hace intersección con el hitbox de la entidad o falso en caso contrario.

En el condicional if() dentro del segundo bucle for() se cambia el texto de la casilla por el valor que tenga la variable turn en ese momento, el cual puede ser un círculo o una cruz.

Esto solo va a ocurrir para la casilla sobre la que se hizo clic y si esta casilla no contiene caracteres, lo que quiere decir que una casilla llena no puede ser modificada.

Al volver a probar el tablero de juego, podemos verificar que el contenido de la casilla cambia al hacer clic en ella.

Por último, vamos a hacer que el juego nos indique si la partida ha tenido un vencedor o si ha terminado en un empate. Para esto, modificamos por completo el código de la función update() que acabamos de agregar:

class BoardScene extends Scene
{
    public override function update()
    {
        var wasTurnChanged = false;

        if (Input.mousePressed && !hasWon)
        {
            var mouseX = Input.mouseX;
            var mouseY = Input.mouseY;

            for (i in 0...BOARD_SIZE)
            {
                for (j in 0...BOARD_SIZE)
                {
                    var entity = slots[i][j];
                    var text = cast(entity.graphic, Text);

                    var hasCollided = entity.collidePoint(
                        entity.x, entity.y, mouseX, mouseY);

                    if (hasCollided && text.text == "")
                    {
                        text.text = turn;
                        wasTurnChanged = true;
                        slotsFilled += 1;
                    }
                }
            }

        }

        for (i in 0...BOARD_SIZE)
        {
            var rLine = 0;
            var cLine = 0;
            var sLine = 0;
            var bsLine = 0;

            for (j in 0...BOARD_SIZE)
            {
                var rText = cast(slots[i][j].graphic, Text);
                var cText = cast(slots[j][i].graphic, Text);
                var bsText = cast(slots[j][j].graphic, Text);
                var si = (BOARD_SIZE - 1) - j;
                var sText = cast(slots[si][j].graphic, Text);

                if (rText.text == turn) rLine += 1;
                if (cText.text == turn) cLine += 1;
                if (bsText.text == turn) bsLine += 1;
                if (sText.text == turn) sLine += 1;

                if (rLine == BOARD_SIZE || cLine == BOARD_SIZE ||
                    sLine == BOARD_SIZE || bsLine == BOARD_SIZE)
                {
                    hasWon = true;
                }
            }
        }

        if (wasTurnChanged && !hasWon)
        {
            if (turn == "O") turn = "X" else turn = "O";
        }

        var text = cast(turnLabel.graphic, Text);
        text.text = "It's \"" + turn + "\" turn";

        if (hasWon) text.text = "\"" + turn + "\" won!";
        if (slotsFilled == Math.pow(BOARD_SIZE, 2)) text.text = "It's a draw!";
    }
}

Aquí volvemos a utilizar bucles for() anidados para recorrer todas las casillas del tablero de juego. A medida que el programa va recorriendo cada casilla, las variables rLine, cLine, bsLine y sLine llevan la cuenta de cuantos círculos o cruces hay en línea.

Si alguna de estas variables es igual al valor de BOARD_SIZE, quiere decir que hay un ganador. Al final imprimimos el mensaje de ganador o de empate.

Exportando el juego como HTML5

¡Felicidades! Ya hemos construido un juego muy sencillo usando un lenguaje de programación muy potente y versátil. Sin embargo, aún no sabemos como distribuirlo para que otras personas puedan jugarlo.

Queremos que sea muy fácil para otros tener acceso al juego, y la mejor manera para lograr esto es exportándolo como HTML5. Afortunadamente con Haxe es muy fácil hacerlo. Solo tenemos que ejecutar el siguiente comando:

$ lime build html5

Para ejecutar el juego en un navegador web, solo tenemos que acceder al directorio bin/html5/bin y abrir el archivo index.html.

Felicitaciones! De esta forma es cómo se crean juegos de video utilizando el lenguage de programación Haxe. Recuerden que el código fuente explicado en este artículo puede ser descargado desde Github o Bitbucket.