Cómo construir un juego Endless Runner en realidad virtual (Parte 3)
Publicado: 2022-03-10Y así continúa nuestro viaje. En esta parte final de mi serie sobre cómo crear un juego de realidad virtual de corredor sin fin, te mostraré cómo puedes sincronizar el estado del juego entre dos dispositivos, lo que te acercará un paso más a la creación de un juego multijugador. Presentaré específicamente MirrorVR, que es responsable de manejar el servidor de mediación en la comunicación de cliente a cliente.
Nota : este juego se puede jugar con o sin auriculares VR. Puede ver una demostración del producto final en ergo-3.glitch.me.
Para comenzar, necesitará lo siguiente.
- Acceso a Internet (específicamente a glitch.com);
- Un proyecto Glitch completado de la parte 2 de este tutorial. Puede comenzar desde el producto terminado de la parte 2 navegando a https://glitch.com/edit/#!/ergo-2 y haciendo clic en "Remix to edit";
- Un visor de realidad virtual (opcional, recomendado). (Uso Google Cardboard, que se ofrece a $ 15 por pieza).
Paso 1: puntuación de visualización
El juego tal como está funciona como mínimo, donde el jugador tiene un desafío: evitar los obstáculos. Sin embargo, fuera de las colisiones de objetos, el juego no proporciona información al jugador sobre el progreso en el juego. Para remediar esto, implementará la visualización de puntaje en este paso. La partitura será un objeto de texto grande colocado en nuestro mundo de realidad virtual, en lugar de una interfaz pegada al campo de visión del usuario.
En la realidad virtual, en general, la interfaz de usuario se integra mejor en el mundo en lugar de quedarse pegada a la cabeza del usuario.

Comience agregando el objeto a index.html . Agregue una combinación de text
, que se reutilizará para otros elementos de texto:
<a-assets> ... <a-mixin text=" font:exo2bold; anchor:center; align:center;"></a-mixin> ... </a-assets>
A continuación, agregue un elemento de text
a la plataforma, justo antes del jugador:
<!-- Score --> <a-text value="" mixin="text" height="40" width="40" position="0 1.2 -3" opacity="0.75"></a-text> <!-- Player --> ...
Esto agrega una entidad de texto a la escena de realidad virtual. El texto no está visible actualmente porque su valor está establecido en vacío. Sin embargo, ahora completará la entidad de texto dinámicamente, utilizando JavaScript. Vaya a activos/ergo.js . Después de la sección de collisions
, agregue una sección de score
y defina una serie de variables globales:
-
score
: la puntuación actual del juego. -
countedTrees
: ID de todos los árboles que se incluyen en la puntuación. (Esto se debe a que las pruebas de colisión pueden activarse varias veces para el mismo árbol). -
scoreDisplay
: referencia al objeto DOM, correspondiente a un objeto de texto en el mundo de la realidad virtual.
/********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;
A continuación, defina una función de configuración para inicializar nuestras variables globales. En la misma línea, defina una función de teardown
.
... var scoreDisplay; function setupScore() { score = 0; countedTrees = new Set(); scoreDisplay = document.getElementById('score'); } function teardownScore() { scoreDisplay.setAttribute('value', ''); }
En la sección Game
, actualice gameOver
, startGame
y window.onload
para incluir la configuración y el desmontaje de la puntuación.
/******** * GAME * ********/ function gameOver() { ... teardownScore(); } function startGame() { ... setupScore(); addTreesRandomlyLoop(); } window.onload = function() { setupScore(); ... }
Defina una función que incremente la puntuación de un árbol en particular. Esta función verificará con countedTrees
para asegurarse de que el árbol no se cuente dos veces.
function addScoreForTree(tree_id) { if (countedTrees.has(tree_id)) return; score += 1; countedTrees.add(tree_id); }
Además, agregue una utilidad para actualizar la visualización de la puntuación utilizando la variable global.
function updateScoreDisplay() { scoreDisplay.setAttribute('value', score); }
Actualice la prueba de colisión en consecuencia para invocar esta función de aumento de puntaje cada vez que un obstáculo haya pasado al jugador. Todavía en assets/ergo.js
, navegue a la sección de collisions
. Agregue la siguiente verificación y actualización.
AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_LINE_END) { addScoreForTree(tree_id); updateScoreDisplay(); } }) } })
Finalmente, actualice la pantalla de puntaje tan pronto como comience el juego. Vaya a la sección Game
y agregue updateScoreDisplay();
para startGame
el juego:
function startGame() { ... setupScore(); updateScoreDisplay(); ... }
Asegúrese de que assets/ergo.js e index.html coincidan con los archivos de código fuente correspondientes. Luego, navegue a su vista previa. Deberías ver lo siguiente:

Esto concluye la visualización de la puntuación. A continuación, agregaremos los menús de inicio y Fin de juego adecuados, para que el jugador pueda volver a jugar el juego como lo desee.
Paso 2: Agregar menú de inicio
Ahora que el usuario puede realizar un seguimiento del progreso, agregará los toques finales para completar la experiencia del juego. En este paso, agregará un menú Inicio y un menú Game Over , lo que permitirá que el usuario inicie y reinicie los juegos.
Comencemos con el menú Inicio donde el jugador hace clic en el botón "Inicio" para comenzar el juego. Para la segunda mitad de este paso, agregará un menú Game Over , con un botón "Reiniciar":

Navegue a index.html en su editor. Luego, busque la sección Mixins
. Aquí, agregue el title
mixin, que define estilos para texto particularmente grande. Usamos la misma fuente que antes, alineamos el texto al centro y definimos un tamaño apropiado para el tipo de texto. (Tenga en cuenta a continuación que el anchor
es donde un objeto de texto está anclado a su posición).
<a-assets> ... <a-mixin text=" font:exo2bold; height:40; width:40; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
A continuación, agregue un segundo mixin para encabezados secundarios. Este texto es un poco más pequeño pero por lo demás es idéntico al título.
<a-assets> ... <a-mixin text=" font:exo2bold; height:10; width:10; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Para la tercera y última combinación, defina propiedades para texto descriptivo, incluso más pequeño que los encabezados secundarios.
<a-assets> ... <a-mixin text=" font:exo2bold; height:5; width:5; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Con todos los estilos de texto definidos, ahora definirá los objetos de texto del mundo. Agregue una nueva sección de Menus
debajo de la sección Score
, con un contenedor vacío para el menú Inicio :
<!-- Score --> ... <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> </a-entity> </a-entity>
Dentro del contenedor del menú de inicio, defina el título y un contenedor para todo el texto que no sea el título:
... <a-entity ...> <a-entity position="0 1 0"> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>
Dentro del contenedor para texto sin título, agregue instrucciones para jugar el juego:
<a-entity...> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> </a-entity>
Para completar el menú Inicio , agregue un botón que diga "Inicio":
<a-entity...> ... <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity>
Vuelva a verificar que el código HTML de su menú Inicio coincida con lo siguiente:
<!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> <a-entity position="0 1 0"> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>
Navegue a su vista previa y verá el siguiente menú Inicio :

Todavía en la sección Menus
(directamente debajo del menú de start
), agregue el menú game-over
usando los mismos complementos:
<!-- Menus --> <a-entity> ... <a-entity position="0 1.1 -3"> <a-text value="?" mixin="heading" position="0 1.7 0"></a-text> <a-text value="Score" mixin="copy" position="0 1.2 0"></a-text> <a-entity> <a-text value="Restart" mixin="heading" position="0 0.7 0"></a-text> <a-box position="0 0.6 -0.05" width="2" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="Game Over" mixin="title"></a-text> </a-entity> </a-entity>
Navegue a su archivo JavaScript, assets/ergo.js . Cree una nueva sección de Menus
antes de la sección de Game
. Además, defina tres funciones vacías: setupAllMenus
, hideAllMenus
y showGameOverMenu
.
/******** * MENU * ********/ function setupAllMenus() { } function hideAllMenus() { } function showGameOverMenu() { } /******** * GAME * ********/
A continuación, actualice la sección Game
en tres lugares. En gameOver
, muestra el menú Game Over :

