Administrar mapas de texto en una matriz 2D para pintar en HTML5 Canvas


46

Entonces, estoy haciendo un juego de rol HTML5 solo por diversión. El mapa es un <canvas>(512 píxeles de ancho, 352 píxeles de alto | 16 cuadros de ancho, 11 cuadros de arriba a abajo). Quiero saber si hay una forma más eficiente de pintar <canvas>.

Así es como lo tengo ahora.

Cómo se cargan y pintan los mosaicos en el mapa

El mapa se pinta con azulejos (32x32) usando la Image()pieza. Los archivos de imagen se cargan a través de un forbucle simple y se colocan en una matriz llamada tiles[]para PINTAR al usar drawImage().

Primero, cargamos los azulejos ...

ingrese la descripción de la imagen aquí

y así es como se está haciendo:

// SET UP THE & DRAW THE MAP TILES
tiles = [];
var loadedImagesCount = 0;
for (x = 0; x <= NUM_OF_TILES; x++) {
  var imageObj = new Image(); // new instance for each image
  imageObj.src = "js/tiles/t" + x + ".png";
  imageObj.onload = function () {
    console.log("Added tile ... " + loadedImagesCount);
    loadedImagesCount++;
    if (loadedImagesCount == NUM_OF_TILES) {
      // Onces all tiles are loaded ...
      // We paint the map
      for (y = 0; y <= 15; y++) {
        for (x = 0; x <= 10; x++) {
          theX = x * 32;
          theY = y * 32;
          context.drawImage(tiles[5], theY, theX, 32, 32);
        }
      }
    }
  };
  tiles.push(imageObj);
}

Naturalmente, cuando un jugador comienza un juego, carga el mapa que dejó por última vez. Pero por aquí, es un mapa de toda la hierba.

En este momento, los mapas usan matrices 2D. Aquí hay un mapa de ejemplo.

[[4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 1, 1, 1, 1], 
[1, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 13, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 13, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 1, 1, 1, 1, 1, 1, 1, 13, 13, 13, 13, 13, 1], 
[1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1, 1, 1]];

Obtengo diferentes mapas usando una ifestructura simple . Una vez que la matriz 2d anterior es return, el número correspondiente en cada matriz se pintará de acuerdo con el Image()almacenado en el interior tile[]. Luego drawImage()ocurrirá y pintará de acuerdo con el xy yy el tiempo 32para pintar en la x-ycoordenada correcta .

Cómo se produce el cambio de mapa múltiple

Con mi juego, mapas tienen cinco cosas a no perder de vista: currentID, leftID, rightID, upID, y bottomID.

  • currentID: la ID actual del mapa en el que se encuentra.
  • leftID: qué ID currentIDcargar para salir al salir a la izquierda del mapa actual.
  • rightID: qué ID de currentIDcargar cuando sale a la derecha del mapa actual.
  • downID: qué ID de currentIDcargar cuando salga en la parte inferior del mapa actual.
  • upID: qué ID de currentIDcargar cuando sales en la parte superior del mapa actual.

Algo que nota: Si bien leftID, rightID, upID, o bottomIDno son específicos, lo que significa que son una 0. Eso significa que no pueden dejar ese lado del mapa. Es simplemente un bloqueo invisible.

Entonces, una vez que una persona sale de un lado del mapa, dependiendo de dónde salió ... por ejemplo, si salió en la parte inferior, bottomIDaparecerá el número de la mapcarga y, por lo tanto, se pintará en el mapa.

Aquí hay un .GIF representativo para ayudarlo a visualizar mejor:

ingrese la descripción de la imagen aquí

Como puede ver, tarde o temprano, con muchos mapas, trataré con muchas ID. Y eso posiblemente puede ser un poco confuso y agitado.

La ventaja obvia es que carga 176 mosaicos a la vez, actualiza un pequeño lienzo de 512x352 y maneja un mapa a la vez. La desventaja es que los identificadores de MAP, cuando se trata con muchos mapas, a veces pueden ser confusos.

Mi pregunta

  • ¿Es esta una forma eficiente de almacenar mapas (dado el uso de mosaicos), o hay una mejor manera de manejar mapas?

