Cómo crear un juego HTML5 usando Haxe
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:
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:
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:
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:
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.