Cómo construir un juego Endless Runner en realidad virtual (Parte 2)

Publicado: 2022-03-10
Resumen rápido ↬ Si alguna vez te has preguntado cómo se construyen los juegos con soporte sin teclado para auriculares VR, este tutorial explica exactamente lo que estás buscando. Así es como usted también puede dar vida a un juego de realidad virtual básico y funcional.

En la Parte 1 de esta serie, vimos cómo se puede crear un modelo de realidad virtual con efectos de iluminación y animación. En esta parte, implementaremos la lógica central del juego y utilizaremos manipulaciones del entorno A-Frame más avanzadas para construir la parte del "juego" de esta aplicación. Al final, tendrás un juego de realidad virtual en funcionamiento con un verdadero desafío.

Este tutorial implica una serie de pasos, que incluyen (pero no se limitan a) detección de colisiones y más conceptos de A-Frame como mixins.

  • Demostración del producto final

requisitos previos

Al igual que en el tutorial anterior, necesitará lo siguiente:

  • Acceso a Internet (específicamente a glitch.com);
  • Un proyecto de Glitch completado desde la parte 1. (Puede continuar desde el producto terminado navegando a https://glitch.com/edit/#!/ergo-1 y haciendo clic en "Remix to edit";
  • Un visor de realidad virtual (opcional, recomendado). (Uso Google Cardboard, que se ofrece a $ 15 por pieza).
¡Más después del salto! Continúe leyendo a continuación ↓

Paso 1: Diseñando los Obstáculos

En este paso, diseñas los árboles que usaremos como obstáculos. Luego, agregará una animación simple que mueve los árboles hacia el jugador, como la siguiente:

Árboles de plantilla moviéndose hacia el jugador
Árboles de plantilla moviéndose hacia el jugador (vista previa grande)

Estos árboles servirán como plantillas para los obstáculos que generes durante el juego. Para la parte final de este paso, eliminaremos estos "árboles de plantilla".

Para comenzar, agregue varios mixins A-Frame diferentes. Los mixins son conjuntos de propiedades de componentes de uso común. En nuestro caso, todos nuestros árboles tendrán el mismo color, altura, ancho, profundidad, etc. En otras palabras, todos sus árboles se verán iguales y, por lo tanto, usarán algunos mixins compartidos.

Nota : en nuestro tutorial, sus únicos activos serán mixins. Visite la página de A-Frame Mixins para obtener más información.

En su editor, navegue hasta index.html . Justo después de su cielo y antes de sus luces, agregue una nueva entidad A-Frame para guardar sus activos:

 <a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ...

En su nueva entidad a-assets , comience agregando una mezcla para su follaje. Este mixins define propiedades comunes para el follaje del árbol de plantilla. En definitiva, es una pirámide blanca de sombra plana, para un efecto low poly.

 <a-assets> <a-mixin geometry=" primitive: cone; segments-height: 1; segments-radial:4; radius-bottom:0.3;" material="color:white;flat-shading: true;"></a-mixin> </a-assets>

Justo debajo de su mezcla de follaje, agregue una mezcla para el tronco. Este tronco será un pequeño prisma rectangular blanco.

 <a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>

A continuación, agregue los objetos del árbol de plantilla que usarán estos mixins. Todavía en index.html , desplácese hacia abajo hasta la sección de plataformas. Justo antes de la sección del jugador, agregue una nueva sección de árbol, con tres entidades de árbol vacías:

 <a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...

A continuación, cambie la posición, cambie la escala y agregue sombras a las entidades del árbol.

 <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>

Ahora, rellene las entidades de árbol con un tronco y follaje, utilizando los mixins que definimos anteriormente.

 <!-- Trees --> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity>

Navegue a su vista previa, y ahora debería ver los siguientes árboles de plantilla.

Árboles de plantilla para obstáculos.
Árboles de plantilla para obstáculos (Vista previa grande)

Ahora, anime los árboles desde una ubicación distante en la plataforma hacia el usuario. Como antes, usa la etiqueta a-animation :

 <!-- Trees --> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity>

Asegúrese de que su código coincida con lo siguiente.

 <a-entity...> <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="-0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <!-- Player --> ...

Navegue a su vista previa y ahora verá los árboles moviéndose hacia usted.

Árboles de plantilla moviéndose hacia el jugador
Árboles de plantillas moviéndose hacia el jugadorÁrboles de plantillas moviéndose hacia el jugador (vista previa grande)

Vuelve a tu editor. Esta vez, seleccione assets/ergo.js . En la sección del juego, configure los árboles después de que se haya cargado la ventana.

 /******** * GAME * ********/ ... window.onload = function() { setupTrees(); }

Debajo de los controles, pero antes de la sección Juego, agregue una nueva sección TREES . En esta sección, defina una nueva función setupTrees .

 /************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ...

En la nueva función setupTrees , obtenga referencias a los objetos DOM del árbol de plantilla y haga que las referencias estén disponibles globalmente.

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); }

A continuación, defina una nueva utilidad removeTree . Con esta utilidad, puede eliminar los árboles de plantilla de la escena. Debajo de la función setupTrees , defina su nueva utilidad.

 function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); }

De vuelta en setupTrees , use la nueva utilidad para eliminar los árboles de plantilla.

 function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }

Asegúrese de que las secciones de su árbol y juego coincidan con lo siguiente:

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); } function removeTree(tree) { tree.parentNode.removeChild(tree); } /******** * GAME * ********/ setupControls(); // TODO: AFRAME.registerComponent has to occur before window.onload? window.onload = function() { setupTrees(); }

Vuelva a abrir su vista previa, y sus árboles ahora deberían estar ausentes. La vista previa debe coincidir con nuestro juego al comienzo de este tutorial.

producto terminado parte 1
Producto terminado de la parte 1 (vista previa grande)

Esto concluye el diseño del árbol de plantillas.

En este paso, cubrimos y usamos mixins A-Frame, que nos permiten simplificar el código definiendo propiedades comunes. Además, aprovechamos la integración de A-Frame con el DOM para eliminar objetos de la escena de realidad virtual de A-Frame.

En el próximo paso, generaremos múltiples obstáculos y diseñaremos un algoritmo simple para distribuir árboles entre diferentes carriles.

Paso 2: Obstáculos de desove

En un juego de corredor sin fin, nuestro objetivo es evitar los obstáculos que vuelan hacia nosotros. En esta implementación particular del juego, usamos tres carriles como es más común.

A diferencia de la mayoría de los juegos de corredores sin fin, este juego solo admitirá el movimiento hacia la izquierda y hacia la derecha . Esto impone una restricción a nuestro algoritmo para generar obstáculos: no podemos tener tres obstáculos en los tres carriles, al mismo tiempo, volando hacia nosotros. Si eso ocurre, el jugador tendría cero posibilidades de supervivencia. Como resultado, nuestro algoritmo de generación debe adaptarse a esta restricción.

En este paso, todas nuestras ediciones de código se realizarán en assets/ergo.js . El archivo HTML seguirá siendo el mismo. Vaya a la sección TREES de assets/ergo.js .

Para comenzar, agregaremos utilidades para generar árboles. Cada árbol necesitará una identificación única, que ingenuamente definiremos como la cantidad de árboles que existen cuando se genera el árbol. Comience rastreando el número de árboles en una variable global.

 /********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ...

A continuación, inicializaremos una referencia al elemento DOM del contenedor del árbol, al que nuestra función de generación agregará árboles. Todavía en la sección TREES , agregue una variable global y luego haga la referencia.

 ... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }

Usando tanto el número de árboles como el contenedor del árbol, escriba una nueva función que genere árboles.

 function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ...

Para facilitar su uso más adelante, creará una segunda función que agregue el árbol correcto al carril correcto. Para comenzar, defina una nueva matriz de templates en la sección TREES .

 var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }

Usando esta matriz de plantillas, agregue una utilidad que genere árboles en un carril específico, dado un ID que represente la izquierda, el medio o la derecha.

 function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); }

Navegue a su vista previa y abra su consola de desarrollador. En su consola de desarrollador, invoque la función global addTreeTo .

 > addTreeTo(0); # spawns tree in left lane 
Invocando addTreeTo manualmente
Invocar addTreeTo manualmente (vista previa grande)

Ahora, escribirá un algoritmo que genere árboles al azar:

  1. Elija un carril al azar (que aún no se haya elegido, para este paso de tiempo);
  2. Genere un árbol con cierta probabilidad;
  3. Si se ha generado el número máximo de árboles para este paso de tiempo, deténgase. De lo contrario, repita el paso 1.

Para efectuar este algoritmo, en su lugar, mezclaremos la lista de plantillas y procesaremos una a la vez. Comience definiendo una nueva función, addTreesRandomly , que acepta varios argumentos de palabras clave diferentes.

 function addTreeTo(position_index) { ... } /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { }

En su nueva función addTreesRandomly , defina una lista de árboles de plantilla y baraje la lista.

 function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); }

Desplácese hacia abajo hasta la parte inferior del archivo y cree una nueva sección de utilidades, junto con una nueva utilidad de shuffle . Esta utilidad barajará una matriz en su lugar.

 /******** * GAME * ********/ ... /************* * UTILITIES * *************/ /** * Shuffles array in place. * @param {Array} a items An array containing the items. */ function shuffle(a) { var j, x, i; for (i = a.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)); x = a[i]; a[i] = a[j]; a[j] = x; } return a; }