Estaba pensando en la línea de un mapa gigante. El tamaño del mapa es grande y es todo un conjunto 2D. La ventana gráfica, sin embargo, sigue siendo 512x352 píxeles.

Aquí hay otro .gif que hice (para esta pregunta) para ayudar a visualizar:

ingrese la descripción de la imagen aquí

Lo siento si no puedes entender mi inglés. Por favor, pregunte cualquier cosa que tenga problemas para entender. Con suerte, lo dejé claro. Gracias.


16
El esfuerzo puesto en esta pregunta con los gráficos y todo merece un voto positivo. :)
Tapio

Respuestas:


5

Editar: acabo de ver que mi respuesta se basó en su código, pero en realidad no respondió su pregunta. Mantuve la vieja respuesta en caso de que pueda usar esa información.

Edición 2: he solucionado 2 problemas con el código original: - La adición de +1 en el cálculo de los extremos xey fue por error dentro de los corchetes, pero debe agregarse después de la división. - Olvidé validar los valores x e y.

Simplemente podría tener el mapa actual en la memoria y cargar los mapas circundantes. Imagine un mundo que consiste en una matriz 2D de niveles individuales del tamaño 5x5. El jugador comienza en el campo 1. Como los límites superior e izquierdo del nivel están en el borde del mundo, no es necesario cargarlos. En ese caso, el nivel 1/1 está activo y los niveles 1/2 y 2/1 están cargados ... Si el jugador ahora se mueve hacia la derecha, todos los niveles (además del que está moviendo) se descargan y los nuevos alrededores están cargado. El nivel medio 2/1 ahora está activo, 1/1, 2/2 y 3/1 ahora están cargados.

Espero que esto te dé una idea de cómo se podría hacer. Pero este enfoque no funcionará muy bien, cuando cada nivel tiene que ser simulado durante el juego. Pero si puede congelar los niveles no utilizados, esto debería funcionar bien.

Vieja respuesta:

Lo que hago cuando renderizo el nivel (también basado en mosaicos) es calcular qué elementos de la matriz de mosaicos se cruzan con la ventana gráfica y luego solo renderizo esos mosaicos. De este modo, puede tener mapas grandes pero solo necesita representar la parte en la pantalla.

//Mock values
//camera position x
//viewport.x = 233f;
//camera position y
//viewport.y = 100f;
//viewport.w = 640
//viewport.h = 480
//levelWidth = 10
//levelHeight = 15
//tilesize = 32;

//startX defines the starting index for the x axis.
// 7 = 233 / 32
int startX = viewport.x / tilesize;

//startY defines the starting index
// 3 = 100 / 32
int startY = viewport.y / tilesize;

//End index y
// 28 = (233 + 640) / 32 + 1
int endX = ((viewport.x + viewport.w) / tilesize) + 1;

//End index y
// 19 = (100 + 480) / 32 + 1
int endX = ((viewport.x + viewport.w) / tilesize) + 1;

//Validation
if(startX < 0) startX = 0;
if(startY < 0) startY = 0;
//endX is set to 9 here
if(endX >= levelWidth) endX = levelWidth - 1;
//endX is set to 14 here
if(endY >= levelHeight) endY = levelHeight - 1;

for(int y = startY; y < yEnd; y++)
    for(int x = startX; x < xEnd; x++)
          [...]

Nota: El código anterior no se prueba, pero debería dar una idea de qué hacer.

Siguiendo un ejemplo básico para una representación de ventana gráfica:

public class Viewport{
    public float x;
    public float y;
    public float w;
    public float h;
}

Una representación de JavaScript se vería así

<script type="text/javascript">
//Declaration
//Constructor
var Viewport = function(xVal, yVal, wVal, hVal){
    this.x = xVal;
    this.y = yVal;
    this.w = wVal;
    this.h = hVal;
}
//Prototypes
Viewport.prototype.x = null;
Viewport.prototype.y = null;
Viewport.prototype.w = null;
Viewport.prototype.h = null;
Viewport.prototype.toString = function(){
    return ["Position: (", this.x, "/" + this.y + "), Size: (", this.w, "/", this.h, ")"].join("");
}

