Uso de máscaras de mosaico para establecer el tipo de mosaico según los mosaicos que lo rodean

Las máscaras de mosaico, que se utilizan comúnmente en los juegos basados ​​en fichas, te permiten cambiar una ficha dependiendo de sus vecinos, lo que te permite mezclar terrenos, reemplazar fichas y mucho más. En este tutorial, te mostraré un método escalable y reutilizable para detectar si los vecinos inmediatos de una ficha coinciden con alguno de los patrones que configuras.

Nota: Aunque este tutorial está escrito con C # y Unity, debes poder usar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos..


¿Qué es una máscara de azulejo?

Considera lo siguiente: estás haciendo una especie de Terraria y quieres que los bloques de tierra se conviertan en bloques de lodo si están cerca del agua. Supongamos que su mundo tiene mucha más agua que tierra, por lo que la forma más económica de hacerlo es comprobando cada bloque de tierra para ver si está cerca del agua y no al revés. Este es un buen momento para usar un máscara de azulejo.

Las máscaras de azulejos son tan antiguas como las montañas, y se basan en una idea simple. En una cuadrícula 2D de objetos, un mosaico puede tener otros ocho mosaicos directamente adyacentes a él. Llamaremos a esto el rango local de esa teja central (Figura 1).


Figura 1. El rango local de un azulejo..

Para decidir qué hacer con su baldosa de tierra, comparará su rango local con un conjunto de reglas. Por ejemplo, puede mirar directamente sobre el bloque de tierra y ver si hay agua allí (Figura 2). El conjunto de reglas que utiliza para evaluar su rango local es su máscara de azulejo.


Figura 2. Máscara de azulejo para identificar un bloque de tierra como un bloque de barro. Los azulejos grises pueden ser cualquier otro azulejo..

Por supuesto, también querrá verificar si hay agua en las otras direcciones, por lo que la mayoría de las máscaras de baldosas tienen más de una disposición espacial (Figura 3).


Figura 3. Las cuatro orientaciones típicas de una máscara de azulejo: 0 °, 90 °, 180 °, 270 °.

Y a veces encontrará que incluso los arreglos de máscara de mosaico realmente simples pueden ser duplicados y no superponibles (Figura 4).


Figura 4. Un ejemplo de quiralidad en una máscara de azulejo. La fila superior no se puede girar para coincidir con la fila inferior.

Estructuras de almacenamiento de una máscara de azulejo

Almacenando sus elementos

Cada celda de una máscara de azulejo tiene un elemento en su interior. El elemento en esa celda se compara con el elemento correspondiente en el rango local para ver si coinciden.

Uso listas como elementos. Las listas me permiten realizar comparaciones de complejidad arbitraria, y Linq de C # proporciona formas muy útiles de combinar listas en listas más grandes, reduciendo la cantidad de cambios que potencialmente puedo olvidar hacer..

Por ejemplo, una lista de azulejos de pared se puede combinar con una lista de azulejos de piso y una lista de azulejos de apertura (puertas y ventanas) para producir una lista de azulejos estructurales, o listas de azulejos de muebles de habitaciones individuales se pueden combinar para hacer Una lista de todos los muebles. Otros juegos pueden tener listas de fichas que se pueden quemar o que están afectadas por la gravedad.

Almacenamiento de la máscara de azulejo en sí mismo

La forma intuitiva de almacenar una máscara de mosaico es como una matriz 2D, pero el acceso a las matrices 2D puede ser lento, y lo va a hacer mucho. Sabemos que un rango local y una máscara de mosaico siempre constarán de nueve elementos, por lo que podemos evitar esa penalización de tiempo mediante el uso de una matriz plana 1D, leyendo el rango local desde arriba hacia abajo, de izquierda a derecha (Figura 4).


Figura 5. Almacenamiento de una máscara de mosaico 2D en una estructura 1D.

Las relaciones espaciales entre los elementos se conservan si alguna vez los necesita (no los he necesitado hasta ahora), y los arreglos pueden almacenar casi cualquier cosa. Las Tilemasks en este formato también pueden rotarse fácilmente mediante lectura / escritura en offset.

Almacenar información de rotación

En algunos casos, es posible que desee girar el panel central de acuerdo con su entorno, como al colocar una pared junto a otras paredes. Me gusta usar matrices dentadas para mantener las cuatro rotaciones de una máscara de mosaico en el mismo lugar, usando el índice de la matriz externa para indicar la rotación actual (más sobre eso en el código).