function gameOver() { ... showGameOverMenu(); } ``` In `startGame`, hide all menus: ``` function startGame() { ... hideAllMenus(); }
A continuación, en window.onload
, elimine la invocación directa a startGame
y, en su lugar, llame a setupAllMenus
. Actualice su oyente para que coincida con lo siguiente:
window.onload = function() { setupAllMenus(); setupScore(); setupTrees(); }
Vuelva a la sección Menu
. Guarde referencias a varios objetos DOM:
/******** * MENU * ********/ var menuStart; var menuGameOver; var menuContainer; var isGameRunning = false; var startButton; var restartButton; function setupAllMenus() { menuStart = document.getElementById('start-menu'); menuGameOver = document.getElementById('game-over'); menuContainer = document.getElementById('menu-container'); startButton = document.getElementById('start-button'); restartButton = document.getElementById('restart-button'); }
A continuación, vincula los botones "Inicio" y "Reiniciar" para startGame
el juego:
function setupAllMenus() { ... startButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame); }
Defina showStartMenu
e invóquelo desde setupAllMenus
:
function setupAllMenus() { ... showStartMenu(); } function hideAllMenus() { } function showGameOverMenu() { } function showStartMenu() { }
Para completar las tres funciones vacías, necesitará algunas funciones auxiliares. Defina las dos funciones siguientes, que acepta un elemento DOM que representa una entidad VR A-Frame y lo muestra u oculta. Defina ambas funciones arriba de showAllMenus
:
... var restartButton; function hideEntity(el) { el.setAttribute('visible', false); } function showEntity(el) { el.setAttribute('visible', true); } function showAllMenus() { ...
Primero hideAllMenus
. Eliminará los objetos de la vista, luego eliminará los detectores de clics para ambos menús:
function hideAllMenus() { hideEntity(menuContainer); startButton.classList.remove('clickable'); restartButton.classList.remove('clickable'); }
En segundo lugar, showGameOverMenu
. Aquí, restaura el contenedor para ambos menús, así como el menú Game Over y el botón "Reiniciar". Sin embargo, elimine el detector de clics del botón 'Inicio' y oculte el menú 'Inicio'.
function showGameOverMenu() { showEntity(menuContainer); hideEntity(menuStart); showEntity(menuGameOver); startButton.classList.remove('clickable'); restartButton.classList.add('clickable'); }
Tercero, showStartMenu
. Aquí, invierta todos los cambios que efectuó showGameOverMenu
.
function showStartMenu() { showEntity(menuContainer); hideEntity(menuGameOver); showEntity(menuStart); startButton.classList.add('clickable'); restartButton.classList.remove('clickable'); }
Vuelva a verificar que su código coincida con los archivos fuente correspondientes. Luego, navegue a su vista previa y observará el siguiente comportamiento:

Con esto concluyen los menús Inicio y Fin del juego .
¡Felicidades! Ahora tiene un juego en pleno funcionamiento con un comienzo y un final adecuados. Sin embargo, nos queda un paso más en este tutorial: necesitamos sincronizar el estado del juego entre los diferentes dispositivos de los jugadores. Esto nos acercará un paso más a los juegos multijugador.
Paso 3: sincronizar el estado del juego con MirrorVR
En un tutorial anterior, aprendió cómo enviar información en tiempo real a través de sockets para facilitar la comunicación unidireccional entre un servidor y un cliente. En este paso, construirá sobre un producto completo de ese tutorial, MirrorVR, que maneja el servidor de mediación en la comunicación de cliente a cliente.
Nota : Puede obtener más información sobre MirrorVR aquí.
Navegue hasta index.html . Aquí, cargaremos MirrorVR y agregaremos un componente a la cámara, lo que indica que debe reflejar la vista de un dispositivo móvil cuando corresponda. Importe la dependencia de socket.io y MirrorVR 0.2.3.
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script> <script src="https://cdn.jsdelivr.net/gh/alvinwan/[email protected]/dist/mirrorvr.min.js"></script>
A continuación, agregue un componente, camera-listener
, a la cámara:
<a-camera camera-listener ...>
Vaya a activos/ergo.js . En este paso, el dispositivo móvil enviará comandos y el dispositivo de escritorio solo reflejará el dispositivo móvil.
Para facilitar esto, necesita una utilidad para distinguir entre dispositivos de escritorio y móviles. Al final de su archivo, agregue una función mobileCheck
después de la shuffle
:
/** * Checks for mobile and tablet platforms. */ function mobileCheck() { var check = false; (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[aw])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); return check; };
Primero, sincronizaremos el inicio del juego. En startGame
, de la sección Juego , agrega una notificación mirrorVR
al final.
function startGame() { ... if (mobileCheck()) { mirrorVR.notify('startGame', {}) } }
El cliente móvil ahora envía notificaciones sobre el inicio de un juego. Ahora implementará la respuesta del escritorio.
En la ventana de carga del oyente, invoque una función setupMirrorVR
:
window.onload = function() { ... setupMirrorVR(); }
Defina una nueva sección encima de la sección Game
para la configuración de MirrorVR:
/************ * MirrorVR * ************/ function setupMirrorVR() { mirrorVR.init(); }
Luego, agregue argumentos de palabras clave a la función de inicialización para mirrorVR. Específicamente, definiremos el controlador para las notificaciones de inicio del juego. Además, especificaremos un ID de habitación; esto asegura que cualquier persona que cargue su aplicación se sincronice inmediatamente.
function setupMirrorVR() { mirrorVR.init({ roomId: 'ergo', state: { startGame: { onNotify: function(data) { hideAllMenus(); setupScore(); updateScoreDisplay(); } }, } }); }
Repita el mismo proceso de sincronización para Game Over . En gameOver
en la sección Game
, agregue una marca para dispositivos móviles y envíe una notificación en consecuencia:
function gameOver() { ... if (mobileCheck()) { mirrorVR.notify('gameOver', {}); } }
Navegue a la sección MirrorVR
y actualice los argumentos de palabras clave con un oyente de gameOver
:
function setupMirrorVR() { mirrorVR.init({ state: { startGame: {... }, gameOver: { onNotify: function(data) { gameOver(); } }, } }) }
A continuación, repita el mismo proceso de sincronización para agregar árboles. Navegue hasta addTreesRandomly
en la sección Trees
. Lleve un registro de qué carriles reciben nuevos árboles. Luego, directamente antes de la directiva de return
, y envíe una notificación en consecuencia:
function addTreesRandomly(...) { ... var numberOfTreesAdded ... var position_indices = []; trees.forEach(function (tree) { if (...) { ... position_indices.push(tree.position_index); } }); if (mobileCheck()) { mirrorVR.notify('addTrees', position_indices); } return ... }
Navegue a la sección MirrorVR
y actualice los argumentos de palabra clave a mirrorVR.init
con un nuevo oyente para árboles:
function setupMirrorVR() { mirrorVR.init({ state: { ... gameOver: {... }, addTrees: { onNotify: function(position_indices) { position_indices.forEach(addTreeTo) } }, } }) }
Finalmente, sincronizamos la puntuación del juego. En updateScoreDisplay
desde la sección Score
, envíe una notificación cuando corresponda:
function updateScoreDisplay() { ... if (mobileCheck()) { mirrorVR.notify('score', score); } }
Actualice la inicialización de mirrorVR
por última vez, con un detector de cambios de puntuación:
function setupMirrorVR() { mirrorVR.init({ state: { addTrees: { }, score: { onNotify: function(data) { score = data; updateScoreDisplay(); } } } }); }
Vuelva a verificar que su código coincida con los archivos de código fuente apropiados para este paso. Luego, navegue a la vista previa de su escritorio. Además, abra la misma URL en su dispositivo móvil. Tan pronto como su dispositivo móvil cargue la página web, su escritorio debería comenzar inmediatamente a reflejar el juego del dispositivo móvil.
Aquí hay una demostración. Observe que el cursor del escritorio no se mueve, lo que indica que el dispositivo móvil controla la vista previa del escritorio.

