Profundizando con A-Frame: ¿intentamos hacer un videojuego?

Pongamos a prueba lo que nos ofrece A-Frame haciendo la base de un videojuego.

Índice de contenidos

1. Introducción

Muchos pensarán sobre la utilidad de la realidad virtual, unos para bien y otros para mal, pero hay una cosa muy clara cuando vemos la facilidad con la que nuestro compañero Enrique Rubio nos presenta el framework A-Frame, y es que tenemos que poner a prueba la madurez de su desarrollo. Y no hay nada más divertido y completo que crear un videojuego.

Para nuestro objetivo no nos iremos muy lejos, volveremos al clásico dungeon/mazmorra donde por arte de cursor haremos frente a enemigos estáticos y poco animados. Con esta base y los conocimientos que aplicaremos, veréis que sois capaces de llevarlo más allá de lo que os muestro, implementando vuestras propias ideas y mejoras.

Antes de comenzar, debemos echarle una ojeada al inspector que nos proporcionan los desarrolladores de A-Frame y, aunque en mi opinión prefiero usarlo como una forma rápida de ver cambios y de editar, es cierto que puede seros de mucha utilidad.

AVISO: he experimentado muchos problemas en navegadores Chrome en varios ordenadores, aunque en mi teléfono Android con Chrome sí funciona. Desde problemas con las colisiones hasta no poder cargar la escena. Recomiendo probar varios navegadores con el ejemplo que os dejo en las referencias antes de poneros a probar cosas.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: portátil MacBook Pro 15′ (2 Ghz Intel Core I7, 8GB DDR3).
  • Sistema operativo: Mac OS Sierra 10.12.5
  • A-Frame v0.6.0

3. El inspector

El inspector de A-Frame, como su nombre indica, nos permite ver y crear gráficamente escenas de A-Frame. Es como un inspector de páginas web pero orientado a A-Frame.

3.1. Instalación local

Para instalarlo en local es tan sencillo como ir a su repositorio en GitHub y clonarlo en donde queramos. Una vez clonado, abrimos la terminal dentro del proyecto y ejecutamos lo siguiente:

Una vez que se haya iniciado el inspector, podremos acceder a él mediante la url http://localhost:3333. En caso del ejemplo sería http://localhost:3333/example/.

También podemos añadir nuestros proyectos de A-Frame tal como se hace con el ejemplo, se copia la carpeta del proyecto en la del proyecto del inspector y tan solo habría que ir a la dirección http://localhost:3333/miproyecto/

3.2. ¿Qué nos permite hacer?

Para ver las posibilidades que nos aporta el inspector, usaremos el ejemplo que viene por defecto.

Hay un atajo de teclado para poder cambiar entre el inspector y la escena: Ctrl + Alt + I.

Desde el principio podemos observar la estructura de la escena en la parte izquierda de la ventana. Aquí podemos crear nuevas entidades o seleccionarlas. También tiene unos iconos para poder copiar o eliminar la entidad; estos aparecen si seleccionamos la entidad.

Como ya podéis haber visto, tenemos el editor gráfico donde es representada toda la escena con sus texturas correspondientes. Conviene mencionar que los objetos con animaciones se encuentran inmóviles.

Finalmente, y no lo menos importante, si hacemos click sobre alguna entidad, nos aparecerá a la derecha un menú con todas las propiedades de dicha entidad. También aparecen unas flechas apuntando a las 3 dimensiones, con las que podremos mover el objeto por la escena.

Mediante las propiedades podemos editar completamente el comportamiento y apariencia de las entidades, desde componentes hasta texturas y efectos.

4. ¡Manos a la obra!

Una vez que hemos visto el inspector, ya podemos comenzar a crear nuestro minijuego. Nuestros objetivos serán los siguientes:

  • Añadiremos un fantasma (enemigo) que está creado con una herramienta externa y lo animaremos un poco.
  • Trataremos el problema del movimiento por el mapa.
  • Crearemos una interfaz para poder matar al enemigo que hemos creado y para ver nuestra vida.
  • Crearemos la interacción entre entidades para poder atacar y ser atacados o incluso morir.
  • Crearemos las colisiones tanto del fantasma como del resto de elementos

