Comment créer un jeu Endless Runner en réalité virtuelle (Partie 2)
Publié: 2022-03-10Dans 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.)
É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 :
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.
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.
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.
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
Maintenant, vous allez écrire un algorithme qui génère des arbres au hasard :
- Choisissez une voie au hasard (qui n'a pas encore été choisie, pour ce pas de temps) ;
- Faire apparaître un arbre avec une certaine probabilité ;
- 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.
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.