Navegue de regreso a la función addTreesRandomly en su sección Trees. Agregue una nueva variable numberOfTreesAdded e itere a través de la lista de árboles definida anteriormente.

 function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); }

En la iteración sobre árboles, genere un árbol solo con alguna probabilidad y solo si la cantidad de árboles agregados no excede 2 . Actualice el bucle for de la siguiente manera.

 function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }

Para concluir la función, devuelva el número de árboles agregados.

 function addTreesRandomly( ... ) { ... return numberOfTreesAdded; }

Vuelva a verificar que su función addTreesRandomly coincida con lo siguiente.

 /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); var numberOfTreesAdded = 0; trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); return numberOfTreesAdded; }

Finalmente, para generar árboles automáticamente, configure un temporizador que active la generación de árboles a intervalos regulares. Defina el temporizador globalmente y agregue una nueva función de desmontaje para este temporizador.

 /********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }

A continuación, defina una nueva función que inicialice el temporizador y guarde el temporizador en la variable global previamente definida. El siguiente temporizador se ejecuta cada medio segundo.

 function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }

Finalmente, inicie el temporizador después de que se haya cargado la ventana, desde la sección Juego.

 /******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }

Navega a tu vista previa y verás que los árboles se reproducen al azar. Tenga en cuenta que nunca hay tres árboles a la vez.

Desove de árboles al azar
Árbol que aparece aleatoriamente (Vista previa grande)

Esto concluye el paso de los obstáculos. Hemos tomado con éxito varios árboles de plantillas y generado una cantidad infinita de obstáculos a partir de las plantillas. Nuestro algoritmo de generación también respeta las limitaciones naturales del juego para que sea jugable.

En el siguiente paso, agreguemos pruebas de colisión.

Paso 3: Prueba de colisión

En esta sección, implementaremos las pruebas de colisión entre los obstáculos y el jugador. Estas pruebas de colisión son más simples que las pruebas de colisión en la mayoría de los otros juegos; sin embargo, el jugador solo se mueve a lo largo del eje x, por lo que siempre que un árbol cruce el eje x, verifique si el carril del árbol es el mismo que el del jugador. Implementaremos esta simple verificación para este juego.

Navegue hasta index.html , hasta la sección TREES . Aquí, agregaremos información de carril a cada uno de los árboles. Para cada uno de los árboles, agregue data-tree-position-index= , de la siguiente manera. Además, agregue class="tree" , para que podamos seleccionar fácilmente todos los árboles en la línea:

 <a-entity data-tree-position-index="1" class="tree" ...> </a-entity> <a-entity data-tree-position-index="0" class="tree" ...> </a-entity> <a-entity data-tree-position-index="2" class="tree" ...> </a-entity>

Navegue a assets/ergo.js e invoque una nueva función setupCollisions en la sección GAME . Además, defina una nueva variable global isGameRunning que indique si un juego existente ya se está ejecutando o no.

 /******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ...

Defina una nueva sección COLLISIONS justo después de la sección TREES pero antes de la sección Juego. En esta sección, defina la función setupCollisions.

 /********* * TREES * *********/ ... /************** * COLLISIONS * **************/ const POSITION_Z_OUT_OF_SIGHT = 1; const POSITION_Z_LINE_START = 0.6; const POSITION_Z_LINE_END = 0.7; function setupCollision() { } /******** * GAME * ********/

