Comment créer un jeu Endless Runner en réalité virtuelle (Partie 2)

Publié: 2022-03-10
Résumé rapide ↬ Si vous vous êtes déjà demandé comment sont construits les jeux avec prise en charge sans clavier pour les casques VR, alors ce tutoriel explique exactement ce que vous recherchez. Voici comment vous aussi pouvez donner vie à un jeu VR basique et fonctionnel.

Dans la partie 1 de cette série, nous avons vu comment créer un modèle de réalité virtuelle avec des effets d'éclairage et d'animation. Dans cette partie, nous allons implémenter la logique de base du jeu et utiliser des manipulations d'environnement A-Frame plus avancées pour créer la partie "jeu" de cette application. À la fin, vous aurez un jeu de réalité virtuelle fonctionnel avec un vrai défi.

Ce didacticiel implique un certain nombre d'étapes, y compris (mais sans s'y limiter) la détection de collision et d'autres concepts A-Frame tels que les mixins.

  • Démo du produit final

Conditions préalables

Tout comme dans le tutoriel précédent, vous aurez besoin des éléments suivants :

  • Accès Internet (spécifiquement à glitch.com);
  • Un projet Glitch terminé à partir de la partie 1. (Vous pouvez continuer à partir du produit fini en accédant à https://glitch.com/edit/#!/ergo-1 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.)
Plus après saut! Continuez à lire ci-dessous ↓

Étape 1 : Concevoir les obstacles

Dans cette étape, vous concevez les arbres que nous utiliserons comme obstacles. Ensuite, vous ajouterez une animation simple qui déplace les arbres vers le joueur, comme suit :

Arbres de modèles se déplaçant vers le joueur
Arbres de modèles se déplaçant vers le joueur ( Grand aperçu )

Ces arbres serviront de modèles pour les obstacles que vous générerez pendant le jeu. Pour la dernière partie de cette étape, nous supprimerons ensuite ces "arbres modèles".

Pour commencer, ajoutez un certain nombre de mixins A-Frame différents. Les mixins sont des ensembles de propriétés de composants couramment utilisés. Dans notre cas, tous nos arbres auront la même couleur, hauteur, largeur, profondeur, etc. En d'autres termes, tous vos arbres se ressembleront et utiliseront donc quelques mixins partagés.

Note : Dans notre tutoriel, vos seuls assets seront des mixins. Visitez la page A-Frame Mixins pour en savoir plus.

Dans votre éditeur, accédez à index.html . Juste après votre ciel et avant vos lumières, ajoutez une nouvelle entité A-Frame pour contenir vos ressources :

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

Dans votre nouvelle entité a-assets , commencez par ajouter un mixin pour votre feuillage. Ce mixins définit des propriétés communes pour le feuillage de l'arborescence du modèle. Bref, c'est une pyramide blanche à ombrage plat, pour un effet 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>

Juste en dessous de votre mixin de feuillage, ajoutez un mixin pour le tronc. Ce tronc sera un petit prisme rectangulaire blanc.

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

Ensuite, ajoutez les objets de l'arborescence de modèles qui utiliseront ces mixins. Toujours dans index.html , faites défiler jusqu'à la section plates-formes. Juste avant la section joueur, ajoutez une nouvelle section arborescence, avec trois entités arborescentes vides :

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

Ensuite, repositionnez, redimensionnez et ajoutez des ombres aux entités de l'arborescence.

 <!-- 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>

Maintenant, remplissez les entités arborescentes avec un tronc et un feuillage, en utilisant les mixins que nous avons définis précédemment.

 <!-- 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>

Accédez à votre aperçu et vous devriez maintenant voir les arborescences de modèles suivantes.

Modèles d'arbres pour les obstacles
Modèles d'arbres pour les obstacles ( Grand aperçu )

Maintenant, animez les arbres depuis un emplacement distant sur la plate-forme vers l'utilisateur. Comme précédemment, utilisez la balise 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>

Assurez-vous que votre code correspond à ce qui suit.

 <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 --> ...

Accédez à votre aperçu et vous verrez maintenant les arbres se déplacer vers vous.

Arbres de modèles se déplaçant vers le joueur
Arbres de modèles se déplaçant vers le joueurArbres de modèles se déplaçant vers le joueur ( Grand aperçu )

Revenez à votre éditeur. Cette fois, sélectionnez assets/ergo.js . Dans la section du jeu, configurez les arborescences après le chargement de la fenêtre.

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

Sous les commandes mais avant la section Jeu, ajoutez une nouvelle section TREES . Dans cette section, définissez une nouvelle fonction setupTrees .

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

Dans la nouvelle fonction setupTrees , obtenez des références aux objets DOM de l'arborescence de modèles et rendez les références disponibles globalement.

 /********* * 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'); }

Ensuite, définissez un nouvel utilitaire removeTree . Avec cet utilitaire, vous pouvez ensuite supprimer les arborescences de modèles de la scène. Sous la fonction setupTrees , définissez votre nouvel utilitaire.

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

De retour dans setupTrees , utilisez le nouvel utilitaire pour supprimer les arborescences de modèles.

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

Assurez-vous que les sections de votre arbre et de votre jeu correspondent aux éléments suivants :

 /********* * 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(); }

Rouvrez votre aperçu et vos arbres devraient maintenant être absents. L'aperçu doit correspondre à notre jeu au début de ce didacticiel.

Produit fini de la partie 1
Produit fini de la partie 1 ( Grand aperçu )

Ceci conclut la conception de l'arborescence de modèles.

Dans cette étape, nous avons couvert et utilisé les mixins A-Frame, qui nous permettent de simplifier le code en définissant des propriétés communes. De plus, nous avons tiré parti de l'intégration A-Frame avec le DOM pour supprimer des objets de la scène A-Frame VR.

Dans la prochaine étape, nous créerons plusieurs obstacles et concevrons un algorithme simple pour répartir les arbres sur différentes voies.

Étape 2 : Obstacles de frai

Dans un jeu de course sans fin, notre objectif est d'éviter les obstacles volant vers nous. Dans cette implémentation particulière du jeu, nous utilisons trois voies comme c'est le plus courant.

Contrairement à la plupart des jeux de coureurs sans fin, ce jeu ne prend en charge que les mouvements à gauche et à droite . Cela impose une contrainte à notre algorithme de création d'obstacles : nous ne pouvons pas avoir trois obstacles dans les trois voies, en même temps, volant vers nous. Si cela se produisait, le joueur n'aurait aucune chance de survie. Par conséquent, notre algorithme de génération doit tenir compte de cette contrainte.

Dans cette étape, toutes nos modifications de code seront effectuées dans assets/ergo.js . Le fichier HTML restera le même. Accédez à la section TREES de assets/ergo.js .

Pour commencer, nous allons ajouter des utilitaires pour générer des arbres. Chaque arbre aura besoin d'un identifiant unique, que nous définirons naïvement comme étant le nombre d'arbres qui existent lorsque l'arbre est généré. Commencez par suivre le nombre d'arbres dans une variable globale.

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

Ensuite, nous allons initialiser une référence à l'élément DOM du conteneur d'arbres, auquel notre fonction spawn ajoutera des arbres. Toujours dans la section TREES , ajoutez une variable globale puis faites la référence.

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

En utilisant à la fois le nombre d'arbres et le conteneur d'arbres, écrivez une nouvelle fonction qui génère des arbres.

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

Pour faciliter l'utilisation plus tard, vous créerez une deuxième fonction qui ajoute le bon arbre à la bonne voie. Pour commencer, définissez un nouveau tableau de templates dans la section TREES .

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

À l'aide de ce tableau de modèles, ajoutez un utilitaire qui génère des arbres dans une voie spécifique, en fonction d'un ID représentant la gauche, le milieu ou la droite.

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

Accédez à votre aperçu et ouvrez votre console développeur. Dans votre console développeur, appelez la fonction globale addTreeTo .

 > addTreeTo(0); # spawns tree in left lane 
Invoquer manuellement addTreeTo
Appelez addTreeTo manuellement ( Grand aperçu )

Maintenant, vous allez écrire un algorithme qui génère des arbres au hasard :

  1. Choisissez une voie au hasard (qui n'a pas encore été choisie, pour ce pas de temps) ;
  2. Faire apparaître un arbre avec une certaine probabilité ;
  3. Si le nombre maximum d'arbres a été engendré pour ce pas de temps, arrêtez. Sinon, répétez l'étape 1.

Pour effectuer cet algorithme, nous allons plutôt mélanger la liste des modèles et traiter un à la fois. Commencez par définir une nouvelle fonction, addTreesRandomly qui accepte un certain nombre d'arguments de mots clés différents.

 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 } = {}) { }

Dans votre nouvelle fonction addTreesRandomly , définissez une liste d'arborescences de modèles et mélangez la liste.

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

Faites défiler vers le bas du fichier et créez une nouvelle section utilitaires, ainsi qu'un nouvel utilitaire de shuffle . Cet utilitaire mélangera un tableau en place.

 /******** * 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; }

Revenez à la fonction addTreesRandomly dans votre section Trees. Ajoutez une nouvelle variable numberOfTreesAdded et parcourez la liste des arbres définis ci-dessus.

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

Dans l'itération sur les arbres, ne générez un arbre qu'avec une certaine probabilité et uniquement si le nombre d'arbres ajoutés ne dépasse pas 2 . Mettez à jour la boucle for comme suit.

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

Pour terminer la fonction, retournez le nombre d'arbres ajoutés.

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

Vérifiez que votre fonction addTreesRandomly correspond à ce qui suit.

 /** * 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; }

Enfin, pour générer automatiquement des arbres, configurez une minuterie qui déclenche la création d'arbres à intervalles réguliers. Définissez la minuterie globalement et ajoutez une nouvelle fonction de démontage pour cette minuterie.

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

Ensuite, définissez une nouvelle fonction qui initialise le temporisateur et enregistre le temporisateur dans la variable globale précédemment définie. La minuterie ci-dessous est exécutée toutes les demi-secondes.

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

Enfin, démarrez le chronomètre après le chargement de la fenêtre, depuis la section Jeu.

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

Accédez à votre aperçu et vous verrez des arbres apparaître au hasard. Notez qu'il n'y a jamais trois arbres à la fois.

Arbre apparaissant au hasard
Arbre apparaissant au hasard ( Grand aperçu )

Ceci conclut l'étape des obstacles. Nous avons pris avec succès un certain nombre d'arborescences de modèles et généré un nombre infini d'obstacles à partir des modèles. Notre algorithme de spawn respecte également les contraintes naturelles du jeu pour le rendre jouable.

À l'étape suivante, ajoutons les tests de collision.

Étape 3 : Test de collision

Dans cette section, nous allons implémenter les tests de collision entre les obstacles et le joueur. Ces tests de collision sont plus simples que les tests de collision dans la plupart des autres jeux ; cependant, le joueur ne se déplace que le long de l'axe des x, donc chaque fois qu'un arbre croise l'axe des x, vérifiez si la voie de l'arbre est la même que la voie du joueur. Nous allons implémenter cette vérification simple pour ce jeu.

Accédez à index.html , jusqu'à la section TREES . Ici, nous allons ajouter des informations de voie à chacun des arbres. Pour chacun des arbres, ajoutez data-tree-position-index= , comme suit. Ajoutez également class="tree" , afin que nous puissions facilement sélectionner tous les arbres sur la ligne :

 <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>

Accédez à assets/ergo.js et appelez une nouvelle fonction setupCollisions dans la section GAME . De plus, définissez une nouvelle variable globale isGameRunning qui indique si un jeu existant est déjà en cours d'exécution ou non.

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

Définissez une nouvelle section COLLISIONS juste après la section TREES mais avant la section Game. Dans cette section, définissez la fonction 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 * ********/

Comme précédemment, nous allons enregistrer un composant AFRAME et utiliser l'écouteur d'événement tick pour exécuter du code à chaque pas de temps. Dans ce cas, nous allons enregistrer un composant auprès du player et effectuer des vérifications sur tous les arbres de cet écouteur :

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

Dans la boucle for , commencez par obtenir les informations pertinentes de l'arborescence :

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

Ensuite, toujours dans la boucle for , supprimez l'arbre s'il est hors de vue, juste après avoir extrait les propriétés de l'arbre :

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

Ensuite, s'il n'y a pas de jeu en cours, ne vérifiez pas s'il y a une collision.

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

Enfin (toujours dans la boucle for ), vérifiez si l'arbre partage la même position au même moment avec le joueur. Si c'est le cas, appelez une fonction gameOver encore à définir :

 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(); } }

