Patrones de procedimiento que se pueden usar con fichas

La generación de procedimientos se utiliza para aumentar la variabilidad de los juegos. Proyectos bien conocidos incluyen Minecraft , Enter the Gungeon y Descenders . En esta publicación, explicaré algunos de los algoritmos que se pueden usar al trabajar con el sistema Tilemap , que apareció como una función 2D en Unity 2017.2, y con RuleTile .

Con la creación procesal de mapas, cada juego de pase será único. Puede usar varios datos de entrada, por ejemplo, el tiempo o el nivel actual del jugador, para cambiar dinámicamente el contenido incluso después del ensamblaje del juego.

¿De qué trata esta publicación?


Veremos algunas de las formas más comunes de crear mundos de procedimientos, así como algunas variaciones que creé. Aquí hay un ejemplo de lo que puede crear después de leer el artículo. Tres algoritmos trabajan juntos para crear un mapa usando Tilemap y RuleTile :


En el proceso de generar un mapa utilizando cualquier algoritmo, obtenemos una matriz int contiene todos los datos nuevos. Puede continuar modificando estos datos o representarlos en un mapa de mosaico.

Antes de seguir leyendo, sería bueno saber lo siguiente:

  1. Distinguimos qué es un mosaico y qué no utiliza valores binarios. 1 es un mosaico, 0 es su ausencia.
  2. Almacenaremos todas las tarjetas en una matriz de enteros bidimensionales devuelta al usuario al final de cada función (a excepción de aquella en la que se realiza la representación).
  3. Usaré la función de matriz GetUpperBound () para obtener la altura y el ancho de cada mapa, de modo que la función reciba menos variables y el código sea más limpio.
  4. A menudo uso Mathf.FloorToInt () , porque el sistema de coordenadas Tilemap comienza en la parte inferior izquierda, y Mathf.FloorToInt () le permite redondear números a un número entero.
  5. Todo el código en esta publicación está escrito en C #.

Generación de matriz


GenerateArray crea una nueva matriz int del tamaño dado. También podemos indicar si la matriz debe estar llena o vacía (1 o 0). Aquí está el código:

 public static int[,] GenerateArray(int width, int height, bool empty) { int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (empty) { map[x, y] = 0; } else { map[x, y] = 1; } } } return map; } 

Representación del mapa


Esta función se usa para representar un mapa en un mapa de mosaico. Recorremos el ancho y la altura del mapa, colocando mosaicos solo cuando la matriz en el punto probado tiene un valor de 1.

 public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile) { //Clear the map (ensures we dont overlap) tilemap.ClearAllTiles(); //Loop through the width of the map for (int x = 0; x < map.GetUpperBound(0) ; x++) { //Loop through the height of the map for (int y = 0; y < map.GetUpperBound(1); y++) { // 1 = tile, 0 = no tile if (map[x, y] == 1) { tilemap.SetTile(new Vector3Int(x, y, 0), tile); } } } } 

Actualización de mapa


Esta función se usa solo para actualizar el mapa, y no para volver a renderizar. Gracias a esto, podemos usar menos recursos sin volver a dibujar cada mosaico y sus datos de mosaico.

 public static void UpdateMap(int[,] map, Tilemap tilemap) //Takes in our map and tilemap, setting null tiles where needed { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //We are only going to update the map, rather than rendering again //This is because it uses less resources to update tiles to null //As opposed to re-drawing every single tile (and collision data) if (map[x, y] == 0) { tilemap.SetTile(new Vector3Int(x, y, 0), null); } } } } 

Ruido perlin


El ruido Perlin se puede utilizar para diversos fines. Primero, podemos usarlo para crear la capa superior de nuestro mapa. Para hacer esto, solo obtenga un nuevo punto usando la posición actual x y seed.

Solución simple


Este método de generación utiliza la forma más simple de realización del ruido Perlin en la generación de niveles. Podemos tomar la función Unity para el ruido Perlin para no escribir el código nosotros mismos. También usaremos solo enteros para el mapa de mosaico, usando la función Mathf.FloorToInt () .

 public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Create the Perlin for (int x = 0; x < map.GetUpperBound(0); x++) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1)); //Make sure the noise starts near the halfway point of the height newPoint += (map.GetUpperBound(1) / 2); for (int y = newPoint; y >= 0; y--) { map[x, y] = 1; } } return map; } 

Esto es lo que parece después de renderizar en un mapa de mosaico:


Suavizado