Una vez tenemos claros nuestros objetivos, pasamos a prepararlo todo. Para ello, según hemos visto anteriormente, iniciaremos el inspector de A-Frame y usaremos el ejemplo que nos aporta como base; así podremos hacer nuestros experimentos y probarlos en una escena ya creada y que tenemos la certeza de que funciona. También podemos duplicar la carpeta “examples” que se encuentra en el inspector (que es el proyecto de ejemplo) y le cambiamos el nombre; de esta manera podremos modificar el nuevo proyecto copiado del ejemplo y tener el ejemplo original intacto. Otra opción es crear una nueva carpeta y empezar un proyecto desde cero, pero hay que tener en cuenta que los proyectos tienen que encontrarse dentro de nuestra carpeta del inspector. Accederemos a ellos con la URL correspondiente, como por ejemplo http://localhost:3333/miproyecto.

Puede ocurrir, y en mi caso ocurrió, que el inspector o la aplicación de A-Frame que queramos iniciar con el comando “npm start” no funcione, haciendo que en la consola nos salgan errores. Una solución común es actualizar node.js y npm, ya que quizás no tengas versiones compatibles.

Una vez abierto el inspector en el proyecto que vamos a cambiar o crear, ya podemos empezar a añadir un modelo creado externamente con sus texturas.

4.1. Añadiendo modelos propios con texturas

En mi caso creé el modelo usando MagicaVoxel (es gratuito), pero pueden usarse otros programas como Blender por ejemplo, que es open source y completamente gratis.

Aquí tenemos terminado a nuestro nuevo amiguito:

Algunos quizás ya sepan que es necesario exportar a nuestro fantasma y muchas herramientas nos permiten hacerlo a varios formatos. Nosotros exportaremos nuestro modelo a “.obj”, y en consecuencia se generará un archivo “.mtl” y otro “.png” aunque solo usaremos el “.obj” y el “.png”.

Aprovechando el inspector, añadiremos el modelo y sus texturas mediante este. Comenzaremos abriendo el inspector —en caso de que no lo tengamos ya— pulsando “Ctrl+Alt+I” y creando una nueva entidad (arriba a la izquierda del inspector encontramos un “+” para crearla). A esta nueva entidad le daremos el ID “Ghost01” y añadiremos 2 componentes: Material y OBJ-Model.

Con el componente OBJ-Model podemos añadir un mtl y un obj, así que en una carpeta llamada “resources”, que crearemos dentro de nuestro proyecto en caso de que no exista, meteremos el obj del modelo que hemos creado y lo referenciaremos en el componente de la entidad escribiendo “resources/ghost.obj”. Esto debería hacer visible en el inspector, en la posición donde se encuentre la entidad, a nuestro fantasma, pero sin textura y de un tamaño gigantesco, cosa que solucionamos variando su escala a 0.1. El componente debería quedarnos de la siguiente forma:

Para añadir la textura tenemos el componente Material en el cual buscaremos la propiedad “src”, que si hacemos click en el rectángulo de su derecha nos abrirá la siguiente ventana para subir las texturas:

Finalmente ya tendremos nuestro fantasma con modelo y texturas propiamente aplicadas. Cabe destacar que no hemos añadido el archivo mtl, ya que no lo coge bien, quitando la textura. La aplicación de los distintos archivos dependerá muy probablemente del programa que los ha exportado, ya que es posible que los que uséis Blender sí tengáis que añadir el mtl. Podemos modificar otras propiedades como la posición y la rotación para colocarlo donde queramos.

CIUDADO: antes de continuar hay que tener presente que, al igual que un inspector de HTML, el inspector de A-Frame no toca tu proyecto, es decir, hay que darle al botón copiar de la entidad, ubicado en la esquina superior derecha de las propiedades de la misma, tal como se muestra en la siguiente imagen:

Una vez copiado, pegaremos el código en nuestro index.html, que será el que tenga todo el código HTML. Todas las entidades de A-Frame deben encontrarte dentro de “a-scene”

He aprovechado también para cambiar la escena de tal forma que he añadido muros (entidad con forma de box o caja) con texturas y he cambiado la textura y la forma del suelo para asemejarlo un poco más con los muros. Aunque por el momento podamos atravesar los muros, lo resolveremos más adelante. La luz también la modifiqué pero realmente configurarla a vuestro gusto. El código HTML resultante:

4.2. Una animación sencilla

Ahora tocaremos un poco el tema de las animaciones, pues en A-Frame estas pueden resumirse como un movimiento entre dos puntos, con propiedades como el número de repeticiones, solo de ida, etc.

Ya que tenemos un fantasma vamos a hacer que este suba y baje un poco para que de la sensación de levitación. Esto sería una animación en el sitio, pero también puede usarse para moverlo de un punto A a un punto B de forma progresiva, sin teletransportarlo de repente.

Esta vez vamos a hacerlo a mano, sobre el index.html, añadiendo dentro de nuestra entidad con ID “Ghost01” un tag con el cual especificaremos que la posición B sea un poco más arriba de su posición original. Especificar solo el “to” sin el “from” hace que la posición from o A sea la original, es decir, la posición en la que tenemos a nuestro fantasma.

Con el atributo repeat especificamos que nunca acabe, con direction=”alternate-reverse” especificamos que sea de ida y vuelta (de A a B y de vuelta a A) y el easing nos permite decir cómo va a ser ese movimiento, en nuestro caso, lineal. Con la duración (dur) podemos regular la velocidad con la que recorre ese camino entre ambos puntos, ya que la modifica para poder recorrer esa distancia en el tiempo especificado.

Una vez añadido volvemos al inspector y entramos a la escena pulsando “Ctrl+Alt+I” de nuevo, miramos a nuestro fantasma y lo veremos levitando arriba y abajo sin parar.

Las animaciones pueden activarse y pararse mediante eventos e incluso existe un componente que permite encadenarlas, pudiendo ser usado para movimientos encadenados entre otras cosas.

5. El problema del movimiento en VR

Habiendo visto cómo podemos hacer que una entidad se mueva, viene el siguiente paso: ¿cómo se moverá el usuario? Esto, sin duda alguna y por el momento, es un problema muy serio de la realidad virtual, ya que solo las gafas (que sin contar Cardboard no es que sean baratas actualmente) no nos permiten movernos. Se han realizado algunos avances, como darle funcionalidad a un mando en el HTC Vive para teletransportar al usuario de un lugar a otro o moverlo progresivamente, pero no todos los usuarios tienen por qué usar el HTC Vive.

Pensándolo y buscando posibles soluciones encontré dos opciones: mover al usuario mediante un sistema de esferas que con mirarlas lo muevan y dejar el sistema de movimiento WASD para los que no usan gafas de VR o los que pueden usarlas junto a un teclado.

Los controles WASD ya vienen incluidos en la cámara de ejemplo, que además viene con un cursor y un raycaster para poder interactuar con otras identidades. En el primer caso va a ser algo más complicado, ya que tendremos que posicionar nuestras esferas por toda la escena y dotarlas de dicha funcionalidad.

Primero crearemos, usando JavaScript, un componente de AFRAME desde cero, el cual generará un eventListener activado al hacer click que tiene como función teletransportar al usuario (la cámara con ID “player”) a su posición.

Mediante este código podemos aprender varias cosas. Todo componente tiene un nombre, en este caso es “static-movement”. El “schema” es una lista con las propiedades del componente que pueden tener especificados valores por defecto. En nuestro caso, “init” es un método que especifica lo que se realiza al crearse la entidad que tiene el componente añadido. Pueden crearse métodos con cualquier nombre, pero hay que tener en cuenta que hay algunos que se ejecutan de forma especial como “init”, “update”, “destroy”, “tick”, etc.