Como antes, registraremos un componente AFRAME y usaremos el detector de eventos tick para ejecutar el código en cada paso de tiempo. En este caso, registraremos un componente con el player y ejecutaremos comprobaciones contra todos los árboles en ese oyente:

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } }

En el ciclo for , comience por obtener la información relevante del árbol:

 document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); }

A continuación, aún dentro del ciclo for , elimine el árbol si no está a la vista, justo después de extraer las propiedades del árbol:

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }

A continuación, si no se está ejecutando ningún juego, no verifique si hay una colisión.

 document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; }

Finalmente (todavía en el bucle for ), verifica si el árbol comparte la misma posición al mismo tiempo que el jugador. Si es así, llame a una función gameOver aún por definir:

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }

Verifique que su función setupCollisions coincida con lo siguiente:

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } if (!isGameRunning) return; if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }) } }) }

Esto concluye la configuración de la colisión. Ahora, agregaremos algunas sutilezas para abstraer las secuencias startGame y gameOver . Navega a la sección GAME . Actualice el bloque window.onload para que coincida con lo siguiente, reemplazando addTreesRandomlyLoop con una función startGame aún por definir.

 window.onload = function() { setupTrees(); startGame(); }

Debajo de las invocaciones de la función de configuración, cree una nueva función startGame . Esta función inicializará la variable isGameRunning consecuencia y evitará llamadas redundantes.

 window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); }