También puede tomar esta función y suavizarla. Establezca intervalos para fijar las alturas de Perlin, y luego realice el suavizado entre estos puntos. Esta función resultará un poco más complicada, porque para intervalos debe considerar listas de valores enteros.

 public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) { //Smooth the noise and store it in the int array if (interval > 1) { int newPoint, points; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Used in the smoothing process Vector2Int currentPos, lastPos; //The corresponding points of the smoothing. One list for x and one for y List<int> noiseX = new List<int>(); List<int> noiseY = new List<int>(); //Generate the noise for (int x = 0; x < map.GetUpperBound(0); x += interval) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1)); noiseY.Add(newPoint); noiseX.Add(x); } points = noiseY.Count; 

En la primera parte de esta función, primero verificamos si el intervalo es mayor que uno. Si es así, entonces genere ruido. La generación se realiza a intervalos para que se pueda aplicar el suavizado. La siguiente parte de la función es suavizar los puntos.

 //Start at 1 so we have a previous position already for (int i = 1; i < points; i++) { //Get the current position currentPos = new Vector2Int(noiseX[i], noiseY[i]); //Also get the last position lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]); //Find the difference between the two Vector2 diff = currentPos - lastPos; //Set up what the height change value will be float heightChange = diff.y / interval; //Determine the current height float currHeight = lastPos.y; //Work our way through from the last x to the current x for (int x = lastPos.x; x < currentPos.x; x++) { for (int y = Mathf.FloorToInt(currHeight); y > 0; y--) { map[x, y] = 1; } currHeight += heightChange; } } } 

El suavizado se realiza de la siguiente manera:

  1. Obtenemos la posición actual y la última.
  2. Obtenemos la diferencia entre dos puntos, la información más importante que necesitamos es la diferencia a lo largo del eje y
  3. Luego determinamos cuánto se necesita hacer el cambio para llegar al punto, esto se hace dividiendo la diferencia en y por la variable de intervalo.
  4. Luego, comenzamos a establecer posiciones, llegando a cero
  5. Cuando lleguemos a 0 en el eje y, agregue el cambio de altura a la altura actual y repita el proceso para la siguiente posición x
  6. Terminando con cada posición entre la última y la actual, pasamos al siguiente punto

Si el intervalo es menor que uno, simplemente usamos la función anterior, que hará todo el trabajo por nosotros.

  else { //Defaults to a normal Perlin gen map = PerlinNoise(map, seed); } return map; 

Echemos un vistazo al render:


Paseo al azar


Caminata aleatoria


Este algoritmo realiza un lanzamiento de moneda. Podemos obtener uno de dos resultados. Si el resultado es "águila", entonces movemos un bloque hacia arriba, si el resultado es "colas", entonces movemos el bloque hacia abajo. Esto crea alturas al moverse constantemente hacia arriba o hacia abajo. El único inconveniente de este algoritmo es su bloqueo muy notable. Echemos un vistazo a cómo funciona.

 public static int[,] RandomWalkTop(int[,] map, float seed) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Set our starting height int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Cycle through our width for (int x = 0; x < map.GetUpperBound(0); x++) { //Flip a coin int nextMove = rand.Next(2); //If heads, and we aren't near the bottom, minus some height if (nextMove == 0 && lastHeight > 2) { lastHeight--; } //If tails, and we aren't near the top, add some height else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) { lastHeight++; } //Circle through from the lastheight to the bottom for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the map return map; } 


Random Walk Top con anti-aliasing

Tal generación nos da alturas más suaves en comparación con la generación de ruido Perlin.

Esta variación de Random Walk proporciona un resultado mucho más suave en comparación con la versión anterior. Podemos implementarlo agregando dos variables más a la función:

  • La primera variable se usa para determinar cuánto tiempo lleva mantener la altura actual. Es entero y se restablece cuando la altura cambia
  • La segunda variable se ingresa a la función y se usa como el ancho de sección mínimo para la altura. Será más claro cuando veamos la función.

Ahora sabemos qué agregar. Echemos un vistazo a la función:

 public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Determine the start position int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Used to determine which direction to go int nextMove = 0; //Used to keep track of the current sections width int sectionWidth = 0; //Work through the array width for (int x = 0; x <= map.GetUpperBound(0); x++) { //Determine the next move nextMove = rand.Next(2); //Only change the height if we have used the current height more than the minimum required section width if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth) { lastHeight--; sectionWidth = 0; } else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth) { lastHeight++; sectionWidth = 0; } //Increment the section width sectionWidth++; //Work our way from the height down to 0 for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the modified map return map; } 

Como puede ver en el gif que se muestra a continuación, suavizar el algoritmo de caminata aleatoria le permite obtener hermosos segmentos planos en el nivel.


Conclusión


Espero que este artículo lo inspire a utilizar la generación de procedimientos en sus proyectos. Si desea obtener más información sobre mapas generados por procedimientos, explore los excelentes recursos de la Wiki de Generación de Procedimientos o Roguebasin.com .

En la segunda parte del artículo, utilizaremos la generación de procedimientos para crear sistemas de cuevas.

Parte 2


Todo lo que discutiremos en esta parte se puede encontrar en este proyecto . Puede descargar activos y probar sus propios algoritmos de procedimiento.


Ruido perlin


En la parte anterior, vimos formas de aplicar el ruido Perlin para crear capas superiores. Afortunadamente, el ruido de Perlin también se puede usar para crear una cueva. Esto se realiza calculando el nuevo valor de ruido de Perlin, que recibe los parámetros de la posición actual multiplicados por el modificador. El modificador es un valor de 0 a 1. Cuanto mayor sea el valor del modificador, más caótica es la generación de Perlin. Luego redondeamos este valor al entero (0 o 1), que almacenamos en la matriz del mapa. Vea cómo se implementa esto:

 public static int[,] PerlinNoiseCave(int[,] map, float modifier, bool edgesAreWalls) { int newPoint; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (edgesAreWalls && (x == 0 || y == 0 || x == map.GetUpperBound(0) - 1 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; //Keep the edges as walls } else { //Generate a new point using Perlin noise, then round it to a value of either 0 or 1 newPoint = Mathf.RoundToInt(Mathf.PerlinNoise(x * modifier, y * modifier)); map[x, y] = newPoint; } } } return map; } 

Usamos el modificador en lugar de la semilla porque los resultados de la generación de Perlin se ven mejor cuando se multiplica por un número de 0 a 0.5. Cuanto menor sea el valor, más bloqueado será el resultado. Eche un vistazo a los resultados de la muestra. Gif comienza con un valor modificador de 0.01 y gradualmente alcanza un valor de 0.25.


De este gif se puede ver que la generación de Perlin con cada incremento simplemente aumenta el patrón.

Paseo al azar


En la parte anterior, vimos que puede usar un lanzamiento de moneda para determinar dónde se moverá la plataforma hacia arriba o hacia abajo. En esta parte, usaremos la misma idea, pero
con dos opciones adicionales para desplazamiento a izquierda y derecha. Esta variación del algoritmo Random Walk nos permite crear cuevas. Para hacer esto, seleccionamos una dirección aleatoria, luego movemos nuestra posición y eliminamos el mosaico. Continuamos este proceso hasta que alcancemos el número requerido de fichas que deben ser destruidas. Hasta ahora, usamos solo 4 direcciones: arriba, abajo, izquierda, derecha.

 public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Define our start x position int floorX = rand.Next(1, map.GetUpperBound(0) - 1); //Define our start y position int floorY = rand.Next(1, map.GetUpperBound(1) - 1); //Determine our required floorAmount int reqFloorAmount = ((map.GetUpperBound(1) * map.GetUpperBound(0)) * requiredFloorPercent) / 100; //Used for our while loop, when this reaches our reqFloorAmount we will stop tunneling int floorCount = 0; //Set our start position to not be a tile (0 = no tile, 1 = tile) map[floorX, floorY] = 0; //Increase our floor count floorCount++; 

La función comienza con lo siguiente:

  1. Encuentra la posición inicial
  2. Calcule el número de baldosas que se eliminarán.
  3. Eliminar el mosaico en la posición inicial
  4. Agregue uno al número de azulejos.

Luego pasamos al while . Él creará una cueva:

 while (floorCount < reqFloorAmount) { //Determine our next direction int randDir = rand.Next(4); switch (randDir) { //Up case 0: //Ensure that the edges are still tiles if ((floorY + 1) < map.GetUpperBound(1) - 1) { //Move the y up one floorY++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase floor count floorCount++; } } break; //Down case 1: //Ensure that the edges are still tiles if ((floorY - 1) > 1) { //Move the y down one floorY--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Right case 2: //Ensure that the edges are still tiles if ((floorX + 1) < map.GetUpperBound(0) - 1) { //Move the x to the right floorX++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Left case 3: //Ensure that the edges are still tiles if ((floorX - 1) > 1) { //Move the x to the left floorX--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; } } //Return the updated map return map; } 

Que estamos haciendo aqui


Bueno, en primer lugar, con la ayuda de un número aleatorio, elegimos en qué dirección movernos. Luego verificamos la nueva dirección con la switch case . En esta declaración, verificamos si la posición es un muro. De lo contrario, elimine el elemento con el mosaico de la matriz. Continuamos haciendo esto hasta llegar al área de piso deseada. El resultado se muestra a continuación:


También creé mi propia versión de esta función, que también incluye direcciones diagonales. El código de función es bastante largo, por lo que si desea verlo, descargue el proyecto desde el enlace al comienzo de esta parte del artículo.

Túnel direccional


Un túnel direccional comienza en un borde del mapa y llega al borde opuesto. Podemos controlar la curvatura y la rugosidad del túnel pasándolos a la función de entrada. También podemos establecer la longitud mínima y máxima de las partes del túnel. Echemos un vistazo a la implementación:

 public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) { //This value goes from its minus counterpart to its positive value, in this case with a width value of 1, the width of the tunnel is 3 int tunnelWidth = 1; //Set the start X position to the center of the tunnel int x = map.GetUpperBound(0) / 2; //Set up our random with the seed System.Random rand = new System.Random(Time.time.GetHashCode()); //Create the first part of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, 0] = 0; } 

Que esta pasando


Primero establecemos el valor del ancho. El valor del ancho pasará del valor con un menos a un positivo. Gracias a esto, obtendremos el tamaño que necesitamos. En este caso, usamos el valor 1, que, en nuestro turno, nos dará un ancho total de 3, porque usamos los valores -1, 0, 1.

Luego, establecemos la posición inicial en x, para esto tomamos el medio del ancho del mapa. Después de eso, podemos colocar un túnel en la primera parte del mapa.


Ahora vamos al resto del mapa.

  //Cycle through the array for (int y = 1; y < map.GetUpperBound(1); y++) { //Check if we can change the roughness if (rand.Next(0, 100) > roughness) { //Get the amount we will change for the width int widthChange = Random.Range(-maxPathWidth, maxPathWidth); //Add it to our tunnel width value tunnelWidth += widthChange; //Check to see we arent making the path too small if (tunnelWidth < minPathWidth) { tunnelWidth = minPathWidth; } //Check that the path width isnt over our maximum if (tunnelWidth > maxPathWidth) { tunnelWidth = maxPathWidth; } } //Check if we can change the curve if (rand.Next(0, 100) > curvyness) { //Get the amount we will change for the x position int xChange = Random.Range(-maxPathChange, maxPathChange); //Add it to our x value x += xChange; //Check we arent too close to the left side of the map if (x < maxPathWidth) { x = maxPathWidth; } //Check we arent too close to the right side of the map if (x > (map.GetUpperBound(0) - maxPathWidth)) { x = map.GetUpperBound(0) - maxPathWidth; } } //Work through the width of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, y] = 0; } } return map; } 

Generamos un número aleatorio para comparar con el valor de rugosidad, y si es más alto que este valor, entonces se puede cambiar el ancho del camino. También verificamos el valor para no hacer que el ancho sea demasiado pequeño. En la siguiente parte del código, avanzamos por el mapa. En cada etapa, ocurre lo siguiente:

  1. Generamos un nuevo número aleatorio en comparación con el valor de la curvatura. Como en la prueba anterior, si es mayor que el valor, entonces cambiamos el punto central de la ruta. También realizamos un control para no ir más allá del mapa.
  2. Finalmente, estamos colocando un túnel en la parte recién creada.

Los resultados de esta implementación se ven así:


Autómatas celulares


Los autómatas celulares usan celdas vecinas para determinar si la celda actual está encendida (1) o apagada (0). La base para determinar las celdas vecinas se crea en base a una cuadrícula de celdas generada aleatoriamente. Generaremos esta cuadrícula fuente utilizando la función C # Random.Next .

Como tenemos un par de implementaciones diferentes de autómatas celulares, escribí una función separada para generar esta cuadrícula básica. La función se ve así:

 public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) { //Seed our random number generator System.Random rand = new System.Random(seed.GetHashCode()); //Initialise the map int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //If we have the edges set to be walls, ensure the cell is set to on (1) if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; } else { //Randomly generate the grid map[x, y] = (rand.Next(0, 100) < fillPercent) ? 1 : 0; } } } return map; } 

En esta función, también puede establecer si nuestra cuadrícula necesita paredes. En todos los demás aspectos, es bastante simple. Verificamos un número aleatorio con porcentaje de relleno para determinar si la celda actual está habilitada. Echa un vistazo al resultado:


El barrio de Moore


El vecindario de Moore se utiliza para suavizar la generación inicial de autómatas celulares. El vecindario de Moore se ve así:


Las siguientes reglas se aplican al vecindario:

  • Verificamos al vecino en cada una de las direcciones.
  • Si el vecino es un mosaico activo, agregue uno al número de mosaicos circundantes.
  • Si el vecino es un mosaico inactivo, entonces no hacemos nada.
  • Si una celda tiene más de 4 mosaicos circundantes, active la celda.
  • Si la celda tiene exactamente 4 mosaicos circundantes, entonces no hacemos nada con ella.
  • Repita hasta que verifiquemos cada mosaico del mapa.

La función de verificación de vecindario de Moore es la siguiente:

 static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* Moore Neighbourhood looks like this ('T' is our tile, 'N' is our neighbours) * * NNN * NTN * NNN * */ int tileCount = 0; for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++) { for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++) { if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1)) { //We don't want to count the tile we are checking the surroundings of if(neighbourX != x || neighbourY != y) { tileCount += map[neighbourX, neighbourY]; } } } } return tileCount; } 

Después de verificar el mosaico, usamos esta información en la función de suavizado. Aquí, como en la generación de autómatas celulares, se puede indicar si los bordes del mapa deben ser muros.

 public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1))) { //Set the edge to be a wall if we have edgesAreWalls to be true map[x, y] = 1; } //The default moore rule requires more than 4 neighbours else if (surroundingTiles > 4) { map[x, y] = 1; } else if (surroundingTiles < 4) { map[x, y] = 0; } } } } //Return the modified map return map; } 

Es importante tener en cuenta aquí que la función tiene un bucle for que realiza el suavizado el número especificado de veces. Gracias a esto, se obtiene una tarjeta más hermosa.


Siempre podemos modificar este algoritmo conectando habitaciones si, por ejemplo, solo hay dos bloques entre ellas.

Barrio Von Neumann


El vecindario von Neumann es otra forma popular de implementar autómatas celulares. Para tal generación, utilizamos un vecindario más simple que en la generación Moore. El barrio se ve así:


Las siguientes reglas se aplican al vecindario:

  • Verificamos los vecinos inmediatos del mosaico, sin considerar los diagonales.
  • Si la celda está activa, agregue uno a la cantidad.
  • Si la celda está inactiva, no haga nada.
  • Si la celda tiene más de 2 vecinos, entonces activamos la celda actual.
  • Si la celda tiene menos de 2 vecinos, entonces hacemos que la celda actual esté inactiva.
  • Si hay exactamente 2 vecinos, no cambie la celda actual.

El segundo resultado utiliza los mismos principios que el primero, pero expande el área del vecindario.

Verificamos a los vecinos con la siguiente función:

 static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* von Neumann Neighbourhood looks like this ('T' is our Tile, 'N' is our Neighbour) * * N * NTN * N * */ int tileCount = 0; //Keep the edges as walls if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1))) { tileCount++; } //Ensure we aren't touching the left side of the map if(x - 1 > 0) { tileCount += map[x - 1, y]; } //Ensure we aren't touching the bottom of the map if(y - 1 > 0) { tileCount += map[x, y - 1]; } //Ensure we aren't touching the right side of the map if(x + 1 < map.GetUpperBound(0)) { tileCount += map[x + 1, y]; } //Ensure we aren't touching the top of the map if(y + 1 < map.GetUpperBound(1)) { tileCount += map[x, y + 1]; } return tileCount; } 

Habiendo recibido el número de vecinos, podemos proceder a suavizar la matriz. Como antes, necesitamos un bucle forpara completar el número de iteraciones de suavizado que se pasan a la entrada.

 public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //Get the surrounding tiles int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1))) { //Keep our edges as walls map[x, y] = 1; } //von Neuemann Neighbourhood requires only 3 or more surrounding tiles to be changed to a tile else if (surroundingTiles > 2) { map[x, y] = 1; } else if (surroundingTiles < 2) { map[x, y] = 0; } } } } //Return the modified map return map; } 

Como puede ver a continuación, el resultado final es mucho más bloqueado que el vecindario de Moore:


Aquí, como en las cercanías de Moore, puede ejecutar un script adicional para optimizar las conexiones entre las partes del mapa.

Conclusión


Espero que este artículo lo inspire a utilizar algún tipo de generación de procedimientos en sus proyectos. Si no ha descargado el proyecto, puede obtenerlo aquí .

Source: https://habr.com/ru/post/471084/


All Articles