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:
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
SpriteRendererParte 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 tipoEscribimos 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 .
ColorInmediatamente 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);
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);
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í:
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í:
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 .