Mapa de tiles en JavaScript

En este artículo vamos a explicar las bases para crear un mapa de tiles con HTML5 y la API Canvas de JavaScript. Cuando desarrollamos un videojuego 2D con JavaScript y HTML5, una técnica muy común para construir escenarios es el uso de tilemaps. Un tilemap es básicamente una matriz (array bidimensional) donde cada celda contiene un número que representa un tile (o tesela). Es decir, el gráfico que debe dibujarse en esa posición, como si se tratara de un mosaico. Puedes consultar mi último libro Mapas y Mundos 2D, donde explico en profundidad estos conceptos y muchas otras técnicas relacionadas.

En este artículo veremos paso a paso cómo dibujar con JavaScript un mapa de tiles en un lienzo Canvas a partir de un array 2D (tilemap), utilizando un tileset como imagen fuente para representar visualmente cada tile. Empecemos por definir cada una de estas palabras.

¿Qué es un tile y qué es un tileset?

Un tile es una imagen pequeña, normalmente cuadrada (por ejemplo, de 45×45 píxeles), que representa una parte del escenario: suelo, hierba, muro, agua, etc. Una característica clave es que, en lugar de usar decenas o cientos de imágenes sueltas, es habitual agrupar todos los tiles en una única imagen más grande llamada tileset. Aquí puedes ver un ejemplo de tileset el cual utilizo para dibujar parte del escenario en el videojuego Space Odyssey.

Ejemplo de tileset
Ejemplo de tileset

Como podemos ver, se trata de una única imagen (tileset) compuesta por múltiples imágenes pequeñas (tiles), organizadas en forma de cuadrícula. Cada pequeña «celda», marcada en rojo, representa un tile. Existen muchas estrategias para diseñar tilesets, desde organizarlos por temáticas o zonas, hasta incluir todos los gráficos del videojuego en una sola imagen.

La principal ventaja de agrupar estos pequeños gráficos en un único tileset es que podemos identificar cada tile mediante un código numérico de dos dígitos que indica su posición (columna, fila) dentro de la cuadrícula. Así, con JavaScript podemos crear un mapa de índices (tilemap) que actúe como plano del escenario, construyendo el entorno mediante la colocación y repetición de estos tiles a lo largo del lienzo.

¿Qué es un tilemap?

Un tilemap es un array bidimensional donde cada número representa el tile que debe dibujarse en esa posición del mapa, por ejemplo:

const tileMap = [
    ['',    '',    '',   3, 40, 40, 83,   '',   '',   '',    '',   3, 40, 50, 40, 83,   '',  '',   '',   '',    3, 50, 40, 50, 83],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    [ 3, 40, 40, 83,    '',   '',   '',   '',   3, 40, 50, 40,  83,   '',   '',   '',   '',  3, 50, 40, 50, 83,   '',   '',    ''],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
    ['',    '',    '',   3, 40, 40, 83,   '',   '',   '',    '',   3, 40, 50, 40, 83,   '',  '',   '',   '',    3, 50, 40, 50, 83],
]};

En este ejemplo, los códigos numéricos [0..99] hacen referencia a distintos tiles gráficos. Cada uno de estos tiles podrá ser extraído mediante JavaScript y su código o índice numérico (columna, fila) dentro del tileset, para ser dibujado en el lienzo Canvas o mapa. Un detalle importante es que debemos definir un mismo orden o método (columna, fila) para identificar cada tile, sabiendo que por las características de un array bidimensional se debe acceder mediante el orden (fila, columna), cuando en matemáticas el orden de escritura habitual es (x, y).

NOTA: En este artículo mostraremos una característica donde, si el tileMap tiene un string vacío (»), no se dibuje nada en esa posición. Esto abre muchas opciones al diseñar nuestros mapas y lo explico más ámpliamente en mi libro Mapas y Mundos 2D.

Dibujar el mapa de tiles

A partir de nuestra imagen tileset y del mapa definido en tileMap, vamos a crear el código JavaScript necesario para dibujar los tiles de nuestro tileMap en el mapa o lienzo Canvas. Al diseñar nuestros mapas es importante encontrar un equilibrio entre las dimensiones del tilemap y del lienzo, de modo que los tiles se ajusten adecuadamente. En este ejemplo, el lienzo mide 800×450 píxeles y el tilemap consta de 10 filas de 45 píxeles cada una. Esto hace que la altura encaje perfectamente, mientras que el ancho del mapa supera el tamaño del lienzo, permitiendo así implementar un posible scroll horizontal. El código fuente completo para implementar un mapa de tiles en JavaScript es el siguiente:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ejemplo de tilemap</title>
  <style>
    body {
      text-align: center;
    }
    canvas {
      margin: auto;
      background-color: black;
    }
    img {
      margin: auto;
      display: block;
    }
  </style>