Esto concluye su proyecto aumentado con mirrorVR.
Este tercer paso introdujo algunos pasos básicos de sincronización del estado del juego; para que esto sea más sólido, puede agregar más controles de cordura y más puntos de sincronización.
Conclusión
En este tutorial, agregó los toques finales a su juego de corredor sin fin e implementó la sincronización en tiempo real de un cliente de escritorio con un cliente móvil, reflejando efectivamente la pantalla del dispositivo móvil en su escritorio. Esto concluye la serie sobre la creación de un juego de corredor sin fin en realidad virtual. Junto con las técnicas de A-Frame VR, ha adquirido modelado 3D, comunicación de cliente a cliente y otros conceptos ampliamente aplicables.
Los siguientes pasos pueden incluir:
- Modelado más avanzado
Esto significa modelos 3D más realistas, potencialmente creados en un software de terceros e importados. Por ejemplo, (MagicaVoxel) simplifica la creación de arte voxel y (Blender) es una solución completa de modelado 3D. - Más complejidad
Los juegos más complejos, como un juego de estrategia en tiempo real, podrían aprovechar un motor de terceros para aumentar la eficiencia. Esto puede significar dejar de lado A-Frame y webVR por completo, y en su lugar publicar un juego compilado (Unity3d).
Otras vías incluyen soporte multijugador y gráficos más ricos. Con la conclusión de esta serie de tutoriales, ahora tiene un marco para explorar más.