Investigación Generación

Generación de contenido procedural - Básico


¿Qué es el PCG?

Aquí debo comenzar diciendo que en realidad una definición certera y aceptada por toda la academia no existe, es por eso que voy a mencionar diversas definiciones que obtuve de las referencias en las que me base:

  • El PCG es la creación algorítmica de contenido de un juego, con entrada de información limitada o indirecta por parte del usuario.
  • El PCG es la generación programática de contenido de un juego, utilizando un proceso aleatorio o pseudo aleatorio que da como resultado un rango impredecible de posibles espacios de juego.
  • El PCG es el concepto o paradigma por el cual todas las piezas de contenido de un juego pueden ser creadas solo mediante la utilización de la programación.

En otras palabras, el PCG es la creación de contenido de un juego mediante la programación. Y para comprender mejor esto, debo explicar su nombre. La palabra clave Procedural, viene de procedimiento, que en la programación es simplemente una instrucción que debe ser ejecutada. Por supuesto los procedimientos (también conocidos como funciones y métodos) son el principal paradigma en la programación. Por otra parte el Content o contenido es todo lo que se presenta ante el usuario, es decir, niveles, modelos, texturas, música, sonidos, historias, inteligencia artificial, entre otras tantas.

/img/c/invproces.png
A la izquierda se puede observar una textura hecha a mano, y a la derecha una textura generada de manera procedimental

¿Por qué deberíamos utilizar el PCG?

Tal vez la respuesta a esta pregunta sea obvia, pero de igual manera es importante analizarla. Por supuesto la razón primordial para utilizar PCG es que nos quita la necesidad (casi en su totalidad) de contar con un diseñador o artista humano que genere contenido para el juego. Sabemos que los humanos somos lentos y costosos y por lo general se necesita cada vez más de ellos para crear contenido de alta calidad para los videojuegos de la industria. Pero si utilizamos el PCG como la roca fundamental sobre la que edificamos el videojuego, nos estaremos ahorrando varios hombres, que podrían haber diseñado o creado contenido de manera manual y no automática y eficiente como lo hacen los algoritmos. Se dice que las ventajas de utilizar el PCG son la unicidad, la robustez, la flexibilidad, la adaptabilidad que aportan al videojuego. Sobre todo ya que podríamos hacer un juego rejugable casi de manera infinita (como veremos más adelante al implementar algoritmos). Un claro ejemplo de esto es el sistema de generación de armas del famoso videojuego Borderlands que se puede apreciar en la imagen.

/img/c/invproces2.png
Generación procedimental de armas en Bordelands de Gearbox Software

Teoría Fundamental

Números Pseudo Aleatorios

Los números aleatorios han sido utilizados en una gran cantidad de juegos desde hace mucho tiempo, desde juegos tradicionales como lo son los juegos de cartas hasta los juegos de mesa con dados. Los números aleatorios entregan al juego un factor que los hace impredecibles. Y sabemos que las cosas impredecibles son emocionantes, desafiantes y ofrecen una experiencia única, por lo tanto entregan un valor único a un universo.

En las ciencias de la computación el estudio de los números aleatorios se ha enfocado por lo general en su uso en la criptografía y la ciber seguridad, por supuesto mediante complejos algoritmos y fórmulas matemáticas, que aquí no serán estudiadas para su alivio. Unity de hecho ya provee una clase llamada Random, la que permite generar números aleatorios, como veremos más adelante.


Números Aleatorios VS Números Pseudo Aleatorios

Y aquí viene la cruda realidad nuevamente a recordarnos porque somos imperfectos. Han de saber queridos lectores que los números pseudo aleatorios tal como su nombre lo predicen, no son números aleatorios. Un evento realmente aleatorio sería por ejemplo lanzar un dado. Pero por otra parte los números pseudo aleatorios son preferidos en la programación de juegos ya que son mucho más sencillos de generar y por supuesto también de reproducir los mismos resultados una y otra vez (ya les explico por qué). Imaginen si lanzamos un dado de 6 caras y nos sale 1 en la cara, luego si lo lanzamos nuevamente y queremos reproducir el mismo resultado tenemos una probabilidad de 1/6 que el dado nos entregue el mismo resultado, ahora piensen en un dado de 1 millón de caras, si lanzamos y nos sale el número 424.342 en la cara del dado, entonces para poder reproducir el mismo resultado tendríamos 1/1.000.000 como probabilidad de que nos salga el mismo resultado, lo que significaría para nosotros gran inversión de tiempo seguramente, ¿ya ven por qué utilizar números pseudo aleatorios nos conviene más?