Vérifiez que votre fonction setupCollisions correspond aux éléments suivants :

 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(); } }) } }) }

Ceci conclut la configuration de la collision. Maintenant, nous allons ajouter quelques subtilités pour faire abstraction des séquences startGame et gameOver . Accédez à la section GAME . Mettez à jour le bloc window.onload pour qu'il corresponde à ce qui suit, en remplaçant addTreesRandomlyLoop par une fonction startGame qui reste à définir.

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

Sous les invocations de la fonction setup, créez une nouvelle fonction startGame . Cette fonction initialisera la variable isGameRunning en conséquence et empêchera les appels redondants.

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

Enfin, définissez gameOver , qui alertera un "Game Over!" message pour l'instant.

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

Ceci conclut la section de test de collision du jeu de coureur sans fin.

Dans cette étape, nous avons de nouveau utilisé des composants A-Frame et un certain nombre d'autres utilitaires que nous avons ajoutés précédemment. Nous avons en outre réorganisé et correctement résumé les fonctions du jeu ; nous augmenterons ensuite ces fonctions de jeu pour obtenir une expérience de jeu plus complète.

Conclusion

Dans la partie 1, nous avons ajouté des commandes adaptées aux casques VR : regardez à gauche pour vous déplacer vers la gauche et à droite pour vous déplacer vers la droite. Dans cette deuxième partie de la série, je vous ai montré à quel point il peut être facile de créer un jeu de réalité virtuelle basique et fonctionnel. Nous avons ajouté une logique de jeu, afin que le coureur sans fin corresponde à vos attentes : courez indéfiniment et faites voler une série infinie d'obstacles dangereux vers le joueur. Jusqu'à présent, vous avez créé un jeu fonctionnel avec une prise en charge sans clavier des casques de réalité virtuelle.

Voici des ressources supplémentaires pour différents contrôles et casques VR :

  • Cadre en A pour casques VR
    Une enquête sur les navigateurs et les casques pris en charge par A-Frame VR.
  • A-Frame pour contrôleurs VR
    Comment A-Frame ne prend en charge aucun contrôleur, contrôleurs 3DoF et contrôleurs 6DoF, en plus d'autres alternatives d'interaction.

Dans la partie suivante, nous ajouterons quelques touches finales et synchroniserons les états du jeu , ce qui nous rapprochera un peu plus des jeux multijoueurs.