//Usage
var viewport = new Viewport(23, 111, 640, 480);
//Alerts "Position: (23/111), Size: (640/480)
alert(viewport.toString());
</script>

Si toma mi ejemplo de código, simplemente tiene que reemplazar las declaraciones int y float con var. También asegúrese de usar Math.floor en esos cálculos, que se asignan a los valores basados ​​en int, para obtener el mismo comportamiento.

Una cosa que consideraría (aunque no estoy seguro de si esto es importante en JavaScript) es colocar todos los mosaicos (o tantos como sea posible) en una gran textura en lugar de usar muchos archivos individuales.

Aquí hay un enlace que explica cómo hacerlo: http://thiscouldbebetter.wordpress.com/2012/02/25/slicing-an-image-into-tiles-in-javascript/


Solo para aclarar ... viewport.x estaría predicho como "531" y viewport.y sería "352" y tilesize = "32". Y tal..?
prueba

Si quiere decir que la posición de la cámara es 531, entonces sí. Agregué algunos comentarios al código anterior.
tom van green

Estoy usando JavaScript ... Soy Java es casi idéntico a esto.
prueba

Sí, solo tiene que hacer una clase de ventana gráfica basada en js (o simplemente puede crear 4 variables x, y, w y h). También debe asegurarse de poner en el suelo (Math.floor) los cálculos asignados a los valores int. Agregué un ejemplo de código, cómo se vería la clase de ventana gráfica. Solo asegúrese de poner la declaración antes del uso.
tom van green

Solo digo que pones 640, 480... pero puede funcionar 512, 352¿verdad?
prueba

3

Almacenar mapas como mosaicos es útil para permitirle reutilizar activos y rediseñar el mapa. Siendo realistas, no ofrece muchos beneficios al jugador. Además, luego vuelves a dibujar cada mosaico cada vez. Esto es lo que haría:

Almacene todo el mapa como una gran matriz. No tiene sentido tener mapas y submapas, así como potencialmente sub-submapas (si está entrando en edificios, por ejemplo). Lo estás cargando todo de todos modos y esto mantiene todo agradable y simple.

Renderiza el mapa de una vez. Ten un lienzo fuera de la pantalla para representar el mapa y luego úsalo en tu juego. Esta página le muestra cómo hacerlo con una simple función de JavaScript:

var renderToCanvas = function (width, height, renderFunction) {
  var buffer = document.createElement('canvas');
  buffer.width = width;
  buffer.height = height;
  renderFunction(buffer.getContext('2d'));
  return buffer;
};

Esto supone que su mapa no va a cambiar mucho. Si desea renderizar submapas en el lugar (por ejemplo, el interior de un edificio), también debe representarlo en un lienzo y luego dibujarlo en la parte superior. Aquí hay un ejemplo de cómo puede usar esto para representar su mapa:

var map = renderToCanvas( map_width*32, map_height*32, renderMap );

var MapSizeX = 512;
var MapSizeY = 352;

var draw = function(canvas, mapCentreX, mapCentreY) {
  var context = canvas.getContext('2d');

  var minX = playerX - MapSizeX/2;
  var minY = playerY - MapSizeY/2;

  context.drawImage(map,minX,minY,MapSizeX,MapSizeY,0,0,MapSizeX,MapSizeY);
}

Esto dibujará una pequeña porción del mapa centrada mapCentreX, mapCentreY. Esto le ofrecerá un desplazamiento suave por todo el mapa, así como el movimiento de sub-mosaico: puede almacenar la posición del jugador como 1/32 de un mosaico, para que pueda obtener un movimiento suave en todo el mapa (Claramente si desea mover mosaico por mosaico como una opción estilística, solo puede aumentar en trozos de 32).

Todavía puede dividir su mapa si lo desea, o si su mundo es enorme y no cabe en la memoria de sus sistemas de destino (tenga en cuenta que incluso para 1 byte por píxel, un mapa de 512x352 es 176 KB) pero por pre -Representando su mapa de esta manera, verá una gran ganancia de rendimiento en la mayoría de los navegadores.

Lo que esto le brinda es la flexibilidad de reutilizar mosaicos en todo el mundo sin el dolor de cabeza de editar una imagen grande, pero también le permite ejecutar esto rápida y fácilmente y solo considerar un 'mapa' (o tantos 'mapas' como desee) .