En los números pseudo aleatorios existe algo conocido como Seed o Semilla que no es más que la representación (en número, o string) de la aleatoriedad que usara nuestro generador de números pseudo aleatorios y la cual nos será de vital importancia a la hora de replicar un resultado o secuencia de acciones. Por ejemplo si generamos un nivel basados en un seed con el número 5 con X algoritmo, luego podemos replicarlo fácilmente al hacer que el seed simplemente tenga el mismo valor que antes. Pero por otra parte la desventaja que esto entrega es que puede darse el caso en que las combinaciones de reglas posibles de nuestro algoritmo se acaben y luego de un tiempo nuestra seed replique los mismos resultados antes vistos, pero en la mayoría de los casos eso será controlado fácilmente.


Programando

A continuación voy a presentar al lector 2 ejemplos que creo son indicados para poder comenzar a entender de manera práctica esta temática tan apasionante. El primero de ellos se trata de un programa que genera el típico “Hola Mundo”, pero esta vez ordenado de manera procedimental, si bien es cierto este es un ejercicio muy pequeño y sencillo, nos servirá para afirmar los conceptos anteriormente estudiados y aprendidos. Luego en un segundo ejemplo mucho más desafiante que el primero, explicaré paso a paso como crear la estructura de una cueva utilizando el PCG, por supuesto este algoritmo será mucho más complejo (aunque no imposible de entender) que el primero.

Unity3D

Bien conocido es el típico programa que la mayoría de nosotros, cuando dábamos nuestros primeros pasos en la programación, creamos en nuestra primera aventura con una IDE, y con la que logramos un resultado que a pesar de ser sencillo, nos permitió vislumbrar todo el potencial que tienen los computadores para seguir instrucciones de manera ordenada. Bueno tomando esto en cuenta quise esta vez llevarlo al plano de los algoritmos procedimentales, aunque no lo considero precisamente 100% apegado a la definición que vimos antes, pero si nos dará una primera impresión de lo que les venía hablando sobre los universos que pueden ser creados por un computador. Lo que se deseamos lograr como objetivo principal con este pequeño ejemplo, es que podamos generar todas las combinaciones posibles de las letras de las palabras “PROCEDURAL”, por supuesto cada vez que el seed tome un valor determinado, la combinación de letras debería ser distinta, considerando claro que tenemos 10 letras, entonces la cantidad total de combinaciones posibles para mezclar esas letras sería por fórmula 10! que es mas de 362.880 combinaciones. Por lo tanto nuestra seed puede dar un resultado distinto 362.880 veces o mas, y luego de eso, tal y como estudiamos anteriormente se comenzarán a repetir los resultados.

 Aqui os dejo el link a GitHub con los diferentes ejemplos.

Sin más preámbulos vamos a analizar el siguiente trozo de código que nos permite generar de manera procedural las combinaciones de letras de las palabras “PROCEDURAL”.


//                                  ┌∩┐(◣_◢)┌∩┐
//                                                                              \\
// PruebaPCG.cs (26/09/2017)                                                    \\
// Autor: Antonio Mateo (Moon Antonio)  antoniomt.moon@gmail.com                \\
// Descripcion:     Prueba de PCG                                               \\
// Fecha Mod:       26/09/2017                                                  \\
// Ultima Mod:      Version Inicial                                             \\
//******************************************************************************\\

#region Librerias
using UnityEngine;
using System.Collections.Generic;
#endregion