El objeto “el” es la entidad y a esa entidad le añadimos el listener que, como observamos, obtenemos, como en HTML, la cámara con ID “player” y editamos su posición para que esta sea la de la entidad que tiene el componente asociado. También imprimimos en consola un mensaje para saber que funciona y que es esa función la que se ejecuta.

Ahora que tenemos el componente creado lo guardamos como “movement.js” y lo añadimos en nuestro index.html. Para poder asignar un componente a una entidad tan solo hay que añadir un atributo con el mismo nombre que el del componente seguido de un “=” y sus propiedades correspondientes, en caso de no tener, solo con poner su nombre bastaría.

Ya que queremos poder movernos por toda la sala que hemos creado con los muros, creamos un total de cuatro esferas de reducido tamaño:

Ahora ya podemos entrar de nuevo a nuestra escena a probar nuestras nuevas esferas.

Si lo probáis veréis que nos teletransporta, pero… ¡no nos deja en el suelo! De momento lo dejaremos así, ya que añadiremos físicas más adelante que nos devolverán al suelo por nuestro propio peso.

6. Interfaces

Tenemos un fantasma y unas esferas que nos permiten movernos por el mapa, pero ¿cómo lo matamos? Aquí entra en juego el sistema de “combate”, en el que hacemos daño al fantasma para matarlo y el fantasma nos responde quitándonos vida. Empezaremos creando las interfaces tanto del fantasma como de nuestra vida.

Para el fantasma crearemos tres botones (Atacar, Defender y Evadir) y una interfaz para mostrar la vida del fantasma. Para ello crearemos entidades de texto y, ya que no todo lo que ocupe dicho texto es clickable, añadiremos por cada texto un plane que los dote de mayor visibilidad de un área completamente clickable. Haremos también otra entidad que sirva de wrapper para toda la interfaz del fantasma, ya que la posición de todos los componentes pasaría a estar ligada a este wrapper y moviendo el wrapper moveríamos toda la interfaz como conjunto.

Cabe destacar el ID del texto de la vida del fantasma, ya que para poder cambiar su valor para actualizarlo es necesario acceder a este, pero nos vemos obligados a establecer un ID a cada texto de vida de cada enemigo y mediante JavaScript programarlo para cumplir con DRY (Don’t Repeat Yourself), ya que si no habría que programar un componente para cada enemigo, y eso no es fácilmente mantenible.

Solo implementaremos más adelante la lógica del botón “Atacar”, por eso se muestra el atributo “attack-button”.

También añadiremos una interfaz para ver nuestra vida, al igual que hacemos con nuestro fantasma:

Como la interfaz de la vida se encuentra “dentro” de la cámara (el usuario), esto hace que su posición sea relativa a la de la cámara, fijándose así en la pantalla del usuario. Debido a que la vista en modo VR vería nuestra perspectiva con respecto al inspector y al modo fuera de VR, se deberá ajustar posteriormente la posición de todos los elementos.

7. Interacción entre entidades

Tenemos interfaz, pero ahora queda darle vida. Mediante JavaScript crearemos componentes para crear interacción entre las interfaces con las entidades que las contienen.

Para comenzar crearemos el archivo “enemy.js” y lo añadiremos al index de nuestro proyecto. En este nuevo archivo implementaremos la lógica de los enemigos; en nuestro caso solo el fantasma.

Este componente nos da un buen ejemplo de cómo añadir propiedades, que en este caso nos permiten decir cuánta vida tendrá el enemigo y el id de dicho enemigo (si es Ghost01 tomará el valor 1).

La propiedad id nos permite, como puede verse en el método updateHealthText, obtener el texto que muestra la vida de dicho enemigo de forma general, es decir, cualquier enemigo podrá actualizar su vida sin problemas y sin duplicar código, pero claro, a coste de seguir una regla de nomenclatura dándoles ids con el esquema “text-health-{ID}”.

