Comment créer un jeu de réalité virtuelle multijoueur en temps réel (Partie 2)
Publié: 2022-03-10Dans cette série de didacticiels, nous allons créer un jeu de réalité virtuelle multijoueur basé sur le Web, où les joueurs devront collaborer pour résoudre un casse-tête. Dans la première partie de cette série, nous avons conçu les orbes présentés dans le jeu. Dans cette partie de la série, nous allons ajouter des mécanismes de jeu et mettre en place des protocoles de communication entre paires de joueurs.
La description du jeu ici est extraite de la première partie de la série : Chaque paire de joueurs reçoit un anneau d'orbes. Le but est d'« allumer » tous les orbes, où un orbe est « allumé » s'il est élevé et lumineux. Un orbe est « éteint » s'il est plus bas et faible. Cependant, certains orbes "dominants" affectent leurs voisins : s'il change d'état, ses voisins changent également d'état. Le joueur 2 peut contrôler les orbes pairs et le joueur 1 peut contrôler les orbes impairs. Cela oblige les deux joueurs à collaborer pour résoudre le puzzle.
Les 8 étapes de ce tutoriel sont regroupées en 3 sections :
- Remplir l'interface utilisateur (étapes 1 et 2)
- Ajouter des mécanismes de jeu (étapes 3 à 5)
- Configuration de la communication (étapes 6 à 8)
Cette partie se terminera par une démo en ligne entièrement fonctionnelle, accessible à tous. Vous utiliserez A-Frame VR et plusieurs extensions A-Frame.
Vous pouvez trouver le code source fini ici.
1. Ajouter des indicateurs visuels
Pour commencer, nous ajouterons des indicateurs visuels de l'ID d'un orbe. Insérez un nouvel élément VR a-text
comme premier enfant de #container-orb0
, sur 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>
Les «dépendances» d'un orbe sont les orbes qu'il basculera lorsqu'il sera basculé: par exemple, disons que l'orbe 1 a comme dépendances les orbes 2 et 3. Cela signifie que si l'orbe 1 est basculé, les orbes 2 et 3 seront également basculés. Nous allons ajouter des indicateurs visuels de dépendances, comme suit, directement aprè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>
Vérifiez que votre code correspond à notre code source pour l'étape 1. Votre orbe doit maintenant correspondre à ce qui suit :
Ceci conclut les indicateurs visuels supplémentaires dont nous aurons besoin. Ensuite, nous ajouterons dynamiquement des orbes à la scène VR, en utilisant ce modèle d'orbe.
2. Ajouter dynamiquement des orbes
Dans cette étape, nous ajouterons des orbes selon une spécification JSON-esque d'un niveau. Cela nous permet de spécifier et de générer facilement de nouveaux niveaux. Nous utiliserons l'orbe de la dernière étape de la partie 1 comme modèle.
Pour commencer, importez jQuery, car cela facilitera les modifications du DOM, et donc les modifications de la scène VR. Directement après l'importation A-Frame, ajoutez ce qui suit à L8 :
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Spécifiez un niveau à l'aide d'un tableau. Le tableau contiendra des littéraux d'objet qui encodent les "dépendances" de chaque orbe. À l'intérieur de la <head>
, ajoutez la configuration de niveau suivante :
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
Pour l'instant, chaque orbe ne peut avoir qu'une seule dépendance à sa "droite" et une à sa "gauche". Immédiatement après avoir déclaré les orbs
ci-dessus, ajoutez un gestionnaire qui s'exécutera au chargement de la page. Ce gestionnaire va (1) dupliquer l'orbe modèle et (2) supprimer l'orbe modèle, en utilisant la configuration de niveau fournie :
$(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) {}
Ensuite, remplissez la fonction de remove
, qui supprime simplement un élément de la scène VR, en fonction d'un sélecteur. Heureusement, A-Frame observe les modifications apportées au DOM et, par conséquent, la suppression de l'élément du DOM suffit pour le supprimer de la scène VR. Remplissez la fonction de remove
comme suit.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Remplissez la fonction clickOrb
, qui déclenche simplement l'action de clic sur un orbe.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
Ensuite, commencez à écrire la fonction populateTemplate
. Dans cette fonction, commencez par obtenir le .container
. Ce conteneur pour l'orbe contient en outre les indicateurs visuels que nous avons ajoutés à l'étape précédente. De plus, nous devrons modifier le comportement onclick
de l'orb, en fonction de ses dépendances. Si une dépendance à gauche existe, modifiez à la fois l'indicateur visuel et le comportement onclick
pour refléter cela ; il en va de même pour une dépendance à droite :
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(); } }
Toujours dans la fonction populateTemplate
, définissez correctement l'ID d'orbe dans tous les éléments de l'orbe et de son conteneur.
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);
Toujours dans la fonction populateTemplate
, définissez le comportement onclick
, définissez la graine aléatoire afin que chaque orbe soit visuellement différent, et enfin, définissez la position de rotation de l'orbe en fonction de son ID.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
À la fin de la fonction, renvoyez le template
avec toutes les configurations ci-dessus.
return template;
Dans le gestionnaire de chargement de document et après avoir supprimé le modèle avec remove('#template')
, activez les orbes qui ont été configurées pour être activées initialement.
$(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); });
Ceci conclut les modifications Javascript. Ensuite, nous allons changer les paramètres par défaut du modèle en celui d'un orbe "off". Modifiez la position et l'échelle de #container-orb0
comme suit :
position="8 0.5 0" scale="0.5 0.5 0.5"
Ensuite, changez l'intensité de #light-orb0
à 0.
intensity="0"
Vérifiez que votre code source correspond à notre code source pour l'étape 2.
Votre scène VR devrait maintenant comporter 5 orbes, remplies dynamiquement. L'un des orbes devrait en outre avoir des indicateurs visuels de dépendances, comme ci-dessous :
Ceci conclut la première section sur l'ajout dynamique d'orbes. Dans la section suivante, nous passerons trois étapes à ajouter des mécanismes de jeu. Plus précisément, le joueur ne pourra basculer que sur des orbes spécifiques en fonction de l'ID du joueur.
3. Ajouter un état terminal
Dans cette étape, nous allons ajouter un état terminal. Si tous les orbes sont activés avec succès, le joueur voit une page "victoire". Pour ce faire, vous devrez suivre l'état de tous les orbes. Chaque fois qu'un orbe est activé ou désactivé, nous devrons mettre à jour notre état interne. Disons qu'une fonction d'assistance toggleOrb
met à jour l'état pour nous. Appelez la fonction toggleOrb
chaque fois qu'un orb change d'état : (1) ajoutez un écouteur de clic au gestionnaire onload et (2) ajoutez un toggleOrb(i);
appel à clickOrb
. Enfin, (3) définissez un toggleOrb
vide.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Pour plus de simplicité, nous utiliserons notre configuration de niveau pour indiquer l'état du jeu. Utilisez toggleOrb
pour basculer l'état on
pour le ième orbe. toggleOrb
peut en outre déclencher un état terminal si tous les orbes sont activés.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Vérifiez que votre code correspond à notre code source pour l'étape 3.
Ceci conclut le mode "solo" du jeu. À ce stade, vous avez un jeu de réalité virtuelle entièrement fonctionnel. Cependant, vous devrez maintenant écrire le composant multijoueur et encourager la collaboration via des mécanismes de jeu.
4. Créer un objet joueur
Dans cette étape, nous allons créer une abstraction pour un joueur avec un ID de joueur. Cet identifiant de joueur sera attribué par le serveur ultérieurement.
Pour l'instant, ce sera simplement une variable globale. Directement après avoir défini orbs
, définissez un ID de joueur :
var orbs = ... var current_player_id = 1;
Vérifiez que votre code correspond à notre code source pour l'étape 4. À l'étape suivante, cet identifiant de joueur sera ensuite utilisé pour déterminer les orbes que le joueur peut contrôler.
5. Basculer conditionnellement les orbes
Dans cette étape, nous allons modifier le comportement de basculement d'orbe. Plus précisément, le joueur 1 peut contrôler les orbes impairs et le joueur 2 peut contrôler les orbes pairs. Tout d'abord, implémentez cette logique aux deux endroits où les orbes changent d'état :
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
Deuxièmement, définissez la fonction allowedToToggle
, juste après clickOrb
. Si le joueur actuel est le joueur 1, les identifiants impairs renverront une valeur vérité-y et ainsi, le joueur 1 sera autorisé à contrôler les orbes impairs. L'inverse est vrai pour le joueur 2. Tous les autres joueurs ne sont pas autorisés à contrôler les orbes.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Vérifiez que votre code correspond à notre code source pour l'étape 5. Par défaut, le joueur est le joueur 1. Cela signifie qu'en tant que joueur 1, vous ne pouvez contrôler que les orbes impairs dans votre aperçu. Ceci conclut la section sur les mécanismes de jeu.
Dans la section suivante, nous faciliterons la communication entre les deux joueurs via un serveur.
6. Configurer le serveur avec WebSocket
Dans cette étape, vous configurerez un serveur simple pour (1) garder une trace des identifiants des joueurs et (2) relayer les messages. Ces messages incluront l'état du jeu, afin que les joueurs puissent être certains que chacun voit ce que l'autre voit.
Nous nous référerons à votre index.html
précédent en tant que code source côté client. Nous ferons référence au code dans cette étape en tant que code source côté serveur. Accédez à glitch.com, cliquez sur "nouveau projet" en haut à droite, et dans le menu déroulant, cliquez sur "hello-express".
Dans le panneau de gauche, sélectionnez "package.json" et ajoutez socket-io
aux dependencies
. Votre dictionnaire dependencies
doit maintenant correspondre aux éléments suivants.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
Dans le panneau de gauche, sélectionnez "index.js" et remplacez le contenu de ce fichier par le minimum socket.io Hello World suivant :
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); });
Ce qui précède configure socket.io sur le port 3000 pour une application express de base. Ensuite, définissez deux variables globales, une pour maintenir la liste des joueurs actifs et une autre pour maintenir le plus petit ID de joueur non attribué.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
Ensuite, définissez la fonction getPlayerId
, qui génère un nouvel ID de joueur et marque le nouvel ID de joueur comme "pris" en l'ajoutant au tableau playerIds
. En particulier, la fonction marque simplement smallestPlayerId
puis met à jour smallestPlayerId
en recherchant le prochain plus petit entier non pris.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
Définissez la fonction removePlayer
, qui met à jour le smallestPlayerId
en conséquence et libère le playerId
fourni afin qu'un autre joueur puisse prendre cet ID.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Enfin, définissez une paire de gestionnaires d'événements de socket qui enregistrent les nouveaux joueurs et désenregistrent les joueurs déconnectés, en utilisant la paire de méthodes ci-dessus.
/** * 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); }); });
Vérifiez à nouveau que votre code correspond à notre code source pour l'étape 6. Ceci conclut l'inscription et la désinscription de base des joueurs. Chaque client peut désormais utiliser l'ID de joueur généré par le serveur.
Dans l'étape suivante, nous modifierons le client pour qu'il reçoive et utilise l'ID de joueur émis par le serveur.
7. Appliquer l'identifiant du joueur
Dans ces deux prochaines étapes, nous allons compléter une version rudimentaire de l'expérience multijoueur. Pour commencer, intégrez l'attribution de l'identifiant du joueur côté client. En particulier, chaque client demandera au serveur un identifiant de joueur. Revenez à l' index.html
côté client sur lequel nous travaillions dans les étapes 4 et précédentes.
Importez socket.io
dans la head
en L7 :
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
Après le gestionnaire de chargement de document, instanciez le socket et émettez un événement newPlayer
. En réponse, le côté serveur générera un nouvel ID de joueur à l'aide de l'événement playerId
. Ci-dessous, utilisez l'URL de votre aperçu de projet Glitch au lieu de lightful.glitch.me
. Vous êtes invités à utiliser l'URL de démonstration ci-dessous, mais toute modification de code que vous apportez ne sera bien sûr pas reflétée.
$(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); });
Vérifiez que votre code correspond à notre code source pour l'étape 7. Maintenant, vous pouvez charger votre jeu sur deux navigateurs ou onglets différents pour jouer les deux côtés d'un jeu multijoueur. Le joueur 1 pourra contrôler les orbes impairs et le joueur 2 pourra contrôler les orbes pairs.
Cependant, notez que basculer les orbes pour le joueur 1 n'affectera pas l'état des orbes pour le joueur 2. Ensuite, nous devons synchroniser les états du jeu.
8. Synchroniser l'état du jeu
Dans cette étape, nous synchroniserons les états du jeu afin que les joueurs 1 et 2 voient les mêmes états d'orbe. Si l'orbe 1 est activé pour le joueur 1, il devrait également l'être pour le joueur 2. Côté client, nous allons à la fois annoncer et écouter les basculements d'orbe. Pour l'annoncer, on passera simplement l'ID de l'orbe qui est basculé.
Avant les deux invocations toggleOrb
, ajoutez l'appel socket.emit
suivant.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
Ensuite, écoutez les basculements d'orbe et basculez l'orbe correspondant. Directement sous l'écouteur d'événement socket playerId
, ajoutez un autre écouteur pour l'événement toggleOrb
.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
Ceci conclut les modifications du code côté client. Vérifiez que votre code correspond à notre code source pour l'étape 8.
Le côté serveur doit maintenant recevoir et diffuser l'ID d'orbe basculé. Dans le index.js
côté serveur, ajoutez l'écouteur suivant. Cet écouteur doit être placé directement sous l'écouteur de disconnect
de la prise.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Vérifiez que votre code correspond à notre code source pour l'étape 8. Maintenant, le joueur 1 chargé dans une fenêtre et le joueur 2 chargé dans une seconde fenêtre verront tous les deux le même état de jeu. Avec cela, vous avez terminé un jeu de réalité virtuelle multijoueur. Les deux joueurs, en outre, doivent collaborer pour atteindre l'objectif. Le produit final correspondra à ce qui suit.
Conclusion
Ceci conclut notre tutoriel sur la création d'un jeu de réalité virtuelle multijoueur. Au cours du processus, vous avez abordé un certain nombre de sujets, notamment la modélisation 3D dans A-Frame VR et les expériences multijoueurs en temps réel à l'aide de WebSockets.
En partant des concepts que nous avons abordés, comment assureriez-vous une expérience plus fluide pour les deux joueurs ? Cela peut inclure la vérification de la synchronisation de l'état du jeu et l'alerte de l'utilisateur dans le cas contraire. Vous pouvez également créer des indicateurs visuels simples pour l'état du terminal et l'état de la connexion du lecteur.
Compte tenu du cadre que nous avons établi et des concepts que nous avons introduits, vous disposez désormais des outils nécessaires pour répondre à ces questions et créer bien plus.
Vous pouvez trouver le code source fini ici.