Requisitos del Código

Con los conceptos básicos explicados, podemos describir qué debe hacer el código:

  1. Definir y almacenar máscaras de azulejos..
  2. Gire las máscaras de mosaico automáticamente, en lugar de que nosotros tengamos que mantener manualmente cuatro copias rotadas de la misma definición.
  3. Agarra el rango local de un azulejo.
  4. Evaluar el rango local contra una máscara de azulejo.

El siguiente código está escrito en C # para Unity, pero los conceptos deberían ser bastante portátiles. El ejemplo es uno de mi propio trabajo al convertir de manera procedimental mapas de solo texto roguelike a 3D (colocando una sección recta de muro, en este caso).


Definir, rotar y almacenar las máscaras de azulejo

Automatizo todo esto con un método de llamada a un DefineTilemask método. Aquí hay un ejemplo de uso, con la declaración del método a continuación..

 Lista estática pública de solo lectura any = new List() ; Lista estática pública de solo lectura ignorado = nueva lista() ", '_'; lista pública de solo lectura estática wall = new List() '#', 'D', 'W'; Lista estática pública[] [] outerWallStraight = MapHelper.DefineTilemask (cualquiera, ignorado, cualquier, muro, cualquier, muro, cualquiera, cualquier, cualquier);

Yo defino la tilemask en su forma no rotada. La lista llamada ignorado almacena caracteres que no describen nada en mi programa: espacios, que se omiten; y los guiones bajos, que uso para mostrar un índice de matriz no válido. Un mosaico en (0,0) (esquina superior izquierda) de una matriz 2D no tendrá nada en su Norte o Oeste, por ejemplo, por lo que su rango local obtiene guiones bajos allí.. algunaes una lista vacía que siempre se evalúa como una coincidencia positiva.

 Lista estática pública[] [] DefineTilemask (Lista NW, lista n, lista nE, lista w, lista centro, lista e, lista sW, lista s, lista sE) Lista[] plantilla = nueva lista[9] nW, n, nE, w, centro, e, sW, s, sE; volver nueva lista[4] [] RotateLocalRange (plantilla, 0), RotateLocalRange (plantilla, 1), RotateLocalRange (plantilla, 2), RotateLocalRange (plantilla, 3);  lista estática pública[] RotateLocalRange (Lista[] localRange, int rotaciones) List[] rotatedList = nueva lista[9] localRange [0], localRange [1], localRange [2], localRange [3], localRange [4], localRange [5], localRange [6], localRange [7], localRange [8]; para (int i = 0; i < rotations; i++)  List[] tempList = nueva lista[9] rotatedList [6], rotatedList [3], rotatedList [0], rotatedList [7], rotatedList [4], rotatedList [1], rotatedList [8], rotatedList [5], rotatedList [2]; rotatedList = tempList;  return rotatedList; 

Vale la pena explicar la implementación de este código. En DefineTilemask, Proporciono nueve listas como argumentos. Estas listas se colocan en una matriz 1D temporal y luego se giran en pasos de + 90 ° escribiendo en una nueva matriz en un orden diferente. Luego, las máscaras giradas se almacenan en una matriz irregular, cuya estructura utilizo para transmitir información de rotación. Si la máscara en el índice externo 0 coincide, entonces el azulejo se coloca sin rotación. Una coincidencia en el índice externo 1 le da a la pieza una rotación de + 90 °, y así sucesivamente.


Agarra el rango local de un azulejo

Este es sencillo. Lee el rango local del mosaico actual en una matriz de caracteres 1D, reemplazando los índices no válidos con guiones bajos..

 / * Uso: char [] localRange = GetLocalRange (plano, fila, columna); blueprint es la matriz 2D que define el edificio. fila y columna son los índices de matriz del mosaico actual que se está evaluando. * / public static char [] GetLocalRange (char [,] thisArray, int row, int column) char [] localRange = new char [9]; int localRangeCounter = 0; // Los iteradores comienzan a contar desde -1 para compensar la lectura hacia arriba y hacia la izquierda, colocando el índice solicitado en el centro. para (int i = -1; i < 2; i++)  for (int j = -1; j < 2; j++)  int tempRow = row + i; int tempColumn = column + j; if (IsIndexValid (thisArray, tempRow, tempColumn) == true)  localRange[localRangeCounter] = thisArray[tempRow, tempColumn];  else  localRange[localRangeCounter] = '_';  localRangeCounter++;   return localRange;  public static bool IsIndexValid (char[,] thisArray, int row, int column)  // Must check the number of rows at this point, or else an OutOfRange exception gets thrown when checking number of columns. if (row < thisArray.GetLowerBound (0) || row > (thisArray.GetUpperBound (0))) return false; si (columna < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) return false; si no devuelve verdadero 

