Cómo construir un juego de realidad virtual multijugador en tiempo real (Parte 2)
Publicado: 2022-03-10En esta serie de tutoriales, crearemos un juego de realidad virtual multijugador basado en la web, donde los jugadores deberán colaborar para resolver un rompecabezas. En la primera parte de esta serie, diseñamos los orbes que aparecen en el juego. En esta parte de la serie, agregaremos mecánicas de juego y estableceremos protocolos de comunicación entre pares de jugadores.
La descripción del juego aquí es un extracto de la primera parte de la serie: cada par de jugadores recibe un anillo de orbes. El objetivo es "encender" todos los orbes, donde un orbe está "encendido" si está elevado y brillante. Un orbe está "apagado" si está más bajo y tenue. Sin embargo, ciertos orbes "dominantes" afectan a sus vecinos: si cambia de estado, sus vecinos también cambian de estado. El jugador 2 puede controlar orbes pares y el jugador 1 puede controlar orbes impares. Esto obliga a ambos jugadores a colaborar para resolver el rompecabezas.
Los 8 pasos de este tutorial se agrupan en 3 secciones:
- Poblando la interfaz de usuario (Pasos 1 y 2)
- Agregar mecánicas de juego (pasos 3 a 5)
- Configuración de la comunicación (pasos 6 a 8)
Esta parte concluirá con una demostración en línea completamente funcional, para que cualquiera pueda jugar. Utilizará A-Frame VR y varias extensiones de A-Frame.
Puede encontrar el código fuente terminado aquí.
1. Agregue indicadores visuales
Para comenzar, agregaremos indicadores visuales de la ID de un orbe. Inserte un nuevo elemento VR a-text
como el primer elemento secundario de #container-orb0
, en L36.
<a-entity ...> <a-text class="orb-id" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="3 3 3" position="0 -2 -0.25" material="side:double"></a-text> ... <a-entity position...> ... </a-entity> </a-entity>
Las "dependencias" de un orbe son los orbes que alternará, cuando se alterna: por ejemplo, digamos que el orbe 1 tiene como dependencias los orbes 2 y 3. Esto significa que si se alterna el orbe 1, los orbes 2 y 3 también se alternarán. Agregaremos indicadores visuales de dependencias, de la siguiente manera, directamente después .animation-position
.
<a-animation class="animation-position" ... /> <a-text class="dep-right" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="10 10 10" position="0 0 1" material="side:double" ></a-text> <a-text class="dep-left" opacity="0.25"rotation="0 -90 0" value="1" color="#FFF" scale="10 10 10" position="0 0 -3" material="side:double" ></a-text>
Verifique que su código coincida con nuestro código fuente para el Paso 1. Su orbe ahora debería coincidir con lo siguiente:
Esto concluye los indicadores visuales adicionales que necesitaremos. A continuación, agregaremos orbes dinámicamente a la escena de realidad virtual, usando este orbe de plantilla.
2. Agregar orbes dinámicamente
En este paso, agregaremos orbes de acuerdo con una especificación JSON-esque de un nivel. Esto nos permite especificar y generar fácilmente nuevos niveles. Usaremos el orbe del último paso en la parte 1 como plantilla.
Para comenzar, importe jQuery, ya que esto facilitará las modificaciones de DOM y, por lo tanto, las modificaciones a la escena de realidad virtual. Directamente después de la importación de A-Frame, agregue lo siguiente a L8:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Especifique un nivel utilizando una matriz. La matriz contendrá objetos literales que codifican las "dependencias" de cada orbe. Dentro de la etiqueta <head>
, agregue la siguiente configuración de nivel:
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
Por ahora, cada orbe solo puede tener una dependencia a la "derecha" y otra a la "izquierda". Inmediatamente después de declarar los orbs
anteriores, agregue un controlador que se ejecutará al cargar la página. Este controlador (1) duplicará el orbe de la plantilla y (2) eliminará el orbe de la plantilla, utilizando la configuración de nivel proporcionada:
$(document).ready(function() { function populateTemplate(orb, template, i, total) {} function remove(selector) {} for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; var template = $('#template').clone(); template = populateTemplate(orb, template, i, orbs.length); $('#carousel').append(template); } remove('#template'); } function clickOrb(i) {}
A continuación, complete la función de remove
, que simplemente elimina un elemento de la escena de realidad virtual, dado un selector. Afortunadamente, A-Frame observa cambios en el DOM y, por lo tanto, basta con eliminar el elemento del DOM para eliminarlo de la escena de realidad virtual. Rellene la función de remove
de la siguiente manera.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Complete la función clickOrb
, que simplemente activa la acción de clic en un orbe.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
A continuación, comience a escribir la función populateTemplate
. En esta función, comience por obtener el .container
. Este contenedor para el orbe también contiene los indicadores visuales que agregamos en el paso anterior. Además, tendremos que modificar el comportamiento al hacer onclick
del orbe, en función de sus dependencias. Si existe una dependencia izquierda, modifique tanto el indicador visual como el comportamiento al hacer onclick
para reflejar eso; lo mismo es válido para una dependencia a la derecha:
function populateTemplate(orb, template, i, total) { var container = template.find('.container'); var onclick = 'document.querySelector("#light-orb' + i + '").emit("switch");'; if (orb.left || orb.right) { if (orb.left) { onclick += 'clickOrb(' + orb.left + ');'; container.find('.dep-left').attr('value', orb.left); } if (orb.right) { onclick += 'clickOrb(' + orb.right + ');'; container.find('.dep-right').attr('value', orb.right); } } else { container.find('.dep-left').remove(); container.find('.dep-right').remove(); } }
Todavía en la función populateTemplate
, establezca la ID del orbe correctamente en todos los elementos del orbe y su contenedor.
container.find('.orb-id').attr('value', i); container.attr('id', 'container-orb' + i); template.find('.orb').attr('id', 'orb' + i); template.find('.light-orb').attr('id', 'light-orb' + i); template.find('.clickable').attr('data-id', i);
Todavía en la función populateTemplate
, configure el comportamiento al hacer onclick
, configure la semilla aleatoria para que cada orbe sea visualmente diferente y, finalmente, configure la posición de rotación del orbe en función de su ID.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
Al finalizar la función, devuelva la template
con todas las configuraciones anteriores.
return template;
Dentro del controlador de carga de documentos y después de eliminar la plantilla con remove('#template')
, active los orbes que se configuraron para estar activados inicialmente.
$(document).ready(function() { ... setTimeout(function() { for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; if (orb.on) { document.querySelector("#container-orb" + i).click(); } } }, 1000); });
Esto concluye las modificaciones de Javascript. A continuación, cambiaremos la configuración predeterminada de la plantilla a la de un orbe 'apagado'. Cambie la posición y la escala de #container-orb0
a lo siguiente:
position="8 0.5 0" scale="0.5 0.5 0.5"
Luego, cambie la intensidad de #light-orb0
a 0.
intensity="0"
Verifique que su código fuente coincida con nuestro código fuente para el Paso 2.
Su escena de realidad virtual ahora debería presentar 5 orbes, poblados dinámicamente. Además, uno de los orbes debe tener indicadores visuales de dependencias, como a continuación:
Esto concluye la primera sección en la adición dinámica de orbes. En la siguiente sección, pasaremos tres pasos agregando mecánicas de juego. Específicamente, el jugador solo podrá alternar orbes específicos según la identificación del jugador.
3. Agregar estado terminal
En este paso, agregaremos un estado terminal. Si todos los orbes se activan con éxito, el jugador verá una página de "victoria". Para hacer esto, deberá realizar un seguimiento del estado de todos los orbes. Cada vez que se activa o desactiva un orbe, necesitaremos actualizar nuestro estado interno. Digamos que una función de ayuda toggleOrb
el estado de las actualizaciones de Orb para nosotros. Invoque la función toggleOrb
cada vez que un orbe cambie de estado: (1) agregue un detector de clics al controlador de carga y (2) agregue un toggleOrb(i);
invocación a clickOrb
. Finalmente, (3) defina un toggleOrb
vacío.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Para simplificar, usaremos nuestra configuración de nivel para indicar el estado del juego. Use toggleOrb
para alternar el estado de on
del i-ésimo orbe. toggleOrb
puede activar un estado terminal si todos los orbes están encendidos.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Vuelva a verificar que su código coincida con nuestro código fuente para el Paso 3.
Esto concluye el modo "un jugador" para el juego. En este punto, tienes un juego de realidad virtual completamente funcional. Sin embargo, ahora deberá escribir el componente multijugador y fomentar la colaboración a través de la mecánica del juego.
4. Crear objeto de jugador
En este paso, crearemos una abstracción para un jugador con una ID de jugador. Este ID de jugador será asignado por el servidor más adelante.
Por ahora, esto será simplemente una variable global. Directamente después de definir los orbs
, define un ID de jugador:
var orbs = ... var current_player_id = 1;
Vuelva a verificar que su código coincida con nuestro código fuente para el Paso 4. En el siguiente paso, esta ID de jugador se usará para determinar qué orbes puede controlar el jugador.
5. Alternar orbes condicionalmente
En este paso, modificaremos el comportamiento de cambio de orbe. Específicamente, el jugador 1 puede controlar orbes impares y el jugador 2 puede controlar orbes pares. Primero, implemente esta lógica en ambos lugares donde los orbes cambian de estado:
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
En segundo lugar, defina la función allowedToToggle
, justo después de clickOrb
. Si el jugador actual es el jugador 1, los identificadores impares devolverán un valor de verdad y, por lo tanto, el jugador 1 podrá controlar los orbes impares. Lo contrario es cierto para el jugador 2. Todos los demás jugadores no pueden controlar los orbes.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Vuelva a verificar que su código coincida con nuestro código fuente para el Paso 5. De manera predeterminada, el jugador es el jugador 1. Esto significa que usted, como jugador 1, solo puede controlar orbes impares en su vista previa. Esto concluye la sección sobre la mecánica del juego.
En el siguiente apartado facilitaremos la comunicación entre ambos jugadores a través de un servidor.
6. Configuración del servidor con WebSocket
En este paso, configurará un servidor simple para (1) realizar un seguimiento de las identificaciones de los jugadores y (2) transmitir mensajes. Estos mensajes incluirán el estado del juego, para que los jugadores puedan estar seguros de que cada uno ve lo que ve el otro.
Nos referiremos a su index.html
anterior como el código fuente del lado del cliente. Nos referiremos al código en este paso como el código fuente del lado del servidor. Vaya a glitch.com, haga clic en "nuevo proyecto" en la esquina superior derecha y, en el menú desplegable, haga clic en "hello-express".
En el panel de la izquierda, seleccione "package.json" y agregue socket-io
a dependencies
. Su diccionario dependencies
ahora debería coincidir con lo siguiente.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
En el panel de la izquierda, seleccione "index.js" y reemplace el contenido de ese archivo con el siguiente mínimo socket.io Hello World:
const express = require("express"); const app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); /** * Run application on port 3000 */ var port = process.env.PORT || 3000; http.listen(port, function(){ console.log('listening on *:', port); });
Lo anterior configura socket.io en el puerto 3000 para una aplicación express básica. A continuación, defina dos variables globales, una para mantener la lista de jugadores activos y otra para mantener la ID de jugador más pequeña sin asignar.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
A continuación, defina la función getPlayerId
, que genera una nueva ID de jugador y marca la nueva ID de jugador como "tomada" al agregarla a la matriz playerIds
. En particular, la función simplemente marca la identificación del jugador smallestPlayerId
y luego actualiza la identificación del jugador smallestPlayerId
buscando el siguiente entero no tomado más pequeño.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
Defina la función removePlayer
, que actualiza el smallestPlayerId
PlayerId en consecuencia y libera el playerId
proporcionado para que otro jugador pueda tomar esa ID.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Finalmente, defina un par de controladores de eventos de socket que registren nuevos jugadores y eliminen el registro de jugadores desconectados, utilizando el par de métodos anterior.
/** * Handle socket interactions */ io.on('connection', function(socket) { socket.on('newPlayer', function() { socket.playerId = getPlayerId(); console.log("new player: ", socket.playerId); socket.emit('playerId', socket.playerId); }); socket.on('disconnect', function() { if (socket.playerId === undefined) return; console.log("disconnected player: ", socket.playerId); removePlayer(socket.playerId); }); });
Vuelva a verificar que su código coincida con nuestro código fuente para el Paso 6. Esto concluye el registro y la cancelación del registro básico del jugador. Cada cliente ahora puede usar la ID de jugador generada por el servidor.
En el próximo paso, modificaremos el cliente para recibir y usar la identificación del jugador emitida por el servidor.
7. Aplicar ID de jugador
En estos dos próximos pasos, completaremos una versión rudimentaria de la experiencia multijugador. Para comenzar, integre el lado del cliente de asignación de ID de jugador. En particular, cada cliente le pedirá al servidor una identificación de jugador. Vuelva al index.html
del lado del cliente en el que estábamos trabajando en los Pasos 4 y anteriores.
Importe socket.io
en la head
en L7:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
Después del controlador de carga de documentos, cree una instancia del socket y emita un evento newPlayer
. En respuesta, el lado del servidor generará una nueva ID de jugador usando el evento playerId
. A continuación, use la URL para la vista previa de su proyecto Glitch en lugar de lightful.glitch.me
. Le invitamos a utilizar la URL de demostración a continuación, pero, por supuesto, no se reflejarán los cambios de código que realice.
$(document).ready(function() { ... }); socket = io("https://lightful.glitch.me"); socket.emit('newPlayer'); socket.on('playerId', function(player_id) { current_player_id = player_id; console.log(" * You are now player", current_player_id); });
Verifique que su código coincida con nuestro código fuente para el Paso 7. Ahora, puede cargar su juego en dos navegadores o pestañas diferentes para jugar dos lados de un juego multijugador. El jugador 1 podrá controlar orbes impares y el jugador 2 podrá controlar orbes pares.
Sin embargo, tenga en cuenta que alternar orbes para el jugador 1 no afectará el estado del orbe para el jugador 2. A continuación, debemos sincronizar los estados del juego.
8. Sincronizar el estado del juego
En este paso, sincronizaremos los estados del juego para que los jugadores 1 y 2 vean los mismos estados del orbe. Si el orbe 1 está encendido para el jugador 1, también debería estarlo para el jugador 2. En el lado del cliente, anunciaremos y escucharemos los cambios de orbe. Para anunciar, simplemente pasaremos la ID del orbe que se activa.
Antes de ambas invocaciones de toggleOrb
, agregue la siguiente llamada socket.emit
.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
A continuación, escuche los cambios de orbe y cambie el orbe correspondiente. Directamente debajo del detector de eventos del socket playerId
, agregue otro detector para el evento toggleOrb
.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
Esto concluye las modificaciones al código del lado del cliente. Vuelva a verificar que su código coincida con nuestro código fuente para el Paso 8.
El lado del servidor ahora necesita recibir y transmitir la ID de orbe alternada. En el index.js
del lado del servidor, agregue el siguiente agente de escucha. Este detector debe colocarse directamente debajo del detector de disconnect
del zócalo.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Vuelva a verificar que su código coincida con nuestro código fuente para el Paso 8. Ahora, el jugador 1 cargado en una ventana y el jugador 2 cargado en una segunda ventana verán el mismo estado del juego. Con eso, has completado un juego de realidad virtual multijugador. Los dos jugadores, además, deben colaborar para completar el objetivo. El producto final coincidirá con lo siguiente.
Conclusión
Esto concluye nuestro tutorial sobre cómo crear un juego de realidad virtual multijugador. En el proceso, ha tocado varios temas, incluido el modelado 3D en A-Frame VR y experiencias multijugador en tiempo real con WebSockets.
Sobre la base de los conceptos que hemos mencionado, ¿cómo garantizaría una experiencia más fluida para los dos jugadores? Esto podría incluir verificar que el estado del juego esté sincronizado y alertar al usuario en caso contrario. También puede crear indicadores visuales simples para el estado del terminal y el estado de conexión del reproductor.
Dado el marco que hemos establecido y los conceptos que hemos presentado, ahora tiene las herramientas para responder estas preguntas y construir mucho más.
Puede encontrar el código fuente terminado aquí.