Skip to content

Crear un grid de tiles con “Tiled” y Javascript

by Pere Monfort on March 25th, 2012

Muchos juegos utilizan un sistema de tiles (“azulejos”) para crear los backgrounds o los niveles. Para este tutorial utilizaremos un programa Open Source llamado “Tiled” para realizar los mapas. Una vez realizados exportaremos la información del mapa y crearemos una función en Javascript encargada de pintar el mapa recién creado a nuestro canvas de HTML5.

Requisitos para el tutorial

  • Lógicamente tener Tiled instalado.
  • Descargar éste Tile set para el tutorial (extraído de google images).

 

Parte 1: Crear mapa con Tiled

Lo primero es crear un mapa nuevo desde el menú “Archivo > Nuevo…” de Tiled. En la ventana que nos aparecerá:

  • Mapa: “ortogonal”
  • Tamaño de mapa: 8 patrones x8 patrones
  • Tamaño del patrón: 32 x 32 pixeles

Una vez creado el nuevo mapa, desde el menú “Mapa > Nuevo conjunto de patrones…” seleccionaremos la imagen utilizada para éste ejemplo.

Veremos como nos aparece en la ventana “Conjunto de Patrones” del panel derecho nuestra imagen partida en porciones de 32×32:

Seleccionando la porción deseada pintamos el mapa a nuestro libre albedrío. Podemos crear nuevas Capas de Patrones en el caso de que queramos distintos niveles de profundidad. Es decir, la capa inferior para el suelo, y una segunda capa para los objetos. En mi caso, el resultado ha sido el siguiente:

Una vez construido nuestro mapa, desde el menú “Archivo > Exportar como…” guardaremos el mapa en formato json.

Paso 2: Código Javascript

Podríamos crear una pequeña función que nos cargara vía ajax los archivos JSON, pero para no alargar el tutorial, lo que haremos es copiar el código JSON directamente a una variable. Pero vayamos por partes, lo primero de todo es crear el archivo HTML, lo llamaremos tiles.html:

<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Project-Cyan.com</title>
<script> /* Código JS aquí */ </script>
</head>
<body>
    <canvas id="canvas" width="256" height="256" style="border:1px solid #000;">
        <p>Explorador no compatible</p>
    </canvas>
</body>
</html>

Dentro de la etiqueta “script” del “head”, asignaremos a una variable todo el contenido del archivo JSON exportado:

var oTileMap = {
    "height":8,
    "layers":[{
        "data":[209, 209, 209, 184, 209, 209, 209, 209, 209, 209, 209, 199, 209, 209, 209, 209, 209, 209, 209, 199, 209, 209, 209, 209, 211, 212, 213, 214, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209],
        "height":8,
        "name":"Capa de Patrones 1",
        "opacity":1,
        "type":"tilelayer",
        "visible":true,
        "width":8,
        "x":0,
        "y":0
        },
        {
        "data":[181, 182, 0, 0, 0, 0, 187, 190, 196, 197, 0, 0, 206, 0, 0, 188, 0, 0, 0, 0, 0, 0, 0, 189, 0, 0, 0, 0, 206, 0, 0, 0, 206, 0, 206, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 202, 203, 0, 0, 0, 0, 0, 0, 217, 218, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        "height":8,
        "name":"Capa de patrones 2",
        "opacity":1,
        "type":"tilelayer",
        "visible":true,
        "width":8,
        "x":0,
        "y":0
    }],
    "orientation":"orthogonal",
    "properties":{},
    "tileheight":32,
    "tilesets":[{
        "firstgid":1,
        "image":"free_tileset_version_10.png",
        "imageheight":1216,
        "imagewidth":480,
        "margin":0,
        "name":"free_tileset_version_10",
        "properties": {},
        "spacing":0,
        "tileheight":32,
        "tilewidth":32
    }],
    "tilewidth":32,
    "version":1,
    "width":8
};

Realmente hay muchas propiedades que no vamos a utilizar, pero igual que antes, para evitar irnos del tópico del tutorial, lo daremos por bueno.

Seguidamente necesitamos crear la instancia a nuestra imagen del tile set:

var oImage = new Image();
oImage.src = "free_tileset_version_10.png";

Y como es lógico, también necesitamos crear nuestro contexto 2D dónde pintaremos el mapa. Cómo de costumbre, necesitaremos esperar a que la página esté cargada antes de poder seleccionar nuestro canvas:

window.onload = function () {
    var oCanvas = document.getElementById("canvas");
    var oContext = oCanvas.getContext("2d");
}

Para finalizar, necesitaremos definir una función que llamaremos “drawTiles” que será la encargada de pintar el mapa en nuestro Canvas. Explicaré el código inline a través de comentarios:

drawTiles = function () {
		// Ancho (en píxeles) de cada tile. En nuestro ejemplo: 32.
	var nTileWidth = oTileMap.tilewidth,
		// Altura (en píxeles) de cada tile. En nuestro ejemplo: 32.
		nTileHeight = oTileMap.tileheight,
		// Ancho (en píxeles) total del mapa. En nuestro ejemplo: 32 * 8 = 256.
		nMapWidth = nTileWidth * oTileMap.width,
		// Capas de nuestro mapa.
		aLayers = oTileMap.layers,
		// Número de capas de nuestro mapa. En nuestro ejemplo: 2 (una para
		// el suelo, otro para los elementos de la habitación).
		aLayersLen = aLayers.length,
		// Número de columnas que tiene la imágen.
		nImageCols = oImage.width / nTileWidth,
		// Número de celdas que tiene la imágen.
		nImageRows = oImage.height / nTileHeight,
		// Variable donde guardaremos la información de la capa actual.
		oCurrentLayer,
		oCurrentLayerLen,
		// Variables para el control de los bucles.
		nTileId,
		nSourceX,
		nSourceY,
		nDataCount,
		nCount,
		nAxisX,
		nAxisY;
 
	// Vamos a pintar todos los elementos de una capa, y al finalizar, pasaremos a la siguiente capa.
	// Recordemos que cada capa contiene un conjunto de elementos. En nuestro caso, la primera
	// capa contiene el suelo, y la segunda capa elementos como las sillas, mesas, etc.
	for (nCount = 0; nCount < aLayersLen; nCount += 1) {
 
		oCurrentLayer = aLayers[nCount].data;
		oCurrentLayerLen = oCurrentLayer.length;
 
		// Variables para controlar en qué posición del Canvas debemos pintar.
		nAxisX = 0;
		nAxisY = 0;
 
		for (nDataCount = 0; nDataCount < oCurrentLayerLen; nDataCount += 1) {
 
			// nTileId contiene el ID del tile que queremos dibujar.
			nTileId = oCurrentLayer[nDataCount];
			// Necesitamos saber en que columna de la imagen se encuentra el tile
			// con nTileId. Restamos una unidad ya que los arrays empiezan por el índice 0,
			// en lugar del índice 1.
			nSourceX = Math.floor(nTileId % nImageCols) -1;
 
			// Si nSourceX === -1 quiere decir que la ID del tile era 0, por lo tanto, es un tile
			// donde no debemos pintar nada. Si es distinto a -1, entonces hay que pintar.
			if (nSourceX !== -1) {
				// Hasta el momento nSourceX contenía la columna, pero necesitamos saber la posición
				// en píxeles de dicha columna.
				nSourceX *= nTileWidth;
 
				// Igual que con nSourceX, necesitamos saber la fila dentro de la imágen en la que se
				// encuentra el nTileId.
				nSourceY = Math.floor(nTileId / nImageCols);
				// Ahora tenemos la fila, y necesitamos saber la posición en píxeles de dicha fila.
				nSourceY *= nTileHeight;
 
				// Finalmente pintamos el tile.
				oContext.drawImage(oImage, nSourceX, nSourceY, nTileWidth, nTileHeight, nAxisX, nAxisY, nTileWidth, nTileHeight);
			}
 
			// Incrementamos la posición X donde vamos a pintar el próximo tile.
			nAxisX += nTileWidth;
			// Si la posición X es igual al ancho del mapa, quiere decir que debemos
			// pasar a dibujar la siguiente fila. Por lo tanto reseteamos la X e
			// incrementamos Y.
			if (nAxisX === nMapWidth) {
				nAxisX = 0;
				nAxisY += nTileHeight;
			}
		}
	}
};
// Llamamos la función drawTiles();
drawTiles();

La función podemos colocarla fuera de la función onload. Pero la llamada sí debemos colocarla dentro del onload, justo después de la creación del contexto del canvas.

Si ejecutamos tiles.html en un navegador, veremos el mapa que construimos en Tiled renderizado en nuestro canvas.

 

Rendimiento:

Durante la ejecución de un juego, no sería demasiado óptimo pintar a cada frame todos los tiles. Así que una buena opción sería la de cachear el resultado final de cada capa la primera vez que la pintásemos en el mapa y, para las consecutivas ejecuciones, utilizar la imagen cacheada para tan sólo tener que llamar la función “drawImage” una vez para cada capa.

 

4 Comments
  1. Muy chulo el tutorial. Me ha servido de mucho. Sólo tengo una duda: dices que una vez que hemos dibujado todos los tiles, lo óptimo sería cachear la imagen que nos queda para no tenener que llamar a DrawTiles() cada vez, lo cual me parece lógico. El caso es que no sé cómo hacerlo. ¿Podrías darme alguna pista para saber por dónde van los tiros?

    ¡Gracias!

  2. Una opción sería crear un elemento canvas nuevo:
    var oCanvasEscenario = document.createElement(“canvas”);
    var oContextEscenario = oCanvasEscenario.createContext(“2d”);

    y pintar los tiles sobre el contexto de ese canvas (oContextEscenario) una única vez, por ejemplo, en el momento de iniciar el juego.

    Entonces, cada vez que quisieras pintar el escenario, deberias hacer:

    oContextPrincipal.drawImage(oCanvasEscenario, x, y);

    Fíjate que el 1r parámetro del drawImage en este caso NO es el contexto, es el canvas (oCanvasEscenario).

    ¡Saludos!

  3. Byterules permalink

    Buenas.
    En primer lugar buen tutorial, y gracias por enseñarnos la aplicación tiled.
    He probado a seguir los pasos pero no consigo cargar la imagen en el canvas ni tampoco veo ningún error de ejecución, así que estoy un poco perdido. ¿Podrías poner un ejemplo de como lo ejecuta? Es decir, un enlace a una pagina donde se ejecute esto que nos enseñas en el tutorial.
    Gracias, un saludo!

  4. Byterules permalink

    Hola de nuevo!

    Ya encontré el error en lo que intenté hacer.

    En el evento onload de la ventana las variables oCanvas, oContext y oImage las tenía definidas como locales, y no tomaba su valor al aplicar la función drawTiles(), por lo que si a alguien más le ocurre ese error, se arregla definiéndolas como variables globales, es decir, sin poner el “var” delante de ellas.

    Un saludo!

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS