Wawa Sensei logo

Physique

Starter pack

La physique ouvre un tout nouveau monde de possibilités pour vos projets 3D. Vous pouvez créer des univers réalistes, des interactions utilisateur et même des jeux.

Dans cette leçon, nous découvrirons les concepts essentiels en construisant un jeu simple.

Ne vous inquiétez pas, aucune connaissance préalable n'est requise, nous commencerons de zéro. (Pour vous dire, j'étais un très mauvais élève en physique à l'école, donc si je peux le faire, vous pouvez le faire aussi!)

Moteurs physiques

Pour ajouter de la physique à nos projets 3D, nous utiliserons un moteur physique. Un moteur physique est une bibliothèque qui gérera tous les calculs complexes pour nous, tels que la gravité, les collisions, les forces, etc.

Dans l'écosystème JavaScript, il existe de nombreux moteurs physiques disponibles.

Deux très populaires sont Cannon.js et Rapier.js.

Poimandres (encore une fois) a créé deux excellentes bibliothèques pour utiliser ces moteurs avec React Three Fiber : react-three-rapier et use-cannon.

Dans cette leçon, nous utiliserons react-three-rapier mais ils sont assez similaires et les concepts que nous apprendrons ici peuvent s'appliquer aux deux.

Pour l'installer, exécutez :

yarn add @react-three/rapier

Nous sommes maintenant prêts à commencer!

Monde Physique

Avant de créer le jeu, couvrons les concepts essentiels.

Tout d'abord, nous devons créer un monde physique. Ce monde contiendra tous les objets physiques de notre scène. Avec react-three-rapier, il suffit d'envelopper tous nos objets avec un composant <Physics>:

// ...
import { Physics } from "@react-three/rapier";

function App() {
  return (
    <>
      <Canvas camera={{ position: [0, 6, 6], fov: 60 }} shadows>
        <color attach="background" args={["#171720"]} />
        <Physics>
          <Experience />
        </Physics>
      </Canvas>
    </>
  );
}

export default App;

Notre monde est maintenant prêt, mais rien ne se passe! C'est parce que nous n'avons pas encore d'objets physiques.

Rigidbody

Pour ajouter de la physique à un objet, nous devons ajouter un rigidbody. Un rigidbody est un composant qui fera bouger notre objet dans le monde physique.

Qu'est-ce qui peut déclencher le mouvement d'un objet ? Les forces, telles que la gravité, les collisions, ou les interactions utilisateur.

Disons à notre monde physique que notre cube situé dans Player.jsx est un objet physique en y ajoutant un rigidbody :

import { RigidBody } from "@react-three/rapier";

export const Player = () => {
  return <RigidBody>{/* ... */}</RigidBody>;
};

Maintenant, notre cube répond à la gravité et tombe. Mais il tombe infiniment !

Nous devons également faire du sol un objet physique pour que le cube puisse entrer en collision avec lui et arrêter de tomber.

Ajoutons un rigidbody au sol dans Experience.jsx, mais comme nous ne voulons pas qu'il bouge et tombe comme le cube, nous ajouterons la prop type="fixed" :

// ...
import { RigidBody } from "@react-three/rapier";

export const Experience = () => {
  return (
    <>
      {/* ... */}
      <RigidBody type="fixed">
        <mesh position-y={-0.251} receiveShadow>
          <boxGeometry args={[20, 0.5, 20]} />
          <meshStandardMaterial color="mediumpurple" />
        </mesh>
      </RigidBody>
      {/* ... */}
    </>
  );
};

Cube sur le sol

Retour à la case départ, nous avons un cube immobile au-dessus du sol. Mais, en coulisses, nous avons un cube réagissant à la gravité et arrêté par sa collision avec le sol.

Forces

Maintenant que nous avons un monde physique et des objets physiques, nous pouvons commencer à jouer avec les forces.

Nous allons faire bouger le cube avec les flèches du clavier. Pour ce faire, utilisons KeyboardControls que nous avons découverts dans la leçon événements:

// ...
import { KeyboardControls } from "@react-three/drei";
import { useMemo } from "react";

export const Controls = {
  forward: "forward",
  back: "back",
  left: "left",
  right: "right",
  jump: "jump",
};

function App() {
  const map = useMemo(
    () => [
      { name: Controls.forward, keys: ["ArrowUp", "KeyW"] },
      { name: Controls.back, keys: ["ArrowDown", "KeyS"] },
      { name: Controls.left, keys: ["ArrowLeft", "KeyA"] },
      { name: Controls.right, keys: ["ArrowRight", "KeyD"] },
      { name: Controls.jump, keys: ["Space"] },
    ],
    []
  );
  return <KeyboardControls map={map}>{/* ... */}</KeyboardControls>;
}

export default App;

Nous pouvons maintenant obtenir les touches pressées dans notre composant Player.jsx :

// ...
import { Controls } from "../App";
import { useKeyboardControls } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";

export const Player = () => {
  const [, get] = useKeyboardControls();

  useFrame(() => {
    if (get()[Controls.forward]) {
    }
    if (get()[Controls.back]) {
    }
    if (get()[Controls.left]) {
    }
    if (get()[Controls.right]) {
    }
    if (get()[Controls.jump]) {
    }
  });
  // ...
};

get() est un moyen alternatif d'obtenir les touches pressées avec le composant KeyboardControls.

Maintenant que nous avons les touches pressées, nous pouvons appliquer des forces au cube. Nous pouvons le faire avec deux méthodes :

  • applyImpulse : appliquer une force instantanée à l'objet
  • setLinVel : définir la vitesse linéaire de l'objet

Découvrons les deux.

Ajoutons un useRef au RigidBody, et utilisons-le pour appliquer une impulsion pour déplacer le cube dans la bonne direction :

import { useRef } from "react";
import { Vector3 } from "three";
const MOVEMENT_SPEED = 0.5;

export const Player = () => {
  const rb = useRef();
  const [, get] = useKeyboardControls();
  const impulse = new Vector3();
  useFrame(() => {
    impulse.x = 0;
    impulse.y = 0;
    impulse.z = 0;
    if (get()[Controls.forward]) {
      impulse.z -= MOVEMENT_SPEED;
    }
    if (get()[Controls.back]) {
      impulse.z += MOVEMENT_SPEED;
    }
    if (get()[Controls.left]) {
      impulse.x -= MOVEMENT_SPEED;
    }
    if (get()[Controls.right]) {
      impulse.x += MOVEMENT_SPEED;
    }
    if (get()[Controls.jump]) {
    }
    rb.current.applyImpulse(impulse, true);
  });
  return <RigidBody ref={rb}>{/* ... */}</RigidBody>;
};

Faites attention à assigner la ref au RigidBody et non à la mesh.

Cela fonctionne, mais cela accélère trop vite et glisse sur le sol. Nous pouvons ajouter plus de friction au sol pour corriger cela :

// ...

export const Experience = () => {
  // ...
  return (
    <>
      {/* ... */}
      <RigidBody type="fixed" friction={5}>
        {/* ... */}
      </RigidBody>
      {/* ... */}
    </>
  );
};

La friction fait que le cube tourne parce qu'il adhère au sol. Nous pouvons corriger cela en verrouillant la rotation du cube :

// ...
export const Player = () => {
  // ...
  return (
    <RigidBody ref={rb} lockRotations>
      {/* ... */}
    </RigidBody>
  );
};

C'est maintenant bien mieux, mais le cube glisse encore un peu. Nous pourrions corriger cela en ajustant linear damping du cube mais nous n'allons pas suivre cette piste dans cette leçon.

Parce que nous aurions également besoin d'ajuster la vitesse maximale du cube pour empêcher qu'il n'accélère constamment. Nous rencontrerions des problèmes lorsque nous utiliserons les touches gauche et droite pour faire tourner le cube au lieu de le déplacer.

Passons à notre système pour utiliser setLinVel au lieu de applyImpulse:

// ...
import { useRef } from "react";
import { Vector3 } from "three";

const MOVEMENT_SPEED = 5;

export const Player = () => {
  // ...
  const rb = useRef();
  const vel = new Vector3();
  useFrame(() => {
    vel.x = 0;
    vel.y = 0;
    vel.z = 0;
    if (get()[Controls.forward]) {
      vel.z -= MOVEMENT_SPEED;
    }
    if (get()[Controls.back]) {
      vel.z += MOVEMENT_SPEED;
    }
    if (get()[Controls.left]) {
      vel.x -= MOVEMENT_SPEED;
    }
    if (get()[Controls.right]) {
      vel.x += MOVEMENT_SPEED;
    }
    if (get()[Controls.jump]) {
    }
    rb.current.setLinvel(vel, true);
  });
  return <RigidBody ref={rb}>{/* ... */}</RigidBody>;
};

Vous pouvez également retirer la friction du sol car elle n'est plus nécessaire.

F2 est votre ami pour renommer rapidement les variables.

Génial ! Nous avons maintenant un cube qui peut bouger avec les flèches du clavier.

Ajoutons une force de saut lorsque l'utilisateur appuie sur la barre d'espace :

// ...
const JUMP_FORCE = 8;

export const Player = () => {
  // ...
  useFrame(() => {
    // ...
    if (get()[Controls.jump]) {
      vel.y += JUMP_FORCE;
    }
    rb.current.setLinvel(vel, true);
  });
  return (
    <RigidBody ref={rb} lockRotations>
      {/* ... */}
    </RigidBody>
  );
};

Nous avons deux problèmes :

  • Le cube ne réagit pas correctement à la gravité (comparez avec le cube qui tombe au début de la leçon)
  • Le cube peut sauter lorsqu'il est déjà en l'air

Le problème de gravité est que nous définissons manuellement la vitesse du cube sur l'axe y. Nous devons le changer uniquement lorsque nous sautons, et laisser le moteur physique gérer la gravité le reste du temps :

// ...

export const Player = () => {
  // ...
  useFrame(() => {
    const curVel = rb.current.linvel();
    if (get()[Controls.jump]) {
      vel.y += JUMP_FORCE;
    } else {
      vel.y = curVel.y;
    }
    rb.current.setLinvel(vel, true);
  });
  // ...
};

Nous obtenons la vitesse actuelle du cube avec rb.current.linvel() et si nous ne sautons pas, nous définissons la vitesse y à l'actuelle.

Pour empêcher le cube de sauter lorsqu'il est déjà en l'air, nous pouvons vérifier si le cube a touché le sol avant de pouvoir sauter à nouveau :

// ...
export const Player = () => {
  // ...
  const inTheAir = useRef(false);
  useFrame(() => {
    // ...
    if (get()[Controls.jump] && !inTheAir.current) {
      vel.y += JUMP_FORCE;
      inTheAir.current = true;
    } else {
      vel.y = curVel.y;
    }
    rb.current.setLinvel(vel, true);
  });
  // ...
};

Nous pouvons maintenant sauter une seule fois, et la gravité fonctionne mais un peu trop lentement.

Avant de corriger cela, voyons comment fonctionne notre système de collision.

End of lesson preview

To get access to the entire lesson, you need to purchase the course.