Evaluar un rango local con una máscara de azulejo

Y aquí es donde ocurre la magia.! TrySpawningTile se le asigna el rango local, una máscara de azulejo, la pieza de pared que se genera si la máscara de azulejo coincide, y la fila y la columna del azulejo que se está evaluando.

Es importante destacar que el método que realiza la comparación real entre el rango local y la máscara de mosaico (TileMatchesTemplate) vuelca una rotación de máscara de azulejo tan pronto como encuentra una falta de coincidencia. No se muestra la lógica básica que define qué máscaras de mosaico usar para qué tipo de mosaico (por ejemplo, no usaría una máscara de azulejo de pared en un mueble).

 / * Uso: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, row, column); * / // Estos cuaterniones tienen una rotación de -90 a lo largo de X porque los modelos deben ser // rotados en Unity debido a los diferentes ejes hacia arriba en Blender. público estático de solo lectura Quaternion ROTATE_NONE = Quaternion.Euler (-90, 0, 0); público estático de solo lectura Quaternion ROTATE_RIGHT = Quaternion.Euler (-90, 90, 0); público estático de solo lectura Quaternion ROTATE_FLIP = Quaternion.Euler (-90, 180, 0); público estático de solo lectura Quaternion ROTATE_LEFT = Quaternion.Euler (-90, -90, 0); bool TrySpawningTile (char [] needleArray, List[] [] templateArray, GameObject tilePrefab, int row, int column) Quaternion horizontalRotation; if (TileMatchesTemplate (needleArray, templateArray, out horizontalRotation) == true) SpawnTile (tilePrefab, row, column, horizontalRotation); devuelve verdadero  else devolver falso;  public static bool TileMatchesTemplate (char [] needleArray, List[] [] tileMaskJaggedArray, out Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; para (int i = 0; i < (tileMaskJaggedArray.Length); i++)  for (int j = 0; j < 9; j++)  if (j == 4) continue; // Skip checking the centre position (no need to ascertain that a block is what it says it is). if (tileMaskJaggedArray[i][j].Count != 0)  if (tileMaskJaggedArray[i][j].Contains (needleArray[j]) == false) break;  if (j == 8) // The loop has iterated nine times without stopping, so all tiles must match.  switch (i)  case 0: horizontalRotation = ROTATE_NONE; break; case 1: horizontalRotation = ROTATE_RIGHT; break; case 2: horizontalRotation = ROTATE_FLIP; break; case 3: horizontalRotation = ROTATE_LEFT; break;  return true;    return false;  void SpawnTile (GameObject tilePrefab, int row, int column, Quaternion horizontalRotation)  Instantiate (tilePrefab, new Vector3 (column, 0, -row), horizontalRotation); 

Valoración y conclusión

Ventajas de esta implementación

  • Muy escalable; simplemente agrega más definiciones de máscaras de azulejo.
  • Los controles que realiza una máscara de azulejo pueden ser tan complejos como quieras.
  • El código es bastante transparente y su velocidad se puede mejorar realizando algunas comprobaciones adicionales en el rango local antes de comenzar a usar las máscaras de mosaico.

Desventajas de esta implementación

  • Puede ser potencialmente muy caro. En el peor de los casos tendrá (36 × norte) + 1 matriz de accesos y llamadas a List.Contains () antes de encontrar un partido, donde norte es el número de definiciones de máscara de mosaico que está buscando. Es esencial hacer lo que pueda para reducir la lista de máscaras de mosaico antes de comenzar la búsqueda.

Conclusión

Las máscaras de azulejo tienen usos no solo en la generación o estética mundial, sino también en elementos que afectan el juego. No es difícil imaginar un juego de rompecabezas en el que se puedan usar máscaras de mosaico para determinar el estado del tablero o los posibles movimientos de piezas, o las herramientas de edición podrían usar un sistema similar para unir bloques entre sí. Este artículo demostró una implementación básica de la idea, y espero que les haya resultado útil..