Prácticas externas

Las prácticas externas del Grado en Diseño y Desarrollo de Videojuegos se centraron en mi caso en dos trabajos: Trabajo Académicamente Dirigido y beca de investigación en el empresa FACSA.


El Trabajo Académicamente Dirigido consistió en realizar una serie de cambios a un artículo en proceso de publicación llamado Analyzing autostereoscopic environment configurations for the design of video games, en el cual se realizaba un experimento con una televisión autoestereoscópica para observar las distintas sensaciones que se producían según la persona.

Descripción Trabajo Académicamente Dirigido:


Hace años era posible ver películas 3D con el uso de unas gafas con un filtro azul y
filtro rojo, pero era incómodo y no se apreciaba los colores como realmente son. Hoy en día,
el uso del 3D en nuestra vida diaria está en auge ya sea con Realidad Virtual, televisores
3D o televisión estereoscópicas o Realidad Aumentada. Es por ello importante aprender
cómo funcionan estas tecnologías y ver hasta qué punto son útiles y si sería posible
poderlas usar a diario. La pantalla de algunos dispositivos (como la consola Nintendo 3DS)
hacen uso de una característica física que tienen nuestros ojos: la separación que hay entre
ellos. Si nos tapamos el ojo derecho veremos que lo que percibimos está ligeramente
desplazado a la izquierda y lo mismo pasa si nos tapamos el ojo izquierdo. A raíz de eso,
las televisiones estereoscópicas o también llamadas televisiones 3D juegan con el enfoque
de un mismo objeto desde distintas perspectivas para crear la sensación de 3D. Es
importante conocer los distintos parámetros implicados en esta generación de sensación 3D
y qué ocurre al variarlos.
En este proyecto se hace uso de un rack de ocho cámaras. Las cámaras se
relacionan entre ellas de tal modo que se han creado tres tipos de visualización
dependiendo de cómo estén colocadas: Ejes ópticos paralelos (POA, Experimento 1), ejes
ópticos convergentes a un punto estático (SCOA, Experimento 2) (Ver Fig. 1, configuración
de las cámaras en los experimentos 1 y 2) y ejes ópticos convergentes a un punto dinámico
(DCOA, Experimento 3) (Ver Fig. 2, configuración de las cámaras en el experimento 3
según si el punto está más cerca o más lejos). En las tres configuraciones se genera el
efecto 3D pero la intención de este proyecto es observar los diferentes resultados que
producen a las personas, pues no todos tenemos la misma distancia focal en nuestra vista,
entre otros factores.
Teniendo en cuenta que tenemos tres tipos de experimentos, hemos definido el
juego en 6 partes: 3 navegando por un escenario que atraviesa una serie de anillos o
túneles y 3 navegando por un terreno procedural. Estos experimentos entran en el campo
del videojuego ya que el experimento consiste en ver objetos en movimiento y si el jugador
se siente perdido o mareado por el uso de esto.



Fig. 1. Configuración de las cámaras en el Experimento 1 (imagen de la izquierda) y Experimento 2 (imagen de la derecha). Se puede observar que el punto de vista de las ocho cámaras en el experimento 1 no converge en ningún punto pero, sin embargo, en el experimento 2 todas convergen en uno fijo.

Fig. 2. Configuración de las cámaras en el Experimento 3. Se observa la diferencia entre los puntos de vista de las cámaras si el punto de convergencia (posición de la nave) está cercano o lejano.

La generación y representación de terrenos procedurales es una parte muy importante de los videojuegos ya que la mayoría de ellos de mundo abierto se genera así (como Minecraft). Así pues, como es costoso de crear, también es importante el uso de conceptos como el nivel de detalle (LOD) de los objetos, para reducir la cantidad de objetos en pantalla y no generar las partes que el jugador no va a visualizar pues esto cargaría el sistema. También el uso de hilos (threads) en la creación de este terreno juega un papel importante pues se pueden lanzar distintos hilos de ejecución a la vez para no tener que esperar a que un proceso termine para ejecutar el siguiente. Esto implica también el uso de cerrojos (locks) para evitar acceder a la misma posición de memoria que se está usando por otro hilo de ejecución, lo cual puede provocar errores.

Partiendo del proyecto que se tenía, los cambios que realicé fueron los siguientes:
  1. Realizar un nuevo escenario de montañas.
  2. Cambiar el control de la nave de modo automático a modo manual.
  3. Poner el entrelazado de las cámaras a las nuevas escenas creadas.
  4. Realizar un game loop de 20 segundos modo automático y 40 segundos a modo manual.
  5. No chocar con las montañas.
  6. Añadir elementos visuales como timer, daño, texto de cambio de niveles, etc.
  7. Añadir todo lo que considere necesario para cambiar un juego.
  8. Realizar un formulario de preguntas sobre los experimentos mostrados para conocer los resultado de este. 


