Generador de cuevas bidimensional aleatorio

Prólogo


Si eres demasiado perezoso para ocuparte de tu tiempo, logrando un nivel para tu juego, entonces has venido al lugar correcto.

Este artículo le explicará en detalle cómo puede utilizar uno de los muchos otros métodos de generación utilizando el ejemplo de las tierras altas y las cuevas. Consideraremos el algoritmo Aldous-Broder y cómo hacer que la cueva generada sea más bella.

Al final de leer el artículo, debería obtener algo como esto:

Resumen


Teoría


La montaña


Para ser sincero, la cueva se puede generar desde cero, pero ¿será de alguna manera fea? En el papel de "plataforma" para la colocación de minas, elegí una cadena montañosa.
Esta montaña se genera de manera bastante simple: tengamos una matriz bidimensional y una altura variable , inicialmente igual a la mitad de la longitud de la matriz en la segunda dimensión; simplemente revisamos sus columnas y rellenamos algo con todas las líneas en la columna a un valor de altura variable , cambiándolo con una probabilidad aleatoria hacia arriba o hacia abajo.

Cueva


Para generar las mazmorras, elegí, como me pareció , un excelente algoritmo. En términos simples, se puede explicar de la siguiente manera: tengamos dos (tal vez diez) variables X e Y , y una matriz bidimensional de 50 por 50, le damos a estas variables valores aleatorios dentro de nuestra matriz, por ejemplo, X = 26 e Y = 28 . Después de eso, hacemos las mismas acciones varias veces: obtenemos un número aleatorio de cero a

ú

, en nuestro caso, hasta cuatro ; y luego dependiendo del número que se retiró
nuestras variables:

switch (Random.Range(0, 4)) { case 0: X += 1; break; case 1: X -= 1; break; case 2: Y += 1; break; case 3: Y -= 1; break; } 

Luego, por supuesto, verificamos si alguna variable ha caído fuera de los límites de nuestro campo:

  X = X < 0 ? 0 : (X >= 50 ? 49 : X); Y = Y < 0 ? 0 : (Y >= 50 ? 49 : Y); 

Después de todas estas comprobaciones, hacemos algo en los nuevos valores X e Y para nuestra matriz (por ejemplo: agregar uno al elemento) .

 array[X, Y] += 1; 

Preparación


Para simplificar la implementación y la visualización de nuestros métodos, ¿dibujaremos los objetos resultantes? ¡Estoy tan contento de que no te importe! Haremos esto con Texture2D .

Para trabajar, solo necesitamos dos scripts:
ground_libray es sobre lo que girará el artículo. Aquí generamos, limpiamos y dibujamos
ground_generator es lo que usará nuestro ground_libray
Deje que el primero sea estático y no herede de nada:

 public static class ground_libray 

Y lo segundo es normal, solo que no necesitaremos el método de actualización .

Además, creemos un objeto de juego en el escenario, con el componente SpriteRenderer

Parte práctica


¿En que consiste?


Para trabajar con datos, utilizaremos una matriz bidimensional. Puede tomar una variedad de diferentes tipos, desde byte o int , hasta Color , pero creo que esto se hará mejor:

Nuevo tipo
Escribimos esto en ground_libray .

 [System.Serializable] public class block { public float[] color = new float[3]; public block(Color col) { color = new float[3] { col.r, col.g, col.b }; } } 


Explicaré esto por el hecho de que nos permitirá guardar nuestra matriz y modificarla si es necesario.

Macizo


Antes de comenzar a generar la montaña, designemos el lugar donde la almacenaremos .

En el script ground_generator, escribí esto:

  public int ground_size = 128; ground_libray.block[,] ground; Texture2D myT; 

ground_size : el tamaño de nuestro campo (es decir, la matriz constará de 16384 elementos).
ground_libray.block [,] ground - este es nuestro campo para la generación.
Texture2D myT es en lo que vamos a recurrir.

¿Cómo va a funcionar?
El principio de trabajar con nosotros será el siguiente : llamaremos a algunos métodos ground_libray desde ground_generator , dando al primero nuestro campo de tierra .

Creemos el primer método en el script ground_libray:

Hacer montaña
  public static float mount_noise = 0.02f; public static void generate_mount(ref block[,] b) { int h_now = b.GetLength(1) / 2; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < h_now; y++) { b[x, y] = new block(new Color(0.7f, 0.4f, 0)); h_now += Random.value > (1.0f - mount_noise) ? (Random.value > 0.5 ? 1 : -1) : 0; } } 

Y de inmediato trataremos de entender lo que está sucediendo aquí: como dije, simplemente pasamos por las columnas de nuestra matriz b , al mismo tiempo que cambiamos la variable de altura h_now , que originalmente era igual a la mitad 128 (64) . Pero todavía hay algo nuevo: mount_noise . Esta variable es responsable de la posibilidad de cambiar h_now , porque si cambia la altura muy a menudo, la montaña se verá como un peine .

Color
Inmediatamente configuré un color ligeramente marrón , deje que sea al menos un poco; en el futuro no lo necesitaremos.

Ahora vayamos a ground_generator y escribamos esto en el método de Inicio :

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); 

Inicializamos el terreno variable una vez que debe hacerse .
Después, sin explicación, envíelo a ground_libray .
Entonces generamos la montaña.

¿Por qué no puedo ver mi montaña?


¡Ahora dibujemos lo que tenemos!

Para dibujar, escribiremos el siguiente método en nuestro ground_libray :

Dibujo
  public static void paint(block[,] b, ref Texture2D t) { t = new Texture2D(b.GetLength(0), b.GetLength(1)); t.filterMode = FilterMode.Point; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) { t.SetPixel(x, y, new Color(0, 0, 0, 0)); continue; } t.SetPixel(x, y, new Color( b[x, y].color[0], b[x, y].color[1], b[x, y].color[2] ) ); } t.Apply(); } 

Aquí ya no le daremos a alguien nuestro campo, solo le daremos una copia del mismo (aunque, debido a la palabra clase, le dimos un poco más que una copia) . También le daremos nuestro Texture2D a este método.

Las dos primeras líneas: creamos nuestra textura del tamaño del campo y eliminamos el filtrado .

Después de lo cual pasamos por todo nuestro campo de matriz y donde no creamos nada (la clase necesita ser inicializada) , dibujamos un cuadro vacío, de lo contrario, si no está vacío, dibujamos lo que guardamos en el elemento.

Y, por supuesto, cuando haya terminado, vamos a ground_generator y agregamos esto:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); //   ground_libray.paint(ground, ref myT); GetComponent<SpriteRenderer>().sprite = Sprite.Create(myT, new Rect(0, 0, ground_size, ground_size), Vector3.zero ); 

Pero no importa cuánto dibujemos en nuestra textura, en el juego solo podemos verlo colocando este lienzo en algo:

SpriteRenderer no acepta Texture2D en ninguna parte , pero nada nos impide crear un sprite a partir de esta textura: Sprite.Create ( textura , rectángulo con las coordenadas de la esquina inferior izquierda y superior derecha , la coordenada del eje ).

Estas líneas se llamarán las últimas, ¡agregaremos el resto por encima del método de pintura !

La mia


Ahora necesitamos llenar nuestros campos con cuevas aleatorias. Para tales acciones, también crearemos un método separado en ground_libray . Me gustaría explicar de inmediato los parámetros del método:

 ref block[,] b -     . int thick -    int size -         Color outLine -   

Cueva
  public static void make_cave(ref block[,] b, int thick, int size, Color outLine) { int xNow = Random.Range(0, b.GetLength(0)); int yNow = Random.Range(0, b.GetLength(1) / 2); for (int i = 0; i < size; i++) { b[xNow, yNow] = null; make_thick(ref b, thick, new int[2] { xNow, yNow }, outLine); switch (Random.Range(0, 4)) { case 0: xNow += 1; break; case 1: xNow -= 1; break; case 2: yNow += 1; break; case 3: yNow -= 1; break; } xNow = xNow < 0 ? 0 : (xNow >= b.GetLength(0) ? b.GetLength(0) - 1 : xNow); yNow = yNow < 0 ? 0 : (yNow >= b.GetLength(1) ? b.GetLength(1) - 1 : yNow); } } 

Para empezar, declaramos nuestras variables X e Y , pero las llamé xNow e yNow, respectivamente.

El primero, es decir , xNow , obtiene un valor aleatorio de cero al tamaño del campo en la primera dimensión.

Y el segundo, yNow , también obtiene un valor aleatorio: de cero a la mitad del campo en la segunda dimensión. Por qué Generamos nuestra montaña desde el medio, la posibilidad de que crezca hasta el "techo" no es grande . En base a esto, no considero relevante generar cuevas en el aire.

Después de eso, se inicia un bucle de inmediato, cuyo número depende del parámetro de tamaño . Cada vez que actualizamos el campo en las posiciones xNow e yNow , solo entonces las actualizamos nosotros mismos (las actualizaciones de campo se pueden poner al final, no sentirá la diferencia)

También hay un método make_thick , en cuyos parámetros pasamos nuestro campo , el ancho del trazo de la cueva , la posición actual de actualización de la cueva y el color del trazo :

Accidente cerebrovascular
  static void make_thick (ref block[,] b, int t, int[] start, Color o) { for (int x = (start[0] - t); x < (start[0] + t); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - t); y < (start[1] + t); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) continue; b[x, y] = new block(o); } } } 

El método toma la coordenada de inicio que se le pasa y, a su alrededor, a distancia t repinta todos los bloques en color o , ¡todo es muy simple!


Ahora agreguemos esta línea a nuestro generador_terreno :

 ground_libray.make_cave(ref ground, 2, 10000, new Color(0.3f, 0.3f, 0.3f)); 

¡Puede instalar el script ground_generator como componente en nuestro objeto y comprobar cómo funciona!



Más sobre las cuevas ...
  • Para hacer más cuevas, puede llamar al método make_cave varias veces (use un bucle)
  • Cambiar el parámetro de tamaño no siempre aumenta el tamaño de la cueva, pero a menudo se hace más grande
  • Al cambiar el parámetro grueso , aumenta significativamente el número de operaciones:
    si el parámetro es 3, entonces el número de cuadrados en un radio de 3 será 36 , entonces con el tamaño del parámetro = 40,000 , el número de operaciones será 36 * 40,000 = 1440000


Corrección de cueva




¿Has notado que en esta vista la cueva no se ve mejor? Demasiados detalles adicionales (tal vez pienses de manera diferente) .

Para deshacerse de las inclusiones de algunos # 4d4d4d , escribiremos este método en ground_libray :

Limpiador
  public static void clear_caves(ref block[,] b) { for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) continue; if (solo(b, 2, 13, new int[2] { x, y })) b[x, y] = null; } } 

Pero será difícil entender lo que está sucediendo aquí si no sabes lo que hace la función solo :

  static bool solo (block[,] b, int rad, int min, int[] start) { int cnt = 0; for (int x = (start[0] - rad); x <= (start[0] + rad); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - rad); y <= (start[1] + rad); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) cnt += 1; else continue; if (cnt >= min) return true; } } return false; } 

En los parámetros de esta función, nuestro campo , el radio de la verificación del punto , el "umbral de destrucción" y las coordenadas del punto a verificar deben estar presentes.
Aquí hay una explicación detallada de lo que hace esta función:
int cnt es el contador del "umbral" actual
A continuación hay dos ciclos que verifican todos los puntos alrededor de aquel cuyas coordenadas se pasan para comenzar . Si hay un punto vacío , entonces agregamos uno al cnt , al alcanzar el "umbral de destrucción" devolvemos la verdad: el punto es superfluo . De lo contrario, no la tocamos.

Establecí el umbral de destrucción en 13 puntos vacíos, y el radio de verificación es 2 (es decir, verificará 24 puntos, sin incluir el central)
Ejemplo
Éste permanecerá ileso, ya que solo hay 9 puntos vacíos.



Pero este no tuvo suerte: alrededor de 14 puntos vacíos



Una breve descripción del algoritmo: revisamos todo el campo y verificamos todos los puntos para ver si son necesarios.

A continuación, simplemente agregamos la siguiente línea a nuestro generador_terreno :

 ground_libray.clear_caves(ref ground); 

Resumen


Como podemos ver, la mayoría de las partículas innecesarias simplemente desaparecieron.

Añade un poco de color


Nuestra montaña se ve muy monótona, me parece aburrida.

Agreguemos un poco de color. Agregue el método level_paint a ground_libray :

Pintando sobre las montañas
  public static void level_paint(ref block[,] b, Color[] all_c) { for (int x = 0; x < b.GetLength(0); x++) { int lvl_div = -1; int counter = 0; int lvl_now = 0; for (int y = b.GetLength(1) - 1; y > 0; y--) { if (b[x, y] != null && lvl_div == -1) lvl_div = y / all_c.Length; else if (b[x, y] == null) continue; b[x, y] = new block(all_c[lvl_now]); lvl_now += counter >= lvl_div ? 1 : 0; lvl_now = (lvl_now >= all_c.Length) ? (all_c.Length - 1) : lvl_now; counter = counter >= lvl_div ? 0 : (counter += 1); } } } </ <cut />source>           .    ,       ,   .       ,      .          <b>Y </b>  ,      . </spoiler>     <b>ground_generator </b> : <source lang="cs"> ground_libray.level_paint(ref ground, new Color[3] { new Color(0.2f, 0.8f, 0), new Color(0.6f, 0.2f, 0.05f), new Color(0.2f, 0.2f, 0.2f), }); 

Elegí solo 3 colores: verde , rojo oscuro y gris oscuro .
Por supuesto, puede cambiar tanto el número de colores como los valores de cada uno. Resultó así:

Resumen


Pero aún así parece demasiado estricto agregar un poco de aleatoriedad a los colores, escribiremos esta propiedad en ground_libray :

Colores al azar
  public static float color_randomize = 0.1f; static float crnd { get { return Random.Range(1.0f - color_randomize, 1.0f + color_randomize); } } 

Y ahora en los métodos level_paint y make_thick , en las líneas donde asignamos colores, por ejemplo en make_thick :

 b[x, y] = new block(o); 

Escribiremos esto:

 b[x, y] = new block(o * crnd); 

Y en level_paint

 b[x, y] = new block(all_c[lvl_now] * crnd); 


Al final, todo debería verse así:

Resumen



Desventajas


Supongamos que tenemos un campo de 1024 por 1024, necesitamos generar 24 cuevas, cuyo grosor será de 4, y el tamaño es de 80,000.

1024 * 1024 + 24 * 64 * 80,000 = 5,368,832,000,000 operaciones.

Este método solo es adecuado para generar pequeños módulos para el mundo del juego, es imposible generar algo muy grande a la vez .

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


All Articles