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 - un outil puissant pour créer des graphiques 3D basés sur à 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 2D à des projets 3D complexes comparables à des applications bureautiques. Cette croissance en popularité et en capacités fait du Web-GameDev un domaine incontournable.
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 . 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.
Tout d'abord, nous aurons besoin d'un modèle de projet React . Commençons donc par l'installer.
npm create vite@latest
npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js
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.
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.
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 .
<mesh position={[0, 3, -5]}> <boxGeometry /> </mesh>
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 .
Revenons au composant Ground et ajoutons un composant RigidBody comme enveloppe sur la surface du sol.
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.
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 .
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.
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.
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".
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.
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 des développeurs @react-trois/fiber .
Pour accéder au convertisseur, vous devrez charger le fichier arme.glb converti.
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 .
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 .
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.
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.
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.
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.
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.
Pour ce faire, ajoutons de nouveaux états via useState .