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..
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).
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.
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).
Y a veces encontrará que incluso los arreglos de máscara de mosaico realmente simples pueden ser duplicados y no superponibles (Figura 4).
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.
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).
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.
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).
Con los conceptos básicos explicados, podemos describir qué debe hacer el código:
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).
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 lecturaany = 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í.. alguna
es 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.
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
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);
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.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..