namespace MoonAntonio
{
    public class PruebaPCG : MonoBehaviour
    {
        #region Variables Publicas
        /// <summary>
        /// <para>Semilla.</para>
        /// </summary>
        public int seed = 1;                                        // Semilla
        #endregion

        #region Variables Privadas
        /// <summary>
        /// <para>Palabra procesada.</para>
        /// </summary>
        private string palabraFinal;                                // Palabra procesada
        /// <summary>
        /// <para>Lista con las letras iniciales.</para>
        /// </summary>
        private List<string> letras = new List<string>();           // Lista con las letras iniciales
        #endregion

        #region Inicializadores
        /// <summary>
        /// <para>Cargador de <see cref="PruebaPCG"/>.</para>
        /// </summary>
        private void Awake()// Cargador de PruebaPCG
        {
            letras.Add("P");
            letras.Add("R");
            letras.Add("O");
            letras.Add("C");
            letras.Add("E");
            letras.Add("D");
            letras.Add("U");
            letras.Add("R");
            letras.Add("A");
            letras.Add("L");
        }

        /// <summary>
        /// <para>Inicializador de <see cref="PruebaPCG"/>.</para>
        /// </summary>
        private void Start()// Inicializador de PruebaPCG
        {
            // Inicializamos la semilla
            Random.InitState(seed);

            // Obtenemos la palabra final sin procesar y la mostramos
            for (int n = 0; n < letras.Count; n++)
            {
                palabraFinal += letras[n];
            }
            Debug.Log("[NORMAL] Palabra inicial: " + palabraFinal);

            // Reseteamos la palabra
            palabraFinal = "";

            // Procesamos
            while (letras.Count > 0)
            {
                int index = Random.Range(0, letras.Count);
                palabraFinal += letras[index];
                letras.RemoveAt(index);
            }
            Debug.Log("[FIX] Palabra procesada: " + palabraFinal);
        }
        #endregion
    }
}

Lo primero que debemos haber notado al mirar el código es que tenemos una variable de tipo int que almacena nuestra seed o semilla, que es la que se encargará por supuesto de decidir la secuencia de números que devuelve la clase Random. Luego se puede observar una variable de tipo string que almacena la palabra final generada por el algoritmo. Y por último una tercera variable que almacena todas las letras de la cadena “HolaMundo” de tipo List almacenando string. En el método Awake ingresamos las letras de la cadena a la lista de manera ordenada, aunque claro está que en este caso el orden no debería influir en lo absoluto más allá de mostrar como era la cadena antes de ser desordenada por la clase Random. Luego en el método Start lo primero que se hace es asignar el seed de nuestra variable al seed de la clase Random, después se recorre con un for la lista de letras y se van ligando a la cadena de la palabra final, para mostrar como era la palabra antes de ser desordenada por consola. Y por último se tiene un while con la condición: mientras queden letras en la lista, y si eso es cierto, entonces se obtiene un índice con la clase Random y el método Range con valores entre 0 y la cantidad de letras (aunque el último valor es exclusivo), posteriormente se concatena a la palabra final la letra que indica el índice obtenido y se quita de la lista de letras la letra ubicada en el índice (esto claro con el objetivo de poder salir en algún momento del ciclo while). Una vez finalizado el ciclo while se imprime por consola la palabra final entregada por nuestra aleatoriedad (especificada por el seed entregado).

/img/c/invproces3.png
5 resultados distintos del algoritmo, utilizando un seed aleatorio cada vez

Como podrán darse cuenta las posibilidades son muchas, como para generalas a mano, y con este simple algoritmo ya podemos comprender porque se dice que el PCG es una de las maneras más efectivas para generar mundos completos a partir de un seed, y porque le entrega rejugabilidad a los juegos casi de manera infinita (espero que sus mentes tengan la imaginación suficiente para pensar que cada cadena puede representar un mundo distinto).


Generando Cuevas

Pasando ahora a la parte más “compleja” de este post, nuestro objetivo será ahora generar una cueva de manera procedimental, con parámetros que nos permitan entregarle al jugador una solución distinta, y probablemente un mundo distinto cada vez que ejecute el programa. Pero para hacer realidad nuestro sueño dorado, primero debemos comprender un concepto conocido como Cellular Autómata o en su traducción al español Autómata Celular, que no es más que un modelo computacional discreto (asumo que el lector sabe el significado de discreto en términos matemáticos, y si no es así, espero lo puedan googlear). Los autómatas celulares han sido ampliamente estudiados en la informática, la física e incluso algunas ramas de la biología, como modelos de computación, de crecimiento, de desarrollo, de fenómenos físicos, etc. Pero para su alegría sus conceptos básicos son en realidad muy simples y pueden explicarse en unos pocos párrafos y una imagen o dos. Un autómata celular consiste en una cuadrícula de dimensiones de NxM, un conjunto de estados posibles, y un conjunto de reglas de transición. En esta cuadrícula cada celda puede tomar distintos estados, pero en el caso más simple, puede estar encendida o apagada (tomar un valor 1 o 0). A medida que se realizan iteraciones sobre el autómata este va evolucionando en pasos discretos y siguiendo sus propias reglas. En cada tiempo t, cada celda decide su nuevo estado basado en el estado de sí misma y todas las celdas de su entorno (sus vecinas) en el momento t-1. Estos vecinos pueden ser tomados en cuenta siguiendo 2 tipos de modelos, que se pueden observar en las siguientes figuras:

/img/c/invproces4.png
a) Vecinos Moore b) Vecinos von Neumann

En nuestro caso utilizaremos el modelo de Moore, para capturar una celda especifica y aplicar su regla de transición tomando en cuenta los 8 vecinos de la celda actual (celda central C), como veremos más adelante.

Los parámetros que utilizaremos para el control de las cuevas generadas serán:

  • X
  • Y
  • Seed
  • Porcentaje de Muros
  • Cantidad de iteraciones de suavizado

Estos parámetros nos permitirán más o menos mantener un control sobre las cuevas que se generen, y por supuesto replicar las estructuras que encontremos interesantes. Entonces se estarán preguntando pero y cómo realmente generamos las cuevas, y aquí viene lo entretenido, ya que aplicando un autómata celular sobre un algoritmo de randomización veremos como el caos se convierte en algo estructurado. Fíjense en el siguiente código:


//                                  ┌∩┐(◣_◢)┌∩┐
//                                                                              \\
// GeneradorMazmorra.cs (26/09/2017)                                            \\
// Autor: Antonio Mateo (Moon Antonio)  antoniomt.moon@gmail.com                \\
// Descripcion:     Generador no logico de mazmorras                            \\
// Fecha Mod:       26/09/2017                                                  \\
// Ultima Mod:      Version Inicial                                             \\
//******************************************************************************\\

#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
    /// <summary>
    /// <para>Generador no logico de mazmorras</para>
    /// </summary>
    public class GeneradorMazmorra : MonoBehaviour 
    {
        #region Variables Publicas
        /// <summary>
        /// <para>Largo de la mazmorra.</para>
        /// </summary>
        public int x = 15;                              // Largo de la mazmorra
        /// <summary>
        /// <para>Altura de la mazmorra.</para>
        /// </summary>
        public int y = 15;                              // Altura de la mazmorra
        /// <summary>
        /// <para>Semilla.</para>
        /// </summary>
        public int seed = 99;                           // Semilla
        /// <summary>
        /// <para>Determina si la seed sera aleatoria.</para>
        /// </summary>
        public bool seedAleatoria = false;              // Determina si la seed sera aleatoria
        /// <summary>
        /// <para>Porcentaje de muros.</para>
        /// </summary>
        [Range(0, 100)] public int Muros = 50;          // Porcentaje de muros
        #endregion

        #region Variabes privadas
        /// <summary>
        /// <para>Mapa de la mazmorra.</para>
        /// </summary>
        private int[,] mazmorraMapa;                    // Mapa de la mazmorra
        #endregion

        #region Inicializadores
        /// <summary>
        /// <para>Inicializador de <see cref="GeneradorMazmorra"/>.</para>
        /// </summary>
        private void Start()// Inicializador de GeneradorMazmorra
        {
            // Creamos la mazmorra
            CrearMazmorra();
        }
        #endregion

        #region Actualizadores
        /// <summary>
        /// <para>Actualizador de <see cref="GeneradorMazmorra"/>.</para>
        /// </summary>
        private void Update()// Actualizador de GeneradorMazmorra
        {
            // Creamos la mazmorra
            if (Input.GetMouseButtonDown(0))
            {
                CrearMazmorra();
            }
        }
        #endregion

        #region Metodos
        /// <summary>
        /// <para>Crear la mazmorra.</para>
        /// </summary>
        private void CrearMazmorra()// Crear la mazmorra
        {
            mazmorraMapa = new int[x, y];
            Procesar();
        }

        /// <summary>
        /// <para>Logica de la creacion de la mazmorra.</para>
        /// </summary>
        private void Procesar()// Logica de la creacion de la mazmorra
        {
            Random.InitState((seedAleatoria) ? Random.Range(int.MinValue, int.MaxValue) : seed);

            for (int n = 0; n < x; n++)// Filas
            {
                for (int i = 0; i < y; i++)// Columnas
                {
                    if (n == 0 || n == (x - 1) || i == 0 || i == (y - 1))
                    {
                        mazmorraMapa[n, i] = 1;
                    }
                    else
                    {
                        int probabilidad = Random.Range(0, 100);
                        if (probabilidad < Muros)
                        {
                            mazmorraMapa[n, i] = 1;
                        }
                        else
                        {
                            mazmorraMapa[n, i] = 0;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// <para>Dibuja la mazmorra.</para>
        /// </summary>
        private void OnDrawGizmos()// Dibuja la mazmorra
        {
            if (mazmorraMapa != null)
            {
                for (int n = 0; n < x; n++)// Filas
                {
                    for (int i = 0; i < y; i++)// Columnas
                    {
                        Gizmos.color = (mazmorraMapa[n, i] == 0) ? Color.white : Color.black;
                        Gizmos.DrawCube(new Vector3(n, i, 0f), new Vector3(0.9f, 0.9f, 0.9f));
                    }
                }
            }
        }
        #endregion
    }
}

Este código simplemente genera caos, como pueden ver en la figura, a pesar de que utiliza algunos de los parámetros que nombré anteriormente, como por ejemplo el seed, el alto, el ancho, e incluso el porcentaje de muros, finalmente solo es capaz de generar caos, ya que no ha sido sometido al autómata celular, es decir, no evoluciona y su forma siempre es el caos, como se puede observar en la figura (los cuadrados o celdas negras representan muros y los blancos suelo, en este caso el porcentaje de muros fue 50%).

/img/c/invproces5.png
Mapa Aleatoriamente llenado con 0’s y 1’s (50%)

Ahora para solucionar esto simplemente hacemos uso del autómata celular, que como bien dije antes, permitirá hacer evolucionar al mapa de manera tal que una vez aplicadas sus reglas (la de conjugación de vecinos), entonces tomará una forma similar a la de una cueva, en términos de estructura, y el código es el siguiente.


//                                  ┌∩┐(◣_◢)┌∩┐
//                                                                              \\
// GeneradorMazmorraLogico.cs (26/09/2017)                                      \\
// Autor: Antonio Mateo (Moon Antonio)  antoniomt.moon@gmail.com                \\
// Descripcion:     Generador logico de mazmorras                               \\
// Fecha Mod:       26/09/2017                                                  \\
// Ultima Mod:      Version Inicial                                             \\
//******************************************************************************\\

#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
    /// <summary>
    /// <para>Generador logico de mazmorras</para>
    /// </summary>
    public class GeneradorMazmorraLogico : MonoBehaviour 
    {
        #region Variables Publicas
        /// <summary>
        /// <para>Largo de la mazmorra.</para>
        /// </summary>
        public int x = 15;                              // Largo de la mazmorra
        /// <summary>
        /// <para>Altura de la mazmorra.</para>
        /// </summary>
        public int y = 15;                              // Altura de la mazmorra
        /// <summary>
        /// <para>Semilla.</para>
        /// </summary>
        public int seed = 99;                           // Semilla
        /// <summary>
        /// <para>Determina si la seed sera aleatoria.</para>
        /// </summary>
        public bool seedAleatoria = false;              // Determina si la seed sera aleatoria
        /// <summary>
        /// <para>Porcentaje de muros.</para>
        /// </summary>
        [Range(0, 100)] public int Muros = 50;          // Porcentaje de muros
        /// <summary>
        /// <para>Iteraciones para suavizar la mazmorra.</para>
        /// </summary>
        public int iteracionesSuavizado = 1;            // Iteraciones para suavizar la mazmorra
        #endregion

        #region Variabes privadas
        /// <summary>
        /// <para>Mapa de la mazmorra.</para>
        /// </summary>
        private int[,] mazmorraMapa;                    // Mapa de la mazmorra
        #endregion

        #region Inicializadores
        /// <summary>
        /// <para>Inicializador de <see cref="GeneradorMazmorra"/>.</para>
        /// </summary>
        private void Start()// Inicializador de GeneradorMazmorra
        {
            // Creamos la mazmorra
            CrearMazmorra();
        }
        #endregion

        #region Actualizadores
        /// <summary>
        /// <para>Actualizador de <see cref="GeneradorMazmorra"/>.</para>
        /// </summary>
        private void Update()// Actualizador de GeneradorMazmorra
        {
            // Creamos la mazmorra
            if (Input.GetMouseButtonDown(0))
            {
                CrearMazmorra();
            }
        }
        #endregion

        #region Metodos
        /// <summary>
        /// <para>Crear la mazmorra.</para>
        /// </summary>
        private void CrearMazmorra()// Crear la mazmorra
        {
            mazmorraMapa = new int[x, y];
            Procesar();

            for (int n = 0; n < iteracionesSuavizado; n++)
            {
                SuavizarMapa();
            }
        }

        /// <summary>
        /// <para>Logica de la creacion de la mazmorra.</para>
        /// </summary>
        private void Procesar()// Logica de la creacion de la mazmorra
        {
            Random.InitState((seedAleatoria) ? Random.Range(int.MinValue, int.MaxValue) : seed);

            for (int n = 0; n < x; n++)// Filas
            {
                for (int i = 0; i < y; i++)// Columnas
                {
                    if (n == 0 || n == (x - 1) || i == 0 || i == (y - 1))
                    {
                        mazmorraMapa[n, i] = 1;
                    }
                    else
                    {
                        int probabilidad = Random.Range(0, 100);
                        if (probabilidad < Muros)
                        {
                            mazmorraMapa[n, i] = 1;
                        }
                        else
                        {
                            mazmorraMapa[n, i] = 0;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// <para>Suavizado del mapa.</para>
        /// </summary>
        private void SuavizarMapa()// Suavizado del mapa
        {
            for (int n = 0; n < x; n++)
            {
                for (int i = 0; i < y; i++)
                {
                    int cantidadVecinosMuro = GetVecinos(n, i);

                    if (cantidadVecinosMuro > 4)
                    {
                        mazmorraMapa[n, i] = 1;
                    }
                    else
                    {
                        mazmorraMapa[n, i] = 0;
                    }
                }
            }
        }

        /// <summary>
        /// <para>Dibuja la mazmorra.</para>
        /// </summary>
        private void OnDrawGizmos()// Dibuja la mazmorra
        {
            if (mazmorraMapa != null)
            {
                for (int n = 0; n < x; n++)// Filas
                {
                    for (int i = 0; i < y; i++)// Columnas
                    {
                        Gizmos.color = (mazmorraMapa[n, i] == 0) ? Color.white : Color.black;
                        Gizmos.DrawCube(new Vector3(n, i, 0f), new Vector3(0.9f, 0.9f, 0.9f));
                    }
                }
            }
        }
        #endregion

        #region Funcionalidad
        /// <summary>
        /// <para>Obtiene los vecinos de las coordenadas.</para>
        /// </summary>
        /// <param name="xM"></param>
        /// <param name="yM"></param>
        /// <returns></returns>
        private int GetVecinos(int xM, int yM)// Obtiene los vecinos de las coordenadas
        {
            int cantidadMuros = 0;
            for (int n = xM - 1; n <= xM + 1; n++)
            {
                for (int i = yM - 1; i <= yM + 1; i++)
                {
                    if (n >= 0 && n < x && i >= 0 && i < y)
                    {
                        if (n != xM || i != yM)
                        {
                            cantidadMuros += mazmorraMapa[n, i];
                        }
                    }
                    else
                    {
                        cantidadMuros++;
                    }
                }
            }
            return cantidadMuros;
        }
        #endregion
    }
}

Lo único que ha cambiado ahora es que el código toma en cuenta la regla de los vecinos,que nos indica que si hay más de 4 vecinos que son muros (es decir más de la mitad del total de 8 vecinos), entonces nuestra celda actual debe convertirse también en muro, por el contrario si son menos de 4 vecinos muros, entonces nuestra celda debe ser un suelo, y solo con esta pequeña modificación y regla ahora nuestro algoritmo arroja resultados como estos, con los mismos parámetros anteriores pero con una cantidad de iteraciones de suavizado igual a 5.

/img/c/invproces6.png
Algoritmo de Generación de Cuevas

Y ahora piensen en que este resultado es utilizando solo los siguientes parámetros simplemente:

/img/c/invproces7.png
Parámetros para replicar la cueva anterior

Alcanzan realmente a asimilar lo que esto significa? Si su respuesta es no, entonces los invito a jugar modificando los parámetros para que puedan observar toda la cantidad de diversos resultados que pueden obtener. Y por cierto no sé si se dieron cuenta pero a nuestra seed cada vez que la variable seedAleatoria esta activada, entonces le asignamos un valor aleatorio entre el mínimo número entero posible para el procesador y el máximo (Random.seed = (semillaAleatoria)?Random.Range(int.MinValue,int.MaxValue):semilla;) y si quieren una seed infinita, podrían por qué no utilizar como medida el tiempo actual, que solo fluye hacia adelante como sabemos…

/img/c/invproces8.gif
Jugando con el algoritmo

Pero no todo es perfecto y si se dan cuenta en realidad nuestra estructura genera en la mayoría de los casos al menos una zona en la que hay solamente suelo, pero que está a si mismo aislada de las demás, por lo que no hay una conexión directa entre las zonas. Para esto por supuesto existe una solución que por lo demás está más allá del alcance de este post pero que pueden buscar por su cuenta partiendo por algo denominado Flood Fill Algorithm o Algoritmo de relleno por difusión.

/img/c/invproces9.gif
Algoritmo de relleno por difusión

Este algoritmo puede ser perfectamente utilizado para reconocer todas aquellas zonas que quedaron aisladas de las demás y por supuesto poder conectarlas, claro si que en el caso de conectarlas deberán idear sus propias soluciones o bien recurrir al clásico A* Pathfindinüg Algorithm, pero eso ya es otro cuento, que requiere de un post aparte.

/img/c/invproces10.gif
A Pathfinding*

Pseudo Conclusiones
En fin, espero que este post les pueda servir como una introducción a esta temática tan interesante (me extendí mucho más de lo que debía pero bueno son los gajes del oficio) y que para mi ha sido un área de estudio desde hace algún tiempo atrás. Sin más que decir, esperando que sus cerebros alucinen con tantos bellos algoritmos, de los cuales puedan sacar sus propias conclusiones, me despido de ustedes, y cualquier duda o consulta, ya saben donde la pueden realizar, saludos!

Referencias Bibliográficas:

 Libro académico de papers relacionados con el PCG

 Libro sobre PCG en Unity

 Wiki de PCG

 Generación de Cuevas en 3D con PCG

 GameDevn


/img/ref.png
.