También es destacable la función “tick”, que se ejecuta en cada frame, con lo cual se ejecuta constantemente. Esta función la hemos implementado para que en cuanto su vida llegue a 0 se elimine la entidad, haciéndola desaparecer.

Añadimos este componente a nuestra entidad fantasma, añadiendo enemy=”health: 30; id: 1″ como atributo.

Ahora haremos un componente en el archivo “player.js” y se lo asignaremos a la cámara con id “player” tal como hemos hecho con el fantasma.

Finalmente, dotaremos de funcionalidad al botón atacar de nuestro fantasma en nuestro nuevo archivo “handleEvents.js” y la vida del jugador:

La propiedad targetEntity sirve para decirle al componente a qué entidad hay que restarle vida y debe atacar al usuario.

Ahora solo nos queda añadir el componente “attack-button” al botón de atacar del fantasma y probarlo.

El resultado lo podemos ver mediante el siguiente gif:


8. Sonidos

Vamos a añadirle algún detalle más, y en este caso trataremos el sonido, que tiene su “truco”.

Leyendo en páginas de internet se ve que hay problemas con el sonido, que no se reproduce o cosas así. Principalmente se refieren a atributos de “sound” como el autoplay. En mi caso surgió un ¿bug? bastante extraño que no he sido capaz de solucionar (además de que solo pude reproducir el audio programáticamente), quizás por cómo trabaja A-Frame con el audio o simplemente sea mi navegador, así que lo veremos más adelante.

Añadiremos una música de fondo para dar algo de ambiente y un efectillo para cuando nos demos de tortas con un enemigo. La música que empleo de fondo la podéis encontrar aquí y el efecto de golpe lo podéis encontrar aquí. Empezaremos por la música de fondo.

Primero hay que cargar el asset de audio para que precargue correctamente y no surjan problemas de sincronización.

A continuación, ya que la música de fondo la escucha el jugador y no puede haber efecto Doppler, lo colocaremos como atributo de la cámara para que se fije al usuario.

Solo nos queda empezar a reproducir la música en cuanto aparece el usuario, con lo cual la función “init” de nuestro componente “player” (player.js) quedaría de la siguiente forma:

Y os preguntaréis, ¿por qué dos veces el mismo audio? Por algún motivo el audio no se reproduce si no tenemos la entidad de debajo del comentario “BG Music”, solo la del atributo de la cámara se escuchará, sin importar cuál de las dos reproduzcamos. La verdad es que es muy extraño cómo pueden afectarse entre ambas entidades, incluso sin tener relación padre-hijo.

Es un apaño “feo” pero por lo menos sabemos cómo reproducir audio en bucle y desde el comienzo. Ahora viene el efecto de sonido que se reproducirá cuando ataquemos al fantasma. Para ello realizaremos lo mismo que para la música de fondo, pero esta vez no pondremos ni loop ni autoplay, ya que lo controlaremos mediante JavaScript. Cuando ataquemos al fantasma reproduciremos el sonido de la siguiente forma en la función “reduceHealth” de “enemy.js”:

Sí, hay que volver a duplicar la entidad del sonido, cosa que me parece muy molesta ya que es innecesario. Supongo que se tratará de algún bug que corregirán en versiones siguientes.

Probemos nuestros sonidos y ¡disfrutemos de matar fantasmas a puñetazos!

9. Colisiones

Hemos avanzado mucho, hasta el punto de tener algo jugable, en donde podemos matar a enemigos repartidos por un mapa sin necesidad de tener piernas. Todo esto está muy bien, pero hay algo que aún falta, ¡atravesamos paredes!

Aunque para los que no usan teclado realmente no debería implicar nada (¿cómo vas a atravesar una pared si no puedes moverte?), esto es un problema en caso de alguien que pueda moverse. Lo resolveremos creando colisiones y para ello emplearemos los componentes Aframe-extras y aframe-physics-system o sistema de físicas. El sistema de físicas, basado en Canon.js, no sólo dotará de colisiones sino también de gravedad lo que nos permitirá volver al suelo cada vez que nos teletransportemos a una esfera.