Generación del terreno procedural



Se quería realizar un escenario con montañas para incluir la nave del anterior escenario de túnel para probar si la visualización y percepción de los distintos experimentos era independiente del escenario en pantalla. Para la realización de este se ha seguido un tutorial (que se puede encontrar en este enlace) aplicando los oportunos cambios.

El terreno procedural se crea mediante chunks. Un chunk es una unidad de medida usada para indicar la cantidad de terreno que Unity es capaz de procesar. Cada chunk crea una malla y cada malla tiene una cierta cantidad de vértices. Unity tiene una cantidad máxima de vértices soportados, así que para evitar sobrecargarlo se crean varios tipos de mallas teniendo cada una un nivel de detalle (LOD, Level Of Detail) que simplifica la malla. Un nivel de detalle de cero significa que esa malla está sin simplificar, es decir, que tiene el máximo nivel de detalle posible.
Dependiendo de la posición del viewer en el terreno, se actualizará o no los chunks visibles. Los chunks más lejanos tendrán un nivel de detalle inferior a los cercanos, es decir, los cercanos se generarán con más vértices que los lejanos (Ver Fig. 3, visualización de los distintos niveles de detalle entre chunks, y Fig. 4, configuración en el Editor de Unity para indicar a qué distancia pasa de un nivel de detalle a otro). Para cada chunk se generará varias mallas, una para cada nivel de detalle, y así se podrá visualizar una u otra. Cada malla creada en Unity tendrá:
  • mesh filter, que obtiene la malla del asset y lo pasa al mesh renderer para la visualización en pantalla,
  • mesh renderer, que obtiene la geometría del mesh filter y la renderiza a la posición definida por la componente Transform del GameObject, y
  •  mesh collider, que coge la malla del objeto y crea un Collider basado en esa malla para poder detectar la colisión y/o evitar la introducción de otros GameObjects dentro de este,
  • todo esto para poder visualizar la malla con los datos obtenidos en el map generator y evitar que la nave pueda atravesar la malla. 

Fig 3. Visualización de los tres niveles de detalle que tiene la malla según la distancia a la nave, En verde los chunks con un LOD de cero, siendo cero la malla de más alto nivel de detalle.

Fig 4. Niveles de detalle según la distancia de los chunks. Un nivel de detalle mayor
significa una mayor simplificación de la malla. En este ejemplo se puede observar
que hay 3 niveles de detalle: LOD 0, que va de una distancia 0 a 700; LOD 1, que va
desde 700 a 1000; LOD 4, que va de 1000 a 1200.


El map generator es un script que usamos para modificar el mapa a conveniencia. Tiene diferentes variables que podemos tocar desde el editor de Unity para probar cuál nos gusta más y guardar esos valores para repetir la malla en cada chunk (Ver Fig. 5, distintos parámetros de configuración del Map Generator para la creación de la malla a conveniencia):
  • DrawMode: una variable del tipo enum para indicar el modo de dibujado para poder visualizar en el editor los diferentes mapas, es decir, el mapa de ruido, el mapa de color o la malla.
  • Un booleano que indica si se usa flat shading o no. El concepto de flat shading se explica más adelante.
  • Octavas, usadas para modificar el ruido de Perlin. Esto indica el número de niveles de detalle que queremos que tenga nuestro ruido de Perlin.
  • La escala del ruido, número que determina a qué distancia se ve el mapa de ruido.
  • Persistance, número que determina cuánto afecta cada octava en la forma total de la malla. El valor de la persistance varía entre 0 y 1. (Ver Fig. 6 y Fig. 7 para ver cómo afecta la persistance para una misma octava)
  • Lacunarity, número que determina cuánto detalle se añade o se elimina en cada octava. (Ver Fig. 8 y Fig. 9 para ver la diferencia entre una lacunarity más baja y otra más alta para la misma octava)
  • Seed, es un valor generado cuando se crean valores aleatorios, pues realmente no son aleatorios, sino que saltan a una serie de valores predefinidos en un rango que para muchos usos puede parecer aleatorio. En otras palabras, si generamos un mapa con los mismos valores y la misma seed siempre será el mismo mapa, sin embargo si creamos un mapa con los mismos valores pero distinta seed, ese mapa cambiará.
  • Mesh Height Multiplier, un valor que altera la altura máxima que va a tener la malla.
  • autoUpdate, para visualizar en el editor todos los cambios que realizamos en el Map Generator de manera automática, sin tener que pulsar el botón Generate.
  • Regiones. Estas regiones indican hasta qué valor de altura debe ser de un tipo de terreno u otro. Estos tipos pueden ser nieve, agua, montaña, arena...etc. Para cada uno de ellos le asignamos un color a visualizar. (Ver Fig. 10, las distintas regiones predefinidas según la altura de la malla)

