Comment créer un jeu Endless Runner en réalité virtuelle (Partie 3)
Publié: 2022-03-10Et ainsi notre voyage continue. Dans cette dernière partie de ma série sur la création d'un jeu de course en réalité virtuelle sans fin, je vais vous montrer comment vous pouvez synchroniser l'état du jeu entre deux appareils, ce qui vous rapprochera un peu plus de la création d'un jeu multijoueur. Je présenterai spécifiquement MirrorVR qui est responsable de la gestion du serveur de médiation dans la communication client à client.
Remarque : Ce jeu peut être joué avec ou sans casque VR. Vous pouvez voir une démo du produit final sur ergo-3.glitch.me.
Pour commencer, vous aurez besoin des éléments suivants.
- Accès Internet (spécifiquement à glitch.com);
- Un projet Glitch réalisé à partir de la partie 2 de ce tutoriel. Vous pouvez commencer à partir du produit fini de la partie 2 en accédant à https://glitch.com/edit/#!/ergo-2 et en cliquant sur "Remixer pour éditer" ;
- Un casque de réalité virtuelle (facultatif, recommandé). (J'utilise Google Cardboard, qui est proposé à 15 $ pièce.)
Étape 1 : Afficher le score
Le jeu tel quel fonctionne au strict minimum, où le joueur se voit confier un défi : éviter les obstacles. Cependant, en dehors des collisions d'objets, le jeu ne fournit pas de retour d'information au joueur concernant la progression dans le jeu. Pour y remédier, vous allez implémenter l'affichage du score dans cette étape. La partition sera un gros objet texte placé dans notre monde de réalité virtuelle, par opposition à une interface collée au champ de vision de l'utilisateur.
Dans la réalité virtuelle en général, l'interface utilisateur est mieux intégrée dans le monde plutôt que collée à la tête de l'utilisateur.
Commencez par ajouter l'objet à index.html . Ajoutez un mixin de text
, qui sera réutilisé pour d'autres éléments de texte :
<a-assets> ... <a-mixin text=" font:exo2bold; anchor:center; align:center;"></a-mixin> ... </a-assets>
Ensuite, ajoutez un élément de text
à la plate-forme, juste avant le lecteur :
<!-- Score --> <a-text value="" mixin="text" height="40" width="40" position="0 1.2 -3" opacity="0.75"></a-text> <!-- Player --> ...
Cela ajoute une entité textuelle à la scène de réalité virtuelle. Le texte n'est pas visible actuellement, car sa valeur est définie sur vide. Cependant, vous allez maintenant remplir dynamiquement l'entité de texte à l'aide de JavaScript. Accédez à assets/ergo.js . Après la section collisions
, ajoutez une section score
et définissez un certain nombre de variables globales :
-
score
: le score du jeu en cours. -
countedTrees
: ID de tous les arbres inclus dans le score. (En effet, les tests de collision peuvent se déclencher plusieurs fois pour le même arbre.) -
scoreDisplay
: référence à l'objet DOM, correspondant à un objet texte dans le monde de la réalité virtuelle.
/********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;
Ensuite, définissez une fonction de configuration pour initialiser nos variables globales. Dans le même ordre d'idées, définissez une fonction de teardown
.
... var scoreDisplay; function setupScore() { score = 0; countedTrees = new Set(); scoreDisplay = document.getElementById('score'); } function teardownScore() { scoreDisplay.setAttribute('value', ''); }
Dans la section Game
, mettez à jour gameOver
, startGame
et window.onload
pour inclure la configuration et le démontage du score.
/******** * GAME * ********/ function gameOver() { ... teardownScore(); } function startGame() { ... setupScore(); addTreesRandomlyLoop(); } window.onload = function() { setupScore(); ... }
Définissez une fonction qui incrémente le score d'un arbre particulier. Cette fonction vérifiera par rapport à countedTrees
pour s'assurer que l'arbre n'est pas compté deux fois.
function addScoreForTree(tree_id) { if (countedTrees.has(tree_id)) return; score += 1; countedTrees.add(tree_id); }
De plus, ajoutez un utilitaire pour mettre à jour l'affichage du score à l'aide de la variable globale.
function updateScoreDisplay() { scoreDisplay.setAttribute('value', score); }
Mettez à jour les tests de collision en conséquence afin d'invoquer cette fonction d'incrémentation du score chaque fois qu'un obstacle a dépassé le joueur. Toujours dans assets/ergo.js
, accédez à la section collisions
. Ajoutez la vérification et la mise à jour suivantes.
AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_LINE_END) { addScoreForTree(tree_id); updateScoreDisplay(); } }) } })
Enfin, mettez à jour l'affichage du score dès le début de la partie. Accédez à la section Game
et ajoutez updateScoreDisplay();
pour startGame
le jeu :
function startGame() { ... setupScore(); updateScoreDisplay(); ... }
Assurez-vous que assets/ergo.js et index.html correspondent aux fichiers de code source correspondants. Ensuite, accédez à votre aperçu. Vous devriez voir ce qui suit :
Ceci conclut l'affichage du score. Ensuite, nous ajouterons des menus de démarrage et de fin de partie appropriés, afin que le joueur puisse rejouer le jeu comme il le souhaite.
Étape 2 : Ajouter un menu Démarrer
Maintenant que l'utilisateur peut suivre la progression, vous allez ajouter des touches finales pour compléter l'expérience de jeu. Dans cette étape, vous ajouterez un menu Démarrer et un menu Game Over , permettant à l'utilisateur de démarrer et de redémarrer des jeux.
Commençons par le menu Démarrer où le joueur clique sur un bouton "Démarrer" pour commencer le jeu. Pour la seconde moitié de cette étape, vous allez ajouter un menu Game Over , avec un bouton « Redémarrer » :
Accédez à index.html dans votre éditeur. Ensuite, trouvez la section Mixins
. Ici, ajoutez le title
mixin, qui définit les styles pour un texte particulièrement volumineux. Nous utilisons la même police qu'auparavant, alignons le texte au centre et définissons une taille appropriée au type de texte. (Notez ci-dessous que l' anchor
est l'endroit où un objet texte est ancré à sa position.)
<a-assets> ... <a-mixin text=" font:exo2bold; height:40; width:40; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Ensuite, ajoutez un deuxième mixin pour les titres secondaires. Ce texte est légèrement plus petit mais est par ailleurs identique au titre.
<a-assets> ... <a-mixin text=" font:exo2bold; height:10; width:10; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Pour le troisième et dernier mixin, définissez les propriétés du texte descriptif - encore plus petites que les en-têtes secondaires.
<a-assets> ... <a-mixin text=" font:exo2bold; height:5; width:5; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Avec tous les styles de texte définis, vous allez maintenant définir les objets de texte dans le monde. Ajoutez une nouvelle section Menus
sous la section Score
, avec un conteneur vide pour le menu Démarrer :
<!-- Score --> ... <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> </a-entity> </a-entity>
Dans le conteneur du menu Démarrer, définissez le titre et un conteneur pour tout le texte autre que le titre :
... <a-entity ...> <a-entity position="0 1 0"> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>
À l'intérieur du conteneur pour le texte autre que le titre, ajoutez des instructions pour jouer au jeu :
<a-entity...> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> </a-entity>
Pour compléter le menu Démarrer , ajoutez un bouton indiquant « Démarrer » :
<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>
Vérifiez que le code HTML de votre menu Démarrer correspond aux éléments suivants :
<!-- 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>
Accédez à votre aperçu et vous verrez le menu Démarrer suivant :
Toujours dans la section Menus
(directement sous le menu start
), ajoutez le menu game-over
en utilisant les mêmes mixins :
<!-- 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>
Accédez à votre fichier JavaScript, assets/ergo.js . Créez une nouvelle section Menus
avant la section Game
. De plus, définissez trois fonctions vides : setupAllMenus
, hideAllMenus
et showGameOverMenu
.
/******** * MENU * ********/ function setupAllMenus() { } function hideAllMenus() { } function showGameOverMenu() { } /******** * GAME * ********/
Ensuite, mettez à jour la section Game
à trois endroits. Dans gameOver
, affichez le menu Game Over :
function gameOver() { ... showGameOverMenu(); } ``` In `startGame`, hide all menus: ``` function startGame() { ... hideAllMenus(); }
Ensuite, dans window.onload
, supprimez l'appel direct à startGame
et appelez à la place setupAllMenus
. Mettez à jour votre écouteur pour qu'il corresponde aux éléments suivants :
window.onload = function() { setupAllMenus(); setupScore(); setupTrees(); }
Revenez à la section Menu
. Enregistrez les références à divers objets 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'); }
Ensuite, liez les boutons "Démarrer" et "Redémarrer" à startGame
:
function setupAllMenus() { ... startButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame); }
Définissez showStartMenu
et appelez-le depuis setupAllMenus
:
function setupAllMenus() { ... showStartMenu(); } function hideAllMenus() { } function showGameOverMenu() { } function showStartMenu() { }
Pour remplir les trois fonctions vides, vous aurez besoin de quelques fonctions d'assistance. Définissez les deux fonctions suivantes, qui acceptent un élément DOM représentant une entité A-Frame VR et l'affichent ou la masquent. Définissez les deux fonctions ci-dessus showAllMenus
:
... var restartButton; function hideEntity(el) { el.setAttribute('visible', false); } function showEntity(el) { el.setAttribute('visible', true); } function showAllMenus() { ...
Remplissez d'abord hideAllMenus
. Vous supprimerez les objets de la vue, puis supprimerez les écouteurs de clic pour les deux menus :
function hideAllMenus() { hideEntity(menuContainer); startButton.classList.remove('clickable'); restartButton.classList.remove('clickable'); }
Deuxièmement, showGameOverMenu
. Ici, restaurez le conteneur pour les deux menus, ainsi que le menu Game Over et l'écouteur de clic du bouton "Redémarrer". Cependant, supprimez l'écouteur de clic du bouton "Démarrer" et masquez le menu "Démarrer".
function showGameOverMenu() { showEntity(menuContainer); hideEntity(menuStart); showEntity(menuGameOver); startButton.classList.remove('clickable'); restartButton.classList.add('clickable'); }
Troisièmement, showStartMenu
. Ici, annulez toutes les modifications effectuées showGameOverMenu
.
function showStartMenu() { showEntity(menuContainer); hideEntity(menuGameOver); showEntity(menuStart); startButton.classList.add('clickable'); restartButton.classList.remove('clickable'); }
Vérifiez que votre code correspond aux fichiers source correspondants. Ensuite, accédez à votre aperçu et vous observerez le comportement suivant :
Ceci conclut les menus Start et Game Over .
Toutes nos félicitations! Vous avez maintenant un jeu entièrement fonctionnel avec un bon début et une bonne fin. Cependant, il nous reste une étape supplémentaire dans ce didacticiel : nous devons synchroniser l'état du jeu entre les différents appareils du lecteur. Cela nous rapprochera un peu plus des jeux multijoueurs.
Étape 3 : Synchronisation de l'état du jeu avec MirrorVR
Dans un didacticiel précédent, vous avez appris à envoyer des informations en temps réel sur les sockets, afin de faciliter la communication unidirectionnelle entre un serveur et un client. Dans cette étape, vous vous baserez sur un produit à part entière de ce didacticiel, MirrorVR, qui gère le serveur de médiation dans la communication client à client.
Remarque : Vous pouvez en savoir plus sur MirrorVR ici.
Accédez à index.html . Ici, nous allons charger MirrorVR et ajouter un composant à la caméra, indiquant qu'il doit refléter la vue d'un appareil mobile, le cas échéant. Importez la dépendance socket.io et 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>
Ensuite, ajoutez un composant, camera-listener
, à la caméra :
<a-camera camera-listener ...>
Accédez à assets/ergo.js . Dans cette étape, l'appareil mobile enverra des commandes et l'appareil de bureau reflétera uniquement l'appareil mobile.
Pour faciliter cela, vous avez besoin d'un utilitaire pour faire la distinction entre les appareils de bureau et les appareils mobiles. A la fin de votre fichier, ajoutez une fonction mobileCheck
après 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; };
Tout d'abord, nous allons synchroniser le début du jeu. Dans startGame
, de la section Game , ajoutez une notification mirrorVR
à la fin.
function startGame() { ... if (mobileCheck()) { mirrorVR.notify('startGame', {}) } }
Le client mobile envoie maintenant des notifications sur le démarrage d'un jeu. Vous allez maintenant implémenter la réponse du bureau.
Dans l'écouteur de chargement de la fenêtre, appelez une fonction setupMirrorVR
:
window.onload = function() { ... setupMirrorVR(); }
Définissez une nouvelle section au-dessus de la section Game
pour la configuration de MirrorVR :
/************ * MirrorVR * ************/ function setupMirrorVR() { mirrorVR.init(); }
Ensuite, ajoutez des arguments de mot-clé à la fonction d'initialisation pour mirrorVR. Plus précisément, nous définirons le gestionnaire des notifications de démarrage de jeu. Nous spécifierons en outre un identifiant de chambre ; cela garantit que toute personne chargeant votre application est immédiatement synchronisée.
function setupMirrorVR() { mirrorVR.init({ roomId: 'ergo', state: { startGame: { onNotify: function(data) { hideAllMenus(); setupScore(); updateScoreDisplay(); } }, } }); }
Répétez le même processus de synchronisation pour Game Over . Dans gameOver
dans la section Game
, ajoutez une vérification pour les appareils mobiles et envoyez une notification en conséquence :
function gameOver() { ... if (mobileCheck()) { mirrorVR.notify('gameOver', {}); } }
Accédez à la section MirrorVR
et mettez à jour les arguments du mot-clé avec un écouteur gameOver
:
function setupMirrorVR() { mirrorVR.init({ state: { startGame: {... }, gameOver: { onNotify: function(data) { gameOver(); } }, } }) }
Ensuite, répétez le même processus de synchronisation pour l'ajout d'arbres. Accédez à addTreesRandomly
dans la section Trees
. Gardez une trace des voies qui reçoivent de nouveaux arbres. Ensuite, juste avant la directive de return
, et envoyez une notification en conséquence :
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 ... }
Accédez à la section MirrorVR
et mettez à jour les arguments du mot-clé vers mirrorVR.init
avec un nouvel écouteur pour les arbres :
function setupMirrorVR() { mirrorVR.init({ state: { ... gameOver: {... }, addTrees: { onNotify: function(position_indices) { position_indices.forEach(addTreeTo) } }, } }) }
Enfin, nous synchronisons le score du jeu. Dans updateScoreDisplay
de la section Score
, envoyez une notification le cas échéant :
function updateScoreDisplay() { ... if (mobileCheck()) { mirrorVR.notify('score', score); } }
Mettez à jour l'initialisation de mirrorVR
pour la dernière fois, avec un écouteur pour les changements de score :
function setupMirrorVR() { mirrorVR.init({ state: { addTrees: { }, score: { onNotify: function(data) { score = data; updateScoreDisplay(); } } } }); }
Vérifiez que votre code correspond aux fichiers de code source appropriés pour cette étape. Ensuite, accédez à l'aperçu de votre bureau. De plus, ouvrez la même URL sur votre appareil mobile. Dès que votre appareil mobile charge la page Web, votre bureau doit immédiatement commencer à refléter le jeu de l'appareil mobile.
Voici une démo. Notez que le curseur du bureau ne bouge pas, ce qui indique que l'appareil mobile contrôle l'aperçu du bureau.
Ceci conclut votre projet augmenté avec mirrorVR.
Cette troisième étape a introduit quelques étapes de base de synchronisation de l'état du jeu ; pour rendre cela plus robuste, vous pouvez ajouter plus de vérifications d'intégrité et plus de points de synchronisation.
Conclusion
Dans ce didacticiel, vous avez apporté la touche finale à votre jeu de coureur sans fin et implémenté la synchronisation en temps réel d'un client de bureau avec un client mobile, reflétant efficacement l'écran de l'appareil mobile sur votre bureau. Ceci conclut la série sur la construction d'un jeu de course sans fin en réalité virtuelle. En plus des techniques A-Frame VR, vous avez appris la modélisation 3D, la communication client à client et d'autres concepts largement applicables.
Les prochaines étapes peuvent inclure :
- Modélisation plus avancée
Cela signifie des modèles 3D plus réalistes, potentiellement créés dans un logiciel tiers et importés. Par exemple, (MagicaVoxel) simplifie la création d'art voxel et (Blender) est une solution de modélisation 3D complète. - Plus de complexité
Des jeux plus complexes, comme un jeu de stratégie en temps réel, pourraient tirer parti d'un moteur tiers pour une efficacité accrue. Cela peut signifier éviter complètement A-Frame et webVR, au lieu de publier un jeu compilé (Unity3d).
D'autres avenues incluent le support multijoueur et des graphismes plus riches. Avec la conclusion de cette série de didacticiels, vous disposez maintenant d'un cadre à explorer davantage.