2

En todas las implementaciones que he visto, los mapas de mosaico a menudo se generan como una imagen grande y luego se "fragmentan" en partes más pequeñas.

https://gamedev.stackexchange.com/a/25035/10055

Estos fragmentos generalmente tienen potencias de 2, por lo que se ajustan a la memoria de manera óptima.


1

Una manera fácil de realizar un seguimiento de las ID sería simplemente usar otra matriz 2d con ellas: al mantener solo x, y en esa "meta matriz", puede buscar fácilmente las otras ID.

Dicho esto, aquí está la versión de Google de los juegos de lienzo en mosaico 2D (bueno, en realidad es multijugador HTML5, pero el motor de mosaico también está cubierto) de su reciente E / S 2012: http://www.youtube.com/watch?v=Prkyd5n0P7k It También tiene código fuente disponible.


1

Su idea de cargar todo el mapa en una matriz primero, es una forma eficiente, pero será fácil obtener la inyección de JavaScript, cualquiera podrá conocer el mapa y allí se va la aventura.

Estoy trabajando en un juego de rol basado en la web también, la forma en que cargo mi mapa es a través de Ajax desde una página PHP, que carga el mapa desde la base de datos, donde dice la posición X, Y, Z y vlaue del azulejo, dibujalo y finalmente el jugador se mueve.

Todavía estoy trabajando en ello para hacerlo más eficiente, pero el mapa se carga lo suficientemente rápido como para mantenerlo así por ahora.


1
¿Puedo ver una demostración?
prueba

1

No , una matriz es la forma más eficiente de almacenar un mapa en mosaico .

Puede haber algunas estructuras que requieren un poco menos de memoria, pero solo a expensas de costos mucho más altos para cargar y acceder a los datos.


Sin embargo, es necesario tener en cuenta cuándo va a cargar el siguiente mapa. Dado que los mapas no son especialmente grandes, lo que realmente llevará más tiempo es esperar a que el servidor entregue el mapa. La carga real solo tomará unos pocos milisegundos. Pero esta espera puede hacerse asincrónica, no tiene que bloquear el flujo del juego. Entonces, lo mejor es no cargar los datos cuando sea necesario, sino antes. El jugador está en un mapa, ahora carga todos los mapas adyacentes. El jugador se mueve en el siguiente mapa, carga todos los mapas adyacentes nuevamente, etc., etc. El jugador ni siquiera notará que tu juego se está cargando en segundo plano.

De esta manera, también puede crear una ilusión de un mundo sin fronteras dibujando también los mapas adyacentes, en ese caso, necesita cargar no solo los mapas adyacentes sino también los adyacentes.


0

No estoy seguro de por qué piensas: como puedes ver, tarde o temprano, con muchos mapas me ocuparé de muchas identificaciones. Y eso posiblemente puede ser un poco confuso y agitado.

LeftID siempre será -1 del currentID, el rightID siempre será +1 del currentID.

UpID siempre será - (Ancho total del mapa) de la ID actual y downID siempre será + (ancho total del mapa) de la ID actual con la excepción de cero, lo que significa que ha llegado al borde.

Un solo mapa se puede dividir en muchos mapas, y se guardan secuencialmente con mapIDXXXX, donde XXXX es la identificación. De esa manera, no hay nada de qué confundirse. He estado en su sitio y parece, podría estar equivocado, que el problema se debe a que su editor de mapas impone una limitación de tamaño que dificulta su automatización para dividir un mapa grande en varios pequeños al guardar y esta limitación es probablemente restringiendo una solución técnica.

He escrito un editor de mapas Javascript que se desplaza en todas las direcciones, mosaicos de 1000 x 1000 (no recomendaría ese tamaño) y todavía se mueve tan bien como un mapa de 50 x 50 y eso es con 3 capas, incluido el desplazamiento de paralaje. Guarda y lee en formato json. Le enviaré una copia si dice que no le importa un correo electrónico de aproximadamente 2 meg. Está destinado a estar en Github, pero no tengo ningún conjunto de fichas decente (legal), por eso no me he molestado en ponerlo allí todavía.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.