Para poder usar este componente simplemente descargamos la versión minificada y la añadimos como cualquier archivo JavaScript. El sistema de físicas contiene varios componentes que añadiremos para poder dotar al usuario y al resto de entidades de colisiones.

Primero añadiremos el componente kinematic-body (de aframe-extras) a nuestra cámara con un radio de 0.5 (además de una altura de 1.6) ya que por defecto nos vuelve muy anchos.

El componente kinematic-body habla por sí mismo, pero en resumidas cuentas convierte nuestra cámara en un cuerpo sólido con físicas propias (se ve afectado por la gravedad).

A continuación, para el resto de entidades añadiremos el componente static-body, el cual por defecto elige automáticamente la mejor forma para su “caja” de colisiones. Estas entidades serán el suelo, las paredes y el fantasma.

Si habéis probado la escena solo habiendo añadido el componente “kinematic-body”, veréis como caéis al infinito, ya que ni siquiera el suelo es considerado sólido, y al haber gravedad la cámara ya no flota “por arte de magia”. Para el suelo y las paredes simplemente hay que añadir el atributo “static-body” (de aframe-physics-system), no es necesario especificar nada más. Si queremos ver que funciona y cómo son las cajas de colisiones, tendremos que añadir el atributo physics=”debug: true”. Si volvemos a entrar a la escena, podremos ver las cajas de colisiones con líneas rojas.

Ahora ya sí que estamos encerrados entre los muros que hemos creado, con un fantasma delante que podemos matar, ¿o no? Vamos a hacer que el fantasma bloquee el camino y para ello hay que dotarlo de su propia caja de colisiones, pero este es un caso algo especial, ya que es un modelo que hemos cargado y nos un problema.

Si añadimos “static-body” a nuestro simpático fantasma veremos cómo de forma automática elige “box” como forma y el cálculo de esta caja es erróneo, ya que solo rodea a la interfaz. Para solucionar este problema de cálculo añadiremos una caja que colocaremos detrás del fantasma, pero que este la incluya, para que la caja de colisiones se calcule para incluir tanto a toda su interfaz como a dicha caja, creando una caja de colisiones que engloba todo, incluido al fantasma. Si hacemos esta caja invisible, será como si nada hubiera pasado, tendremos una caja de colisiones correcta.

Nuestra entidad fantasma quedaría de la siguiente forma:

Ahora que ya hemos creado las colisiones de todo lo necesario, solo queda probarlas y ajustar valores para que quede todo a nuestro gusto.

A partir de este punto podemos hacer crecer nuestra pequeña prueba y añadir más muros, más fantasmas, más tipos de enemigos o todo lo que queramos hacer.

10. Conclusiones

Como hemos estado viendo a lo largo de este tutorial, las posibilidades que nos ofrece A-Frame son, como mínimo, decentes. Es verdad que se encuentra lejos de estar completo, aunque no es algo que no se pueda solucionar.

Uno de los principales inconvenientes, que espero que amplíen, son las animaciones. Las animaciones son algo básicas, fáciles de usar para algo simple pero no para tareas más sofisticadas. Un ejemplo lo encontraríamos a la hora de hacer que nuestro fantasma se moviera, si queremos que levite y se mueva a la vez nos resulta imposible ya que sólo se ejecuta una animación simultáneamente, habría que moverlo según un vector que sea la combinación de ambos movimientos, cosa que complica la tarea. Lo bueno de su simplicidad es que se pueden crear animaciones de forma dinámica mediante el método “setAttribute”.

Otra mejora sería el crear una entidad interfaz o algo similar que nos permita componer interfaces de forma más rápida, sin tanto ajuste.

Aunque lo anterior se base en mi experiencia, no es absoluto. A-Frame es flexible y nos permite realizar cualquier idea rápidamente, aunque tenga sus posibles inconvenientes. Espero que evolucione más y nos muestre buenos resultados.

11. Referencias