Cargando en memoria los assets de un mundo


Aclaración: Antes de empezar a explicar la solución que estoy desarrollando, tengo que aclara que lo primero que necesitas estudiar, para el lenguaje y el motor que estes desarrollando, es multithreating y el funcionamiento de la lectura y escritura en memoria, del propio lenguaje y el posible comportamiento del motor gráfico.

Las necesidades

Durante el desarrollo de Blood and Fire, de Alchemic Warriors, una historia, que fue planeada para ser varias sagas de una franquicias, es decir, una historia transmedia, se publica en diferentes medios (comics, novelas, juegos, etc), enfrente varias complicaciones a la hora de manejar la carga de los terrenos, casi vacios, es decir, con vegetación y poco más, cuándo agrege un pequeño grupo de casas, los fps comenzaban a bajar.

A futuro, la idea se iria complicando, si llevaba a la práctica, además de los assets más frecuentes, que son las meshes y las texturas, necesitaría terrenos, configuraciónes de iluminación. En mi casó un horario de aparación de la niebla, densidad máxima, lumiens, ciclo lunar, intensidad de las estrellas, etc.

En un principio, comencé a inspirarme con Assassins Creed Odysey, soy una única persona, que no tiene chances de hacer un crowfounding, por lo tanto la idea de llegar a complejar el proyecto, es descabellada. Sin embargo, sirve para plantearme cosas a futuro, las posibles entregas a futuro, idea general del gamepaly, tener un producto reutilizable como asset que pueda ser vendido como addon y si es posible, dar vida a mis más de diez producciónes transmedia, que tengo en el tintero como historias sueltas, en realidad espero llegar a veinte.

En pocas palabras, esto surge de la necesidad de un buen gameplay, por el cuál valga la pena pagar y a su vez, garantice al game designer, una buen margen de libertad, para hacerse el artista. He pasado por mis momentos mágicos, durante el desarrollo del GDD, se que a veces podemos poner algunas metas poco realistas.

Una idea de solución

Para grandes escenarios, que poseen varios mapas, es necesario subdividirlos en varias escenas, con sus assets correspondientes y a veces, otras subescenas, dentro de un edificio, habitación, etc. Estos criterios dependen de la complejidad del entorno, lo que vemos y lo que necesitamos cargar luego.

Para evitar cortes en el gameplays, lo que algunos también llaman “tirones”, la carga de los assets, deberían estan listados, para no saber que necesitamos cargar y en que momento. Lo que nos lleva a nuevas complicaciones en el caso de tener un mundo abierto.

Básicamente la idea, es tener los assets, listados y listos para ser procesados, ya que dependiendo de los discos y la cantidad de datos de transferencia que estemos usando durante el proceso de lectura y escritura, del propio gameplay, es decir, si estamos grabando las acciones del jugador, para un uso posteriores y/o si estamos leyendo gran cantidad de archivos. Un ejemplo de esto, es un creador de contenido grabando la partida, o descargando otro juego al mismo tiempo y en el mismo disco.

Antes de ir más profundo, quiero dejarles este gráfico:

No es lo más profesional, pero esto tambíen espero que pueda ser usado por game designers

Los assets, que estan en disco, serán leídos cuando el jugador, entre al nivel, es decir, cuándo inice la partida. Luego de entender en dónde esta el jugador y que necesita cargar, la cpu leera los primeros assets, los procesará y enviará a la GPU, en dónde tiene su propia memoria, para usos propios de los cálculos relacionados a los gráficos.

Básicamente, mi idea es tener una base de datos, con los hash de cada uno de los posibles objetos, su dirección en almacenamiento y su relación:

  • Meshes
  • Shaders
  • Textures

Dejaré en claro, por las dudas que surga un idea erronea, los terrains, son meshes, que poseen varias imágenes (splatmaps), que a su vez llevan otras imágenes, que serán desplegadas de acuerdo a ciertas caracterísiticas definidas por el, o los shaders. También, puede darse el caso de que el mismo terrain, necesite una imagen, que defina las alturas, es decir un heightmap.

Las meshes, necesitan un o varios shaders, para ser representados en pantalla, con lo cuál también necesitaremos una o varias imagenes. Si el objeto, necesita un efecto especial, como por ejemplo, la suciedad de un determinado bioma, como por ejemplo, la tierra que cubre a un auto abandonado en un escenario post apocaliptico, también deberíamos agregar los datos, para cargarlos dependiendo de las cordenadas, que estarán realacionadas a un bioma.