Fig 5. Diferentes parámetros que generan el terreno.


Fig.6. Generación del mapa con una persistencia de 0 y una octava de 10.



Fig. 7. Generación del mapa con una persistencia de 0.276 y una octava de 10

Fig. 8. Generación del mapa con una octava de 14 y una lacunarity baja (1.39)

Fig. 9. Generación del mapa con una octava de 14 y una lacunarity más alta (2.85)

Fig. 10. Regiones de terreno

El mesh collider es muy costoso de aplicar por lo que solo se aplicará a aquellas mallas que estén lo suficientemente cercas del viewer para chocar con él, es decir, las mallas que tengan un LOD de cero (siendo cero la malla de más alto nivel de detalle).


Los vértices de la malla se pueden crear mediante flat shading o no. Flat shading, como su propio nombre indica, es realizar las sombras y luces de un objeto de manera más uniforme para evitar lo que se conoce como “hard edges”. Pero para hacerlo más uniformes necesitamos más normales y para crear más normales necesitamos más vértices.
Por lo que si se crean con flat shading se necesitan más vértices por malla, lo que hace que haya más vértices por chunk y por ello tenemos que reducir el tamaño del chunk ya que Unity tiene un número máximo de vértices soportados. El tamaño asignado para un chunk normal es de 239, y para un chunk con flat shading es de 95. De esta manera podemos confirmar que nunca se crean más vértices de los que Unity puede soportar. (Ver Fig. 11 para visualizar el tamaño de un chunk sin flat shading y Fig. 12 para visualizar el tamaño de un chunk con flat shading, ambas capturas están tomadas desde el mismo punto de vista por lo que se puede observar la diferencia del tamaño)
Fig. 11. Tamaño de un chunk normal (sin flat shading). La malla será de mayor tamaño pues tiene menos vértices.
Fig. 12. Tamaño de un chunk con flat shading. La malla es de menor tamaño pues
necesita de más vértices. Es decir, en menos espacio tiene la misma cantidad de
vértices que en una malla sin flat shading.

Como se ha mencionado anteriormente, hay diferentes modos de dibujado debido a que necesitamos de diferentes mapas para generar la malla final. Estos mapas son el mapa de ruido y el mapa de color. A raíz de estos dos mapas podemos generar el mapa de alturas (mediante el multiplicador de alturas, el mapa de ruido y el mapa de color que asigna el color según la altura de la malla).


Para generar las mallas tenemos dos elementos principales: el número de vértices y el número de triángulos. El número de vértices se calcula multiplicando el ancho por el alto de la malla y el número de triángulos es: (width - 1) * (height -1) * 6.
Los triángulos se generan desde la esquina superior izquierda, por lo que habrá que tener en cuenta que los vértices del extremo derecho y los vértices del extremo inferior no formarán triángulos. Sabiendo esto, cuando realicemos el bucle de generación de triángulos de la malla habrá que comprobar que los vértices que escojamos no sean ninguno de los indicados anteriormente.
Si la malla a crear tiene un nivel de detalle superior a 0 (siendo 0 el valor de más nivel de detalle, es decir, donde la malla es visible con todos sus vértices) entonces se aplica la simplificación. La simplificación se usará para crear los triángulos de la malla de tal manera que “se saltará” el número indicado de vértices. Este salto se realiza en el bucle de creación de triángulos, no recorriendo cada vértice. (Ver Fig. 13, el bucle realiza ciertas iteraciones, dependiendo del LOD de la malla) Esto es así para poder crear triángulos más grandes, por tanto menor cantidad de triángulos y mayor simplificación de la malla.
Como para cada nivel de detalle habrá que crear su correspondiente malla, esto puede hacer que el ordenador se quede colgado durante un cierto tiempo, dependiendo de la potencia de este. Para evitar eso, hemos hecho uso de hilos de ejecución o threads. Un hilo de ejecución es una parte del procesador que realiza una tarea independiente del resto de tareas. Es posible que un hilo de ejecución quiera acceder a una posición de memoria que esté en uso en ese momento, lo cual puede crear un error catastrófico. Por ello es conveniente el uso de cerrojos, que no es más que un elemento que bloquea la lectura y/o escritura en una posición de memoria si está siendo usada.