</head>
<body>
  <h1>Ejemplo de tilemap</h1>
  <p>Obtenemos una imagen tileset y procedemos a dibujar diferentes tiles según el tilemap</p>
  <figure>
    <img id="tileset" title="Imagen del tileset" src="tileset.jpg" alt="Imagen del tileset">
    <figcaption>Imagen del tileset</figcaption>
  </figure>
  <canvas id="miCanvas" width="800" height="450"></canvas>

  <script>
    const canvas = document.getElementById('miCanvas');
    const ctx = canvas.getContext('2d');
    const tileset = document.getElementById('tileset');
    const TILE_SIZE = 45; // tamaño de cada tile en píxeles
    const tileMap = [
       ['',    '',    '',   3, 40, 40, 83,   '',   '',   '',    '',   3, 40, 50, 40, 83,   '',  '',   '',   '',    3, 50, 40, 50, 83],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       [ 3, 40, 40, 83,    '',   '',   '',   '',   3, 40, 50, 40,  83,   '',   '',   '',   '',  3, 50, 40, 50, 83,   '',   '',    ''],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       ['',    '',    '',   '',    '',   '',   '',   '',   '',   '',    '',   '',    '',   '',   '',   '',   '',  '',    '',   '',   '',    '',   '',   '',    ''],
       ['',    '',    '',   3, 40, 40, 83,   '',   '',   '',    '',   3, 40, 50, 40, 83,   '',  '',   '',   '',    3, 50, 40, 50, 83],
   ]};

    tileset.onload = () => {
      for (let row=0; row < tileMap.length; row++) {
        for (let col = 0; col < tileMap[row].length; col++) {
          const tileIndex = tileMap[row][col];

          // Coordenadas del tile dentro del tileset
          if (tileIndex == '') continue;
          const sy = (tileIndex % 10) * TILE_SIZE;
          const sx = Math.floor(tileIndex / 10) * TILE_SIZE;

          // Coordenadas donde dibujar el tile en el canvas
          const dx = col * TILE_SIZE;
          const dy = row * TILE_SIZE;

          // Dibujar el tile en el canvas
          ctx.drawImage(
            tileset,
            sx, sy, TILE_SIZE, TILE_SIZE, // recorte del tileset
            dx, dy, TILE_SIZE, TILE_SIZE  // posición en el canvas
          ); 
        }
      }
    };
  </script>
</body>
</html>

Código JavaScript para el mapa de tiles

Este código JavaScript es el núcleo de un sistema para dibujar un mapa de tiles en un lienzo Canvas usando un tileset y un tilemap, veamos cómo funciona:

  • canvas, ctx, tileset: Primero, seleccionamos el elemento <canvas> del HTML y accedemos a su contexto 2D, que es lo que nos permite dibujar. También cargamos la imagen del tileset desde el DOM.
  • TILE_SIZE: Cada tile tiene un tamaño fijo. En este caso, cada celda mide 45×45 píxeles, tanto en el tileset como al dibujarlo en el lienzo Canvas.
  • tileMap: El tilemap es una matriz bidimensional que representa el escenario. Cada celda puede contener un número (el índice del tile a dibujar desde el tileset) o una cadena vacía ('') para dejar ese espacio del mapa vacío.
  • onload: Para que este ejemplo funcione, debemos esperar a que la imagen del tileset se cargue y luego recorremos cada fila y columna del tilemap. En un videojuego real esto se implementaría de forma diferente.
  • tileIndex: Al recorrer el array tileMap, obtenemos el código de cada tile que debe ser dibujado en el lienzo Canvas.
  • if (tileIndex == ») continue; Si el código es una celda está vacía, no se dibuja nada y continuamos recorriendo el tileMap.
  • sx, sy: Aquí calculamos la posición (columna, fila) del tile dentro del tileset. La esquina superior izquierda es (0, 0) y podemos calcular su posición con:
    • tileIndex % 10 nos da la fila dentro del tileset.
    • Math.floor(tileIndex / 10) nos da la columna.
    • Al multiplicar ambos por TILE_SIZE obtenemos sus coordenadas (sx, sy) dentro del tileset.
  • dx, dy: Calculamos las coordenadas (x, y) donde se va a dibujar el tile en el lienzo Canvas, multiplicando su posición en el tilemap por el tamaño del tile. En este caso mantenemos el mismo tamaño.
  • drawImage(): Por último, usamos drawImage() para dibujar el tile correspondiente en la posición deseada del lienzo. Esta función extrae un recorte del tileset usando (sx, sy) como origen y TILE_SIZE como dimensiones, y lo coloca en (dx, dy) con tamaño TILE_SIZE dentro del Canvas.

Con este sistema podemos construir nuestro mapa de tiles en JavaScript a partir de un tileset de hasta 100 tiles diferentes (0 a 99) organizados en un tileset de 10 filas y 10 columnas.

Ejemplo del mapa de tiles

Al ejecutar el código anterior obtendremos el siguiente resultados visual. Se trata de una página HTML con un lienzo Canvas sobre el cual hemos dibujado nuestro mapa de tiles con JavaScript, a partir de nuestro tileset y nuestro tileMap.

Mapa de tiles en JavaScript

Mapa de tiles en JavaScript

Como ves, el uso de tilesets y tilemaps es una técnica potente y eficiente para construir un mapa completo a partir de pequeños gráficos reutilizables (tiles) con JavaScript. Gracias a la combinación de un mapa numérico (tilemap) y una imagen base (tileset), podemos generar escenarios visualmente ricos con muy poco código JavaScript y gran flexibilidad.

Esta técnica, no solo permite crear un mapa de tiles con JavaScript puro, sinó que mejora el rendimiento y la organización de los recursos gráficos, permitiendo un control total sobre el diseño de niveles. Esto nos permite facilitar su edición, extensión y reutilización en distintos contextos del juego.

En este artículo sólo hemos implementado una técnica muy básica para crear un mapa de tiles con JavaScript. Pero con una estructura bien definida y un poco de lógica, podemos construir mapas muy grandes con poco coste en rendimiento. Esta técnica es simple, eficiente y ampliamente utilizada en juegos 2D como plataformas, RPGs o estrategia. Si quieres profundizar más en esta temática te animo a leer mi último libro Mapas y Mundos 2D. En él encontrarás todos estos conceptos, y muchos otros, en profundidad.

¡ Espero que este artículo sea de vuestro interés !

Deja un comentario