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:
- Distinguimos qué es un mosaico y qué no utiliza valores binarios. 1 es un mosaico, 0 es su ausencia.
- 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).
- 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.
- 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.
- 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) {
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)
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;
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) {
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.
El suavizado se realiza de la siguiente manera:
- Obtenemos la posición actual y la última.
- Obtenemos la diferencia entre dos puntos, la información más importante que necesitamos es la diferencia a lo largo del eje y
- 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.
- Luego, comenzamos a establecer posiciones, llegando a cero
- 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
- 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 {
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) {
Random Walk Top con anti-aliasingTal 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) {
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;
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) {
La función comienza con lo siguiente:
- Encuentra la posición inicial
- Calcule el número de baldosas que se eliminarán.
- Eliminar el mosaico en la posición inicial
- Agregue uno al número de azulejos.
Luego pasamos al
while
. Él creará una cueva:
while (floorCount < reqFloorAmount) {
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) {
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.
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:
- 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.
- 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) {
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) { 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)) {
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))) {
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) { int tileCount = 0;
Habiendo recibido el número de vecinos, podemos proceder a suavizar la matriz. Como antes, necesitamos un bucle for
para 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++) {
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í .