Fig. 13. La variable meshSimplificationIncrement es una variable que será 1 si el nivel de detalle de esa malla es 0 (nivel de detalle más alto, sin simplificación). De otra manera se multiplicará el nivel de detalle por 2. Esta variable se usa en el bucle para realizar los saltos de la creación de triángulos, por lo que si el LOD es 0 recorrerá todos y cada uno de los vértices, pero si el LOD es 1 el bucle irá de dos en dos, si el LOD es 2 el bucle irá de cuatro en cuatro, etc.
Una vez creados los triángulos de la malla, hay que calcular las normales de estos. Según sea flat shading o no, se calcularán las normales de forma distinta, pues como se ha mencionado anteriormente, con el uso de flat shading se tienen muchos más vértices y cada vértice tiene que calcular su normal.(Ver Fig. 14 y Fig. 15, comparación de los bucles que crean las normales según sea la configuración con flat shading o sin). La visualización con flat shading será más uniforme (Ver Fig 16, malla sin flat shading y Fig 17, malla con flat shading para observar las diferencias en las sombras en cada polígono. En la versión sin flat shading el salto de la sombras es mucho más notable que en la versión con flat shading)

Fig. 14. Cálculo de las normales en el caso de no usar flat shading. El cálculo no recorre todos los vértices pues se calcula la normal para cada triángulo, es decir, la normal es la misma para los tres vértices que forman el triángulo, por lo que tan solo se recorre ⅓.

Fig. 15. Cálculo de las normales en el caso de flat shading. Se recorre todo el vector de triángulos pues por cada vértice se calcula la normal.

Fig 16. Visualización sin flat shading. Se puede observar que siendo los polígonos del mismo tipo de terreno se puede notar el borde entre polígonos.
Fig 17. Visualización con flat shading. El salto entre los diferentes polígonos es menor, no se nota tanto la separación entre dos polígonos del mismo tipo de terreno.

Movimiento de la nave



En el proyecto principal había un único movimiento, que seguía una trayectoria predefinida, sin la posibilidad de que el jugador pudiera interactuar con la nave de ninguna manera. Para evitar que fuera así, se creó un nuevo script de movimiento controlado por las teclas WASD (o flechas de dirección) y las teclas Q y E para avanzar y retroceder por el eje Z. Como el tipo de movimiento no era el convencional (solo en dos ejes) y este se realizaba en tres ejes, tuve que investigar cómo realizarlo. Además, había que evitar que la nave saliera de la pantalla y pudiera perderse de vista cuando se está jugando. El resultado de este movimiento se puede ver el la Fig. 18.

Como nunca había hecho un movimiento en los tres ejes, tuve que investigar sobre cómo hacerlo. Además, tenía que evitar que la nave saliera de la pantalla en ninguno de los tres ejes. El resultado de ese tipo de movimiento puede verse en esta imagen.

Fig. 18. Script movimiento de la nave en los ejes XYZ, acotándolo a la posición de la cámara para que no se salga de la visualización de esta.

Para cambiar del movimiento automático al manual se realizó un script auxiliar que cuenta el tiempo que ha pasado desde la inicialización del experimento actual y se inicializará con la nave en modo con trayectoria predefinida y cuando pasan 20 segundos activa el movimiento manual. A los 60 segundos, se cambia de experimento, siendo el orden el siguiente:
  1. Experimento 1 con túnel.
  2. Experimento 2 con túnel.
  3. Experimento 3 con túnel.
  4. Experimento 1 con terreno procedural.
  5. Experimento 2 con terreno procedural.
  6. Experimento 3 con terreno procedural.

Canvas y HUD


Anteriormente la nave solo indicaba la colisión con un asteroide mediante un temblor que apenas era perceptible para el jugador. Para evitar eso, se realizó un indicador de daño que convierte la pantalla en rojo durante un segundo (Ver Fig. 19, visualización del panel de daño cuando la nave atraviesa un asteroide) y además realiza un sonido de choque.
Fig 19. Indicador de daño cuando la nave atraviesa un asteroide. La nave tiembla y se produce un sonido de choque cuando esto pasa.

También consideré necesario una pantalla que indique cuando el jugador pasa a controlar la nave y una pantalla inicial que indique el experimento que se va a probar.
También hice una pantalla que indique cuando pasas del modo automático al modo manual (es decir, cuando el jugador tiene el control de la nave) (Ver Fig. 20, pantalla de aviso de cambio de control de la nave) y una pantalla al principio de cada escena que indique el experimento que vas a realizar (Ver Fig. 21, pantalla de inicio al experimento), para que los testeadores sepan cuál van a probar y puntuen el test. Por petición de la escritora del artículo, añadí un botón para poder activar y desactivar el entrelazado de las cámaras, situado en la esquina inferior derecha.
Fig. 20. Captura del juego donde se visualiza la pantalla de aviso al jugador del cambio de estado del movimiento de la nave. El aviso se realiza mediante tres frases: READY, STEADY, GO!

Fig. 21. Imagen del HUD que avisa del experimento que va a visualizar el jugador. Para que esta pantalla desaparezca, el jugador debe pulsar la barra espaciadora del teclado.


Resultado





Comentarios

Entradas populares de este blog

TFM - Farming simulation VR game for elderly

Bachelor’s Thesis / Game Lab project