Finalmente, defina gameOver , que alertará un "¡Juego terminado!" mensaje por ahora.

 function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }

Esto concluye la sección de prueba de colisión del juego de corredor sin fin.

En este paso, nuevamente usamos componentes A-Frame y una serie de otras utilidades que agregamos anteriormente. Además, reorganizamos y abstrajimos adecuadamente las funciones del juego; Posteriormente, aumentaremos estas funciones de juego para lograr una experiencia de juego más completa.

Conclusión

En la parte 1, agregamos controles compatibles con auriculares VR: mira a la izquierda para moverte a la izquierda y a la derecha para moverte a la derecha. En esta segunda parte de la serie, les mostré lo fácil que puede ser crear un juego de realidad virtual básico y funcional. Agregamos la lógica del juego, para que el corredor sin fin coincida con sus expectativas: corra para siempre y tenga una serie interminable de obstáculos peligrosos volando hacia el jugador. Hasta ahora, ha creado un juego que funciona con soporte sin teclado para auriculares de realidad virtual.

Aquí hay recursos adicionales para diferentes controles y auriculares VR:

  • Marco en A para auriculares VR
    Una encuesta de navegadores y auriculares compatibles con A-Frame VR.
  • A-Frame para controladores VR
    Cómo A-Frame no admite controladores, controladores 3DoF y controladores 6DoF, además de otras alternativas para la interacción.

En la siguiente parte, agregaremos algunos toques finales y sincronizaremos los estados del juego , lo que nos acercará un paso más a los juegos multijugador.