Con lo cual, aqui tendríamos que darle al shader del auto los siguientes valores para cargar en pantalla:

  • Posición: X: 202, Y: 60, Z: 108 (Bioma pantano)
    • Suciedad: 0.8
    • Color: RGB(42, 25, 12)
    • Musgo: 1
  • Posición: X: 104, Y: 20, Z: 50 (Bioma playa)
    • Suciedad: 0.1
    • Color: RGB(177, 167, 160)
    • Erosión de la pintura: 0.5

Como pueden ver, aqui tenemos valores diferentes, que requerirían de un comprobación que puedan alterar la ejecución del shader, la necesidad de un shader por cada bioma, tipo de objeto, etc, dependerá de la situacion. La idea de la base de datos, que carge los datos de forma rápida, es tener la libertad para ser una poco más creativos. En este caso, los valores del shader, podrían cambiar si el auto es reparado y luego llevado a otro bioma. Por ejemplo, en el desierto, podrían volver a tener algunos daños en la pintura, después de atravezar una tormenta de arena, así como también, llenarse de fango al ir por un camino de tierra, lo cuál también podría requerir un mapa, de humedad.

Para cargar nuevos mapas, que representen por ejemplo, la humedad, crear mapas de flujo de corrientes, para el caso de necesitar crear una zona indundable, también deberíamos hacerlos en forma de relación y ser llamados a travez de una determinada acción o evento, también porque no a travez de un observer. Por ejemplo, si el jugador esta en la posición A, en dónde hay una represa, durante una tormenta, y ha llenado los requisitos, el agua desbordará la construcción y el jugador deberá huir o cumplir con ciertos eventos. Esta útlima idea estaba planeada para mi juego, aunque estoy pensando en descartarla por falta de recursos, a continuación entenderán el porque.

Más alla de la muralla de agua, que son una mesh que aparece a una determinada altura, o se levanta del nivel X1 al nivel X2, el nivel a dónde final del agua, más la fiesta de partículas, que eso requeriría. La ejecución, estaría limitada, los cálculos de la luz solar, las nubes, del entorno, deberían tener ciertas variaciones, para optimizar o minizar el uso de los recursos, es decir, para que terminemos con un pase de diapositivas. Otra forma sería, preconfigurar, los datos de los valores de cada sistema durante el evento, con lo cuál podríamos llegar a hablar de un “Event Database”, para manejar todas las configuraciones del entorno la historia y los condicionales.

Esto también abre la posibilidad, de tener una columna, de acuerdo a un game state, para determinar la situación de un objeto o en mi caso, hacer un “JOIN” lo que se suele hacer en las bases de datos relacionales, para obtener los datos de una segunda consulta, para darle al objeto place, los datos del estado de limpieza o de abandono, para que pueda darle a los objetos hijos, los valores para que representen en los shaders o instancie la vegetación necesaria, para que paresca abandona, eso si también, no requiere de tener algún carro abandonado, que luego de lugar a otras mecánicas o simplemente, barriles rotos, escombros, cenizas, estandartes rotos, fauna, etc.

En resumen, ante grandes cantidades de objetos y variaciones, que requieren de acuerdo a la toma, necesitamos tener todo prestablecido, para evitar llamar a la mayor cantidad de objetos, preconfigurados por el editor, para ser leídos durante la ejecución del juego. Hay que recordar, que la lectura es un proceso bloqueante, con lo cuál no podemos usar las capacidades de un procesador moderno. Esto es algo que aprendí de NodeJS, cuándo lo usaba para crear datos de prueba, leía las configuraciones del archivo inicial, luego realizaba las operaciones en N core, sin volver a leer, si lo quería, se realizaba una escritura.

La posible solución

A continuación dejo una versión simplicada del algoritmo:

  1. Inicio de la partida
  2. Cargar la lista de los assets, de acuerdo a la posición y game state.
  3. Cargar los assets, en memoria, de la posición del jugador.
  4. Leer y precargar los datos de cada shader, de cada objeto.
  5. Renderizar los assets, estáticos, que estan siendo enfocados por la cámara.
  6. Renderizar los assets, dinámicos, que se requieren. (NPC, fauna, etc)
  7. Evaluar, si es posible, si existe una camino a seguir para precargar. De lo contrario, esperar a que el jugador llegue a una zona de transición.
  8. Si el jugador, ha llegado a una zona de transición, cargar los assets del siguiente escenario.
    • Si es un interior, ocultar todo lo que esta afuera.
    • Si es un exterior, precargar, la lista del interior. Repetir si hay subniveles.
    • Si es un exterior y un nuevo bioma, precargar la lista de assets. Si es posible, usar la niebla, para ocultar la instanciación de los objetos.
,