Dans le développement Web moderne, les frontières entre les applications classiques et les applications Web s'estompent chaque jour. Aujourd'hui, nous pouvons créer non seulement des sites Web interactifs, mais également des jeux à part entière directement dans le navigateur. L'un des outils qui rendent cela possible est la bibliothèque React Three Fiber - un outil puissant pour créer des graphiques 3D basés sur Three.js à l'aide de la technologie React .
React Three Fiber est un wrapper sur Three.js qui utilise la structure et les principes de React pour créer des graphiques 3D sur le Web. Cette pile permet aux développeurs de combiner la puissance de Three.js avec la commodité et la flexibilité de React , rendant le processus de création d'une application plus intuitif et organisé.
Au cœur de React Three Fiber se trouve l'idée selon laquelle tout ce que vous créez dans une scène est un composant React . Cela permet aux développeurs d'appliquer des modèles et des méthodologies familiers.
L'un des principaux avantages de React Three Fiber est sa facilité d'intégration avec l'écosystème React . Tous les autres outils React peuvent toujours être facilement intégrés lors de l'utilisation de cette bibliothèque.
Web-GameDev a connu des évolutions majeures ces dernières années, passant de simples jeux Flash 2D à des projets 3D complexes comparables à des applications bureautiques. Cette croissance en popularité et en capacités fait du Web-GameDev un domaine incontournable.
L’un des principaux avantages du jeu en ligne est son accessibilité. Les joueurs n'ont pas besoin de télécharger et d'installer de logiciel supplémentaire : il suffit de cliquer sur le lien dans leur navigateur. Cela simplifie la distribution et la promotion des jeux, les rendant accessibles à un large public dans le monde entier.
Enfin, le développement de jeux Web peut être un excellent moyen pour les développeurs de s’essayer au gamedev en utilisant des technologies familières. Grâce aux outils et bibliothèques disponibles, même sans expérience en graphisme 3D, il est possible de créer des projets intéressants et de qualité !
Les navigateurs modernes ont parcouru un long chemin, passant d'outils de navigation Web assez simples à des plates-formes puissantes permettant d'exécuter des applications et des jeux complexes. Les principaux navigateurs tels que Chrome , Firefox , Edge et autres sont constamment optimisés et développés pour garantir des performances élevées, ce qui en fait une plate-forme idéale pour développer des applications complexes.
L'un des outils clés qui ont alimenté le développement des jeux sur navigateur est WebGL . Cette norme permettait aux développeurs d'utiliser l'accélération graphique matérielle, ce qui améliorait considérablement les performances des jeux 3D. Avec d'autres webAPI, WebGL ouvre de nouvelles possibilités pour créer des applications Web impressionnantes directement dans le navigateur.
Néanmoins, lors du développement de jeux pour navigateur, il est crucial de considérer différents aspects de performances : l'optimisation des ressources, la gestion de la mémoire et l'adaptation aux différents appareils sont autant de points clés qui peuvent affecter la réussite d'un projet.
Cependant, les mots et la théorie sont une chose, mais l’expérience pratique en est une autre. Pour vraiment comprendre et apprécier tout le potentiel du développement de jeux Web, le meilleur moyen est de se plonger dans le processus de développement. Par conséquent, comme exemple de développement réussi d’un jeu Web, nous créerons notre propre jeu. Ce processus nous permettra d'apprendre les aspects clés du développement, de faire face à des problèmes réels et d'y trouver des solutions, et de voir à quel point une plate-forme de développement de jeux Web peut être puissante et flexible.
Dans une série d'articles, nous verrons comment créer un jeu de tir à la première personne en utilisant les fonctionnalités de cette bibliothèque, et plongerons dans le monde passionnant du développement de jeux Web !
Dépôt sur GitHub
Maintenant, commençons !
Tout d'abord, nous aurons besoin d'un modèle de projet React . Commençons donc par l'installer.
npm create vite@latest
Installez des packages npm supplémentaires.
npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js
Supprimez ensuite tout ce qui est inutile de notre projet.
Dans le fichier main.jsx , ajoutez un élément div qui sera affiché sur la page en tant que portée. Insérez un composant Canvas et définissez le champ de vision de la caméra. À l’intérieur du composant Canvas , placez le composant App .
Ajoutons des styles à index.css pour étendre les éléments de l'interface utilisateur sur toute la hauteur de l'écran et afficher la portée sous la forme d'un cercle au centre de l'écran.
Dans le composant App , nous ajoutons un composant Sky , qui sera affiché en arrière-plan dans notre scène de jeu sous la forme d'un ciel.
Créons un composant Ground et plaçons-le dans le composant App .
Dans Ground , créez un élément de surface plane. Sur l'axe Y, déplacez-le vers le bas pour que ce plan soit dans le champ de vision de la caméra. Et retournez également le plan sur l’axe X pour le rendre horizontal.
Même si nous avons spécifié le gris comme couleur du matériau, l'avion apparaît complètement noir.
Par défaut, il n'y a pas d'éclairage dans la scène, ajoutons donc une source de lumière ambientLight , qui éclaire l'objet de tous les côtés et n'a pas de faisceau dirigé. En tant que paramètre, définissez l'intensité de la lueur.
Pour que la surface du sol ne paraisse pas homogène, nous ajouterons de la texture. Créez un motif de la surface du sol sous la forme de cellules qui se répètent tout au long de la surface.
Dans le dossier des ressources , ajoutez une image PNG avec une texture.
Pour charger une texture sur la scène, utilisons le hook useTexture du package @react-trois/drei . Et comme paramètre pour le hook nous passerons l'image de texture importée dans le fichier. Définissez la répétition de l'image dans les axes horizontaux.
À l'aide du composant PointerLockControls du package @react-trois/drei , fixez le curseur sur l'écran afin qu'il ne bouge pas lorsque vous déplacez la souris, mais change la position de la caméra sur la scène.
Faisons une petite modification pour le composant Ground .
Pour plus de clarté, ajoutons un simple cube à la scène.
<mesh position={[0, 3, -5]}> <boxGeometry /> </mesh>
Pour l'instant, il est simplement suspendu dans l'espace.
Utilisez le composant Physique du package @react-trois/rapier pour ajouter de la « physique » à la scène. En tant que paramètre, configurez le champ de gravité, où nous définissons les forces gravitationnelles le long des axes.
<Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>
Cependant, notre cube est à l’intérieur du composant physique, mais rien ne lui arrive. Pour que le cube se comporte comme un véritable objet physique, nous devons l'envelopper dans le composant RigidBody du package @react-trois/rapier .
Après cela, nous verrons immédiatement qu'à chaque rechargement de la page, le cube tombe sous l'influence de la gravité.
Mais maintenant, il y a une autre tâche : il est nécessaire de faire du sol un objet avec lequel le cube peut interagir et au-delà duquel il ne tombera pas.
Revenons au composant Ground et ajoutons un composant RigidBody comme enveloppe sur la surface du sol.
Désormais, en tombant, le cube reste au sol comme un véritable objet physique.
Créons un composant Player qui contrôlera le personnage sur la scène.
Le personnage est le même objet physique que le cube ajouté, il doit donc interagir avec la surface du sol ainsi qu'avec le cube sur la scène. C'est pourquoi nous ajoutons le composant RigidBody . Et créons le personnage sous la forme d'une capsule.
Placez le composant Player à l'intérieur du composant Physics.
Maintenant, notre personnage est apparu sur scène.
Le personnage sera contrôlé à l'aide des touches WASD et sautera à l'aide de la barre d'espace .
Avec notre propre crochet de réaction, nous implémentons la logique de déplacement du personnage.
Créons un fichier hooks.js et ajoutons-y une nouvelle fonction usePersonControls .
Définissons un objet au format {"keycode": "action à effectuer"}. Ensuite, ajoutez des gestionnaires d'événements pour appuyer et relâcher les touches du clavier. Lorsque les gestionnaires seront déclenchés, nous déterminerons les actions en cours d’exécution et mettrons à jour leur état actif. Comme résultat final, le hook renverra un objet au format {"action in progress": "status"}.
Après avoir implémenté le hook usePersonControls , il doit être utilisé lors du contrôle du personnage. Dans le composant Player , nous ajouterons le suivi de l'état de mouvement et mettrons à jour le vecteur de direction de mouvement du personnage.
Nous définirons également des variables qui stockeront les états des directions de mouvement.
Pour mettre à jour la position du personnage, utilisons le cadre fourni par le package @react-two/fiber . Ce hook fonctionne de la même manière que requestAnimationFrame et exécute le corps de la fonction environ 60 fois par seconde.
Explication du code :
1. const playerRef = useRef(); Créez un lien pour l'objet joueur. Ce lien permettra une interaction directe avec l'objet joueur sur la scène.
2. const { avancer, reculer, gauche, droite, sauter } = usePersonControls(); Lorsqu'un hook est utilisé, un objet avec des valeurs booléennes indiquant quels boutons de commande sont actuellement enfoncés par le joueur est renvoyé.
3. useFrame((state) => { ... }); Le hook est appelé sur chaque image de l'animation. A l'intérieur de ce crochet, la position et la vitesse linéaire du joueur sont mises à jour.
4. if (!playerRef.current) return; Vérifie la présence d'un objet joueur. S'il n'y a pas d'objet joueur, la fonction arrêtera l'exécution pour éviter les erreurs.
5. vitesse const = playerRef.current.linvel(); Obtenez la vitesse linéaire actuelle du joueur.
6. frontVector.set(0, 0, arrière - avant) ; Définissez le vecteur de mouvement avant/arrière en fonction des boutons enfoncés.
7. sideVector.set(gauche - droite, 0, 0); Définissez le vecteur de mouvement gauche/droite.
8. direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); Calculez le vecteur final du mouvement du joueur en soustrayant les vecteurs de mouvement, en normalisant le résultat (de sorte que la longueur du vecteur soit de 1) et en multipliant par la constante de vitesse de mouvement.
9. playerRef.current.wakeUp(); "Réveille" l'objet joueur pour s'assurer qu'il réagit aux changements. Si vous n'utilisez pas cette méthode, après un certain temps, l'objet "se mettra en veille" et ne réagira plus aux changements de position.
10. playerRef.current.setLinvel({ x : direction.x, y : vitesse.y, z : direction.z }); Définissez la nouvelle vitesse linéaire du joueur en fonction de la direction de mouvement calculée et conservez la vitesse verticale actuelle (afin de ne pas affecter les sauts ou les chutes).
En conséquence, en appuyant sur les touches WASD , le personnage a commencé à se déplacer dans la scène. Il peut également interagir avec le cube, car ce sont tous deux des objets physiques.
Afin d'implémenter le saut, utilisons les fonctionnalités des packages @dimforge/rapier3d-compat et @react-trois/rapier . Dans cet exemple, vérifions que le personnage est au sol et que la touche saut a été enfoncée. Dans ce cas, nous définissons la direction et la force d'accélération du personnage sur l'axe Y.
Pour Player, nous ajouterons une rotation de masse et de bloc sur tous les axes, afin qu'il ne tombe pas dans des directions différentes lors d'une collision avec d'autres objets de la scène.
Explication du code :
- const monde = rapier.world; Accéder à la scène du moteur physique Rapier . Il contient tous les objets physiques et gère leur interaction.
- const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x : 0, y : -1, z : 0 })); C'est ici qu'a lieu le "raycasting" (raycasting). Un rayon est créé qui commence à la position actuelle du joueur et pointe vers le bas de l'axe y. Ce rayon est « projeté » dans la scène pour déterminer s'il croise un objet de la scène.
- const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1,5; La condition est vérifiée si le joueur est au sol :
- rayon - si le rayon a été créé ;
- ray.collider - si le rayon est entré en collision avec un objet sur la scène ;
- Math.abs(ray.toi) - le "temps d'exposition" du rayon. Si cette valeur est inférieure ou égale à la valeur donnée, cela peut indiquer que le joueur est suffisamment proche de la surface pour être considéré « au sol ».
Vous devez également modifier le composant Sol pour que l'algorithme de lancer de rayons permettant de déterminer l'état « d'atterrissage » fonctionne correctement, en ajoutant un objet physique qui interagira avec d'autres objets de la scène.
Montons la caméra un peu plus haut pour avoir une meilleure vue de la scène.
Code de section
Pour déplacer la caméra, nous obtiendrons la position actuelle du joueur et changerons la position de la caméra à chaque fois que l'image sera actualisée. Et pour que le personnage se déplace exactement le long de la trajectoire vers laquelle la caméra est dirigée, nous devons ajouter applyEuler .
Explication du code :
La méthode applyEuler applique la rotation à un vecteur en fonction des angles d'Euler spécifiés. Dans ce cas, la rotation de la caméra est appliquée au vecteur direction . Ceci est utilisé pour faire correspondre le mouvement par rapport à l'orientation de la caméra, de sorte que le joueur se déplace dans la direction de rotation de la caméra.
Ajustons légèrement la taille du Player et rendons-le plus grand par rapport au cube, augmentant ainsi la taille de CapsuleCollider et corrigeant la logique de "saut".
Code de section
Pour que la scène ne semble pas complètement vide, ajoutons la génération de cubes. Dans le fichier json, listez les coordonnées de chacun des cubes puis affichez-les sur la scène. Pour ce faire, créez un fichier cubes.json , dans lequel nous listerons un tableau de coordonnées.
[ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]
Dans le fichier Cube.jsx , créez un composant Cubes , qui générera des cubes en boucle. Et le composant Cube sera un objet directement généré.
import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); }
Ajoutons le composant Cubes créé au composant App en supprimant le cube unique précédent.
Ajoutons maintenant un modèle 3D à la scène. Ajoutons un modèle d'arme pour le personnage. Commençons par rechercher un modèle 3D. Par exemple, prenons celui-ci .
Téléchargez le modèle au format GLTF et décompressez l'archive à la racine du projet.
Afin d'obtenir le format dont nous avons besoin pour importer le modèle dans la scène, nous devrons installer le package complémentaire gltf-pipeline .
npm i -D gltf-pipeline
À l'aide du package gltf-pipeline , reconvertissez le modèle du format GLTF au format GLB , car dans ce format toutes les données du modèle sont placées dans un seul fichier. En tant que répertoire de sortie pour le fichier généré, nous spécifions le dossier public .
gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb
Ensuite, nous devons générer un composant React qui contiendra le balisage de ce modèle pour l'ajouter à la scène. Utilisons la ressource officielle des développeurs @react-trois/fiber .
Pour accéder au convertisseur, vous devrez charger le fichier arme.glb converti.
Par glisser-déposer ou par recherche dans l'Explorateur, recherchez ce fichier et téléchargez-le.
Dans le convertisseur, nous verrons le composant de réaction généré, dont nous transférerons le code à notre projet dans un nouveau fichier WeaponModel.jsx , en changeant le nom du composant pour le même nom que le fichier.
Importons maintenant le modèle créé dans la scène. Dans le fichier App.jsx , ajoutez le composant WeaponModel .
À ce stade de notre scène, aucun des objets ne projette d’ombre.
Pour activer les ombres sur la scène, vous devez ajouter l'attribut shadows au composant Canvas .
Ensuite, nous devons ajouter une nouvelle source de lumière. Bien que nous ayons déjà ambientLight sur la scène, il ne peut pas créer d'ombres pour les objets, car il ne dispose pas de faisceau lumineux directionnel. Ajoutons donc une nouvelle source de lumière appelée directionnelleLight et configurons-la. L'attribut permettant d'activer le mode ombre " cast " est castShadow . C'est l'ajout de ce paramètre qui indique que cet objet peut projeter une ombre sur d'autres objets.
Après cela, ajoutons un autre attribut containShadow au composant Ground , ce qui signifie que le composant de la scène peut recevoir et afficher des ombres sur lui-même.
Des attributs similaires doivent être ajoutés aux autres objets de la scène : cubes et joueur. Pour les cubes, nous ajouterons castShadow et containShadow , car ils peuvent à la fois projeter et recevoir des ombres, et pour le joueur, nous ajouterons uniquement castShadow .
Ajoutons castShadow pour Player .
Ajoutez castShadow et containShadow pour Cube .
Si vous regardez attentivement maintenant, vous constaterez que la surface sur laquelle l’ombre est projetée est assez petite. Et en dépassant cette zone, l’ombre est simplement coupée.
La raison en est que, par défaut, la caméra ne capture qu'une petite zone des ombres affichées de directionnelLight . Nous pouvons pour le composant directionnelLight en ajoutant des attributs supplémentaires shadow-camera-(top, bottom, left, right) pour étendre cette zone de visibilité. Après avoir ajouté ces attributs, l'ombre deviendra légèrement floue. Pour améliorer la qualité, nous ajouterons l'attribut shadow-mapSize .
Ajoutons maintenant l'affichage des armes à la première personne. Créez un nouveau composant d'arme , qui contiendra la logique de comportement de l'arme et le modèle 3D lui-même.
import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }
Plaçons ce composant au même niveau que le RigidBody du personnage et dans le hook useFrame nous définirons la position et l'angle de rotation en fonction de la position des valeurs de la caméra.
Pour rendre la démarche du personnage plus naturelle, nous ajouterons un léger mouvement de l'arme lors du déplacement. Pour créer l'animation, nous utiliserons la bibliothèque tween.js installée.
Le composant Weapon sera enveloppé dans une balise de groupe afin que vous puissiez y ajouter une référence via le hook useRef .
Ajoutons un useState pour enregistrer l'animation.
Créons une fonction pour initialiser l'animation.
Explication du code :
- const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Création d'une animation d'un objet "balançant" de sa position actuelle vers une nouvelle position.
- const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Création d'une animation de l'objet revenant à sa position de départ une fois la première animation terminée.
- twSwayingAnimation.chain(twSwayingBackAnimation); Connecter deux animations afin que lorsque la première animation se termine, la deuxième animation démarre automatiquement.
Dans useEffect , nous appelons la fonction d'initialisation de l'animation.
Il faut maintenant déterminer le moment pendant lequel le mouvement se produit. Cela peut être fait en déterminant le vecteur actuel de la direction du personnage.
Si un mouvement de personnage se produit, nous actualiserons l'animation et la réexécuterons une fois terminé.
Explication du code :
- const isMoving = direction.length() > 0; Ici, l'état de mouvement de l'objet est vérifié. Si le vecteur direction a une longueur supérieure à 0, cela signifie que l'objet a une direction de mouvement.
- if (isMoving && isSwayingAnimationFinished) { ... } Cet état est exécuté si l'objet se déplace et que l'animation "swinging" est terminée.
Dans le composant App , ajoutons un useFrame où nous mettrons à jour l'animation interpolée.
TWEEN.update() met à jour toutes les animations actives dans la bibliothèque TWEEN.js . Cette méthode est appelée sur chaque image d'animation pour garantir le bon déroulement de toutes les animations.
Code de rubrique :
Nous devons définir le moment où un coup de feu est tiré, c'est-à-dire le moment où le bouton de la souris est enfoncé. Ajoutons useState pour stocker cet état, useRef pour stocker une référence à l'objet arme et deux gestionnaires d'événements pour appuyer et relâcher le bouton de la souris.
Implémentons une animation de recul lorsque vous cliquez sur le bouton de la souris. Nous utiliserons la bibliothèque tween.js à cet effet.
Définissons des constantes pour la force de recul et la durée de l'animation.
Comme pour l'animation de mouvement de l'arme, nous ajoutons deux états useState pour l'animation de recul et de retour à la position d'origine et un état avec l'état de fin de l'animation.
Créons des fonctions pour obtenir un vecteur aléatoire d'animation de recul - generateRecoilOffset et generateNewPositionOfRecoil .
Créez une fonction pour initialiser l'animation de recul. Nous ajouterons également useEffect , dans lequel nous spécifierons l'état "shot" comme dépendance, afin qu'à chaque plan l'animation soit à nouveau initialisée et de nouvelles coordonnées de fin soient générées.
Et dans useFrame , ajoutons une vérification pour "maintenir" la touche de la souris pour le tir, afin que l'animation de tir ne s'arrête pas tant que la touche n'est pas relâchée.
Réaliser l'animation « d'inactivité » pour le personnage, afin qu'il n'y ait pas de sensation de jeu « suspendu ».
Pour ce faire, ajoutons de nouveaux états via useState .
Corrigeons l'initialisation de l'animation "wiggle" pour utiliser les valeurs de l'état. L'idée est que différents états : marcher ou s'arrêter, utiliseront des valeurs différentes pour l'animation et à chaque fois l'animation sera initialisée en premier.
Dans cette partie, nous avons implémenté la génération de scènes et le mouvement des personnages. Nous avons également ajouté un modèle d'arme, une animation de recul lors du tir et au ralenti. Dans la prochaine partie, nous continuerons à affiner notre jeu en ajoutant de nouvelles fonctionnalités.
Également publié ici .