Uno de los artículos más populares en mi sitio está dedicado a la
generación de mapas poligonales (
traducción en Habré). Crear esas tarjetas requiere mucho esfuerzo. Pero no comencé con esto, sino con una tarea
mucho más simple, que describiré aquí. Esta técnica simple le permite crear esas tarjetas en menos de 50 líneas de código:
No explicaré cómo
dibujar esas tarjetas: depende del idioma, la biblioteca de gráficos, la plataforma, etc. Simplemente explicaré cómo
llenar la matriz con datos del mapa.
El ruido
Una forma estándar de generar mapas 2D es utilizar el ruido con una banda de frecuencia limitada como bloque de construcción, como el ruido Perlin o el ruido simplex. Así es como se ve la función de ruido:
Asignamos un número de 0.0 a 1.0 a cada punto en el mapa. En esta imagen, 0.0 es negro y 1.0 es blanco.
Aquí se explica cómo configurar el color de cada punto de la cuadrícula en la sintaxis de un lenguaje tipo C:
for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } }
El bucle funcionará de la misma manera en Javascript, Python, Haxe, C ++, C #, Java y la mayoría de los otros lenguajes populares, por lo que lo mostraré en una sintaxis tipo C para que pueda convertirlo al idioma que necesita. En el resto del tutorial, mostraré cómo cambia el cuerpo del bucle (
value[y][x]=…
línea
value[y][x]=…
) al agregar nuevas funciones. La demostración mostrará un ejemplo completo.
En algunas bibliotecas, será necesario cambiar o multiplicar los valores resultantes para devolverlos en el rango de 0.0 a 1.0.
Altura
El ruido en sí mismo es solo una colección de números. Necesitamos darle
significado . Lo primero en lo que puede pensar es vincular el valor de ruido a la altura (esto se llama un "mapa de altura"). Tomemos el ruido que se muestra arriba y dibujémoslo como altura:
El código se mantuvo casi igual, con la excepción del bucle interno. Ahora se ve así:
elevation[y][x] = noise(nx, ny);
Sí, y eso es todo. Los datos del mapa permanecieron igual, pero ahora los llamaré
elevation
(altura), no
value
.
Tenemos muchas colinas, pero nada más. Que esta mal
Frecuencia
Se puede generar ruido a cualquier
frecuencia . Hasta ahora he elegido solo una frecuencia. Veamos cómo afecta.
Intente cambiar el valor con el control deslizante (en el artículo original) y vea qué sucede a diferentes frecuencias:
Simplemente cambia la escala. Al principio esto no parece muy útil, pero no lo es. Tengo un
tutorial más (
traducción en Habré), que explica la
teoría : conceptos como frecuencia, amplitud, octavas, ruido rosa y azul, etc.
elevation[y][x] = noise(freq * nx, freq * ny);
A veces también es útil recordar la
longitud de onda , que es el recíproco de la magnitud. Cuando la frecuencia se duplica, el tamaño solo se reduce a la mitad. Doblar la longitud de onda todos los dobles. La longitud de onda es la distancia medida en píxeles / mosaicos / metros o cualquier otra unidad que haya seleccionado para los mapas. Está relacionado con la frecuencia:
wavelength = map_size / frequency
.
Octavas
Para hacer que el mapa de altura sea más interesante,
agregaremos ruido con diferentes frecuencias :
elevation[y][x] = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 2 * ny);
Mezclemos grandes colinas de baja frecuencia con pequeñas colinas de alta frecuencia en un mapa.
Mueva el control deslizante (en el artículo original) para agregar pequeñas colinas a la mezcla:
¡Ahora se parece mucho más al alivio fractal que necesitamos! Podemos obtener colinas y montañas escarpadas, pero aún no tenemos llanuras planas. Para hacer esto, necesitas algo más.
Redistribución
La función de ruido nos da valores entre 0 y 1 (o de -1 a +1, dependiendo de la biblioteca). Para crear llanuras planas, podemos
elevar la altura a una potencia .
Mueva el control deslizante (en el artículo original) para obtener diferentes grados.
e = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 4 * ny); elevation[y][x] = Math.pow(e, exponent);
Los valores altos
reducen las alturas promedio a las llanuras , y los valores bajos elevan las alturas promedio hacia los picos de las montañas. Necesitamos omitirlos. Uso las funciones de potencia porque son más simples, pero puedes usar cualquier curva; Tengo una
demo más complicada.
Ahora que tenemos un mapa de elevación realista, ¡agreguemos biomas!
Biomas
El ruido da números, pero necesitamos un mapa con bosques, desiertos y océanos. Lo primero que puede hacer es convertir pequeñas alturas en agua:
function biome(e) { if (e < waterlevel) return WATER; else return LAND; }
¡Vaya, esto ya se está convirtiendo en un mundo generado por procedimientos! Tenemos agua, hierba y nieve. Pero, ¿y si necesitamos más? Hagamos una secuencia de agua, arena, hierba, bosque, sabana, desierto y nieve:
Alivio basado en la altura function biome(e) { if (e < 0.1) return WATER; else if (e < 0.2) return BEACH; else if (e < 0.3) return FOREST; else if (e < 0.5) return JUNGLE; else if (e < 0.7) return SAVANNAH; else if (e < 0.9) return DESERT; else return SNOW; }
Wow, eso se ve genial! Para tu juego, puedes cambiar los valores y los biomas. Crysis tendrá mucha más jungla; Skyrim tiene mucho más hielo y nieve. Pero no importa cómo cambie los números, este enfoque es bastante limitado. Los tipos de relieve corresponden a las alturas, por lo tanto, forman tiras. Para hacerlos más interesantes, necesitamos elegir biomas basados en otra cosa. Creemos un
segundo mapa de ruido para la humedad.
Arriba está el ruido de las alturas; fondo - ruido de humedadAhora usemos altura y humedad
juntos . En la primera imagen que se muestra a continuación, el eje y es la altura (tomada de la imagen de arriba) y el eje x es la humedad (la segunda imagen es más alta). Esto nos da un mapa convincente:
Alivio basado en dos valores de ruidoLas pequeñas alturas son océanos y costas. Grandes alturas son rocosas y nevadas. En el medio, obtenemos una amplia gama de biomas. El código se ve así:
function biome(e, m) { if (e < 0.1) return OCEAN; if (e < 0.12) return BEACH; if (e > 0.8) { if (m < 0.1) return SCORCHED; if (m < 0.2) return BARE; if (m < 0.5) return TUNDRA; return SNOW; } if (e > 0.6) { if (m < 0.33) return TEMPERATE_DESERT; if (m < 0.66) return SHRUBLAND; return TAIGA; } if (e > 0.3) { if (m < 0.16) return TEMPERATE_DESERT; if (m < 0.50) return GRASSLAND; if (m < 0.83) return TEMPERATE_DECIDUOUS_FOREST; return TEMPERATE_RAIN_FOREST; } if (m < 0.16) return SUBTROPICAL_DESERT; if (m < 0.33) return GRASSLAND; if (m < 0.66) return TROPICAL_SEASONAL_FOREST; return TROPICAL_RAIN_FOREST; }
Si es necesario, puedes cambiar todos estos valores de acuerdo con los requisitos de tu juego.
Si no necesitamos biomas, los gradientes suaves (vea
este artículo ) pueden crear colores:
Tanto para los biomas como para los gradientes, un solo valor de ruido no proporciona suficiente variabilidad, pero dos es suficiente.
Clima
En la sección anterior, usé la
altitud como un sustituto de la
temperatura . Cuanto mayor sea la altura, menor será la temperatura. Sin embargo, la latitud geográfica también afecta las temperaturas. Usemos la altura y la latitud para controlar la temperatura:
Cerca de los polos (grandes latitudes) el clima es más frío, y en las cimas de las montañas (grandes alturas) el clima también es más frío. Hasta ahora no he trabajado mucho: para el enfoque correcto de estos parámetros, necesita una gran cantidad de configuraciones sutiles.
También hay cambio climático
estacional . En verano e invierno, los hemisferios norte y sur se vuelven más cálidos y fríos, pero en el ecuador la situación no cambia mucho. Aquí también se puede hacer mucho, por ejemplo, uno puede simular los vientos y las corrientes oceánicas predominantes, el efecto de los biomas en el clima y el efecto promedio de los océanos en las temperaturas.
Las islas
En algunos proyectos, necesitaba que los bordes del mapa fueran agua. Esto convierte el mundo en una o más islas. Hay muchas maneras de hacer esto, pero utilicé una solución bastante simple en mi generador de mapas poligonales: cambié la altura como
e = e + a - b*d^c
, donde
d
es la distancia desde el centro (en una escala de 0-1). Otra opción es cambiar
e = (e + a) * (1 - b*d^c)
. La constante
a
eleva todo hacia arriba,
b
baja los bordes hacia abajo
c
controla la tasa de disminución.
No estoy completamente satisfecho con esto y queda mucho por explorar. ¿Debería ser Manhattan o la distancia euclidiana? ¿Debería depender de la distancia al centro o de la distancia al borde? ¿Debería la distancia ser cuadrada, lineal o tener algún otro grado? ¿Debería ser suma / resta, o multiplicación / división, o algo más? En el artículo original,
intente Agregar, a = 0.1, b = 0.3, c = 2.0, o
intente Multiplicar, a = 0.05, b = 1.00, c = 1.5. Las opciones que más le convengan dependen de su proyecto.
¿Por qué apegarse a las funciones matemáticas estándar? Como dije en mi
artículo sobre el daño en RPG (
traducción en Habré), todos (incluyéndome a mí) usan funciones matemáticas, como polinomios, distribuciones exponenciales, etc., pero en la computadora no podemos limitarnos a ellas. Podemos tomar
cualquier función de formación y usarla aquí, usando la tabla de búsqueda
e = e + height_adjust[d]
. Hasta ahora no he estudiado este tema.
Ruido de punta
En lugar de elevar la altura a una potencia, podemos usar el valor absoluto para crear picos agudos:
function ridgenoise(nx, ny) { return 2 * (0.5 - abs(0.5 - noise(nx, ny))); }
Para agregar octavas, podemos variar las amplitudes de las frecuencias altas para que solo las montañas reciban el ruido adicional:
e0 = 1 * ridgenoise(1 * nx, 1 * ny); e1 = 0.5 * ridgenoise(2 * nx, 2 * ny) * e0; e2 = 0.25 * ridgenoise(4 * nx, 4 * ny) * (e0+e1); e = e0 + e1 + e2; elevation[y][x] = Math.pow(e, exponent);
No tengo mucha experiencia con esta técnica, así que necesito experimentar para aprender a usarla bien. También puede ser interesante mezclar ruido puntiagudo de baja frecuencia con ruido no puntiagudo de alta frecuencia.
Terrazas
Si redondeamos la altura a los siguientes n niveles, obtenemos terrazas:
Este es el resultado de aplicar la función de redistribución de altura en la forma
e = f(e)
. Arriba, usamos
e = Math.pow(e, exponent)
para agudizar los picos de las montañas; aquí usamos
e = Math.round(e * n) / n
para crear terrazas. Si utiliza una función que no sea de paso, las terrazas se pueden redondear o aparecer solo a ciertas alturas.
Colocación de árboles
Usualmente usamos ruido fractal para la altura y la humedad, pero también se puede usar para colocar objetos espaciados de manera desigual, como árboles y piedras. Para la altura usamos amplitudes altas con frecuencias bajas ("ruido rojo"). Para colocar objetos necesita usar amplitudes altas con frecuencias altas ("ruido azul"). A la izquierda hay un patrón de ruido azul; a la derecha hay lugares donde el ruido es mayor que los valores adyacentes:
for (int yc = 0; yc < height; yc++) { for (int xc = 0; xc < width; xc++) { double max = 0;
Al elegir diferentes R para cada bioma, podemos obtener una densidad variable de árboles:
Es genial que ese ruido se pueda usar para colocar árboles, pero otros algoritmos a menudo son más efectivos y crean una distribución más uniforme: manchas Poisson, mosaicos Van o tramado gráfico.
Hasta el infinito y más allá
Los cálculos del bioma en la posición (x, y) son independientes de los cálculos de todas las demás posiciones. Este
cálculo local tiene dos propiedades convenientes: se puede calcular en paralelo y se puede usar para terrenos infinitos.
Coloque el cursor del mouse en el minimapa (en el artículo original) a la izquierda para generar el mapa a la derecha. Puede generar cualquier parte de la tarjeta sin generar (e incluso sin almacenar) la tarjeta completa.


Implementación
El uso de ruido para generar terreno es una solución popular, y en Internet puede encontrar tutoriales para muchos idiomas y plataformas diferentes. El código para generar tarjetas en diferentes idiomas es aproximadamente el mismo. Aquí está el bucle más simple en tres idiomas diferentes:
- Javascript:
let gen = new SimplexNoise(); function noise(nx, ny) {
- C ++:
module::Perlin gen; double noise(double nx, double ny) {
- Python:
from opensimplex import OpenSimplex gen = OpenSimplex() def noise(nx, ny):
Todas las bibliotecas de ruido son muy parecidas. Pruebe
opensimplex para Python , o
libnoise para C ++ , o
simplex-noise para Javascript. Para los idiomas más populares, hay muchas bibliotecas de ruido. O puede aprender cómo funciona el ruido Perlin o darse cuenta usted mismo del ruido.
No lo hiceEn diferentes bibliotecas de ruido para su idioma, los detalles de la aplicación pueden variar ligeramente (algunos números de retorno en el rango de 0.0 a 1.0, otros en el rango de -1.0 a +1.0), pero la idea básica es la misma. Para un proyecto real, es posible que necesite ajustar la función de
noise
y el objeto
gen
en una clase, pero estos detalles son irrelevantes, por lo que los hice globales.
Para un proyecto tan simple, no importa qué ruido use: ruido Perlin, ruido simplex, ruido OpenSimplex, ruido de valor, compensación de punto medio, algoritmo de diamante o la transformación inversa de Fourier. Cada uno de ellos tiene sus pros y sus contras, pero para un generador de tarjetas similar, todos crean más o menos los mismos valores de salida.
La representación del mapa depende de la plataforma y el juego, por lo que no lo implementé; Este código solo es necesario para generar alturas y biomas, cuya representación depende del estilo utilizado en el juego. Puede copiar, portar y usarlo en sus proyectos.
Los experimentos
Observé mezclar octavas, elevar grados a una potencia y combinar alturas con humedad para crear un bioma.
Aquí puede estudiar un gráfico interactivo que le permite experimentar con todos estos parámetros, que muestra en qué consiste el código:
Aquí hay un código de muestra:
var rng1 = PM_PRNG.create(seed1); var rng2 = PM_PRNG.create(seed2); var gen1 = new SimplexNoise(rng1.nextDouble.bind(rng1)); var gen2 = new SimplexNoise(rng2.nextDouble.bind(rng2)); function noise1(nx, ny) { return gen1.noise2D(nx, ny)/2 + 0.5; } function noise2(nx, ny) { return gen2.noise2D(nx, ny)/2 + 0.5; } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var nx = x/width - 0.5, ny = y/height - 0.5; var e = (1.00 * noise1( 1 * nx, 1 * ny) + 0.50 * noise1( 2 * nx, 2 * ny) + 0.25 * noise1( 4 * nx, 4 * ny) + 0.13 * noise1( 8 * nx, 8 * ny) + 0.06 * noise1(16 * nx, 16 * ny) + 0.03 * noise1(32 * nx, 32 * ny)); e /= (1.00+0.50+0.25+0.13+0.06+0.03); e = Math.pow(e, 5.00); var m = (1.00 * noise2( 1 * nx, 1 * ny) + 0.75 * noise2( 2 * nx, 2 * ny) + 0.33 * noise2( 4 * nx, 4 * ny) + 0.33 * noise2( 8 * nx, 8 * ny) + 0.33 * noise2(16 * nx, 16 * ny) + 0.50 * noise2(32 * nx, 32 * ny)); m /= (1.00+0.75+0.33+0.33+0.33+0.50); } }
Hay una dificultad: para el ruido de las alturas y la humedad, es necesario usar una semilla diferente, de lo contrario resultarán iguales, y las tarjetas no se verán tan interesantes. En Javascript, uso
la biblioteca prng-parkmiller ; en C ++, puede usar dos
objetos independientes de
linear_congruential_engine ; en Python, puede crear dos instancias separadas de una
clase random.Random .
Pensamientos
Me gusta este enfoque para la generación de mapas por su
simplicidad . Es rápido y requiere muy poco código para producir resultados decentes.
No me gustan sus limitaciones en este enfoque. Los cálculos locales significan que cada punto es independiente de todos los demás. Las diferentes áreas del mapa
no están
conectadas entre sí . Cada lugar en el mapa "parece" lo mismo. No hay restricciones globales, por ejemplo, "debería haber de 3 a 5 lagos en el mapa" o características globales, como un río que fluye desde la cima del pico más alto hacia el océano. Además, no me gusta el hecho de que para obtener una buena imagen es necesario configurar los parámetros durante mucho tiempo.
¿Por qué lo recomiendo? Creo que este es un buen punto de partida, especialmente para juegos independientes y jams de juegos. Dos de mis amigos escribieron la versión inicial de
Realm of the Mad God en solo 30 días para un
concurso de juegos . Me pidieron que ayudara a crear mapas. Utilicé esta técnica (además de algunas características más que resultaron no ser muy útiles) e hice un mapa para ellas. Unos meses más tarde, después de recibir comentarios de los jugadores y estudiar cuidadosamente el diseño del juego, creamos un generador de mapas más avanzado basado en polígonos Voronoi, descrito
aquí (
traducción en Habré). Este generador de tarjetas no utiliza las técnicas descritas en este artículo. Utiliza el ruido para crear mapas de una manera completamente diferente.
Información adicional
Hay
muchas cosas interesantes que puedes hacer con las funciones de ruido. Si busca en Internet, puede encontrar opciones tales como turbulencia, ondulación, multifractal acanalado, amortiguación de amplitud, en terrazas, ruido voronoi, derivados analíticos, deformación de dominios y otros. Puede usar
esta página como fuente de inspiración. No los considero aquí; mi artículo se centra en la simplicidad.
Este proyecto fue influenciado por mis proyectos de generación de mapas anteriores:
- Usé el ruido general de Perlin para mi primer generador de cartas del Reino del Dios Loco . Lo usamos durante los primeros seis meses de prueba alfa, y luego lo reemplazamos con un generador de mapas en polígonos Voronoi , especialmente creado para los requisitos de juego que determinamos durante la prueba alfa. Los biomas y sus colores para el artículo se toman de estos proyectos.
- Al estudiar el procesamiento de señales de audio, escribí un tutorial de ruido que explica conceptos como frecuencia, amplitud, octavas y el "color" del ruido. Los mismos conceptos que funcionan para el sonido también se aplican a la generación de tarjetas basadas en ruido. En ese momento, creé una generación de socorro de demostración en bruto, pero no los terminé.
- A veces experimento para encontrar límites. Quería saber cuánto código se necesita mínimamente para crear mapas atractivos. En este mini proyecto, llegué a cero líneas de código: todo se hace con filtros de imagen (turbulencia, umbrales, gradientes de color). Esto me hizo feliz y triste. ¿En qué medida se puede realizar la generación de mapas mediante filtros de imagen? En lo suficientemente grande. Todo lo descrito anteriormente sobre el "esquema de gradientes de color suaves" se toma de este experimento. La capa de ruido es un filtro de imagen de turbulencia; las octavas son imágenes superpuestas entre sí; La herramienta de grado se llama "corrección de curva" en Photoshop.
Lo que me molesta un poco es que la mayoría del código que escriben los desarrolladores de juegos para la generación de terreno basada en el ruido (incluido el desplazamiento del punto medio) resulta ser el mismo que en los filtros de sonido e imagen. Por otro lado, crea resultados bastante decentes en solo unas pocas líneas de código, por eso escribí este artículo. Este es un
punto de referencia rápido y fácil . Por lo general, no uso esas tarjetas durante mucho tiempo, pero las reemplazo con un generador de mapas más complejo tan pronto como descubro qué tipos de tarjetas se adaptan mejor al diseño del juego. Este es un patrón estándar para mí: comenzar con algo extremadamente simple y luego reemplazarlo solo después de comprender mejor el sistema con el que estoy trabajando.
Hay
muchas más cosas que se pueden hacer con el ruido, en el artículo que mencioné solo algunas. Pruebe
Noise Studio para probar de forma interactiva varias funciones.