Physics

Starter pack

La física abre un mundo completamente nuevo de posibilidades para tus proyectos 3D. Puedes crear universos realistas, interacciones de usuario e incluso juegos.

En esta lección, descubriremos los conceptos esenciales mientras construimos un juego simple.

No te preocupes, no se requiere conocimiento previo; comenzaremos desde cero. (Para que lo sepas, yo era muy mal estudiante en física en la escuela, así que si yo puedo hacerlo, tú también puedes hacerlo).

Motores de física

Para agregar física a nuestros proyectos 3D, utilizaremos un motor de física. Un motor de física es una biblioteca que manejará todas las matemáticas complejas por nosotros, como la gravedad, las colisiones, las fuerzas, etc.

En el ecosistema de JavaScript, hay muchos motores de física disponibles.

Dos muy populares son Cannon.js y Rapier.js.

Poimandres (de nuevo) ha creado dos excelentes bibliotecas para usar estos motores con React Three Fiber: react-three-rapier y use-cannon.

En esta lección, utilizaremos react-three-rapier, pero son bastante similares y los conceptos que aprenderemos aquí se pueden aplicar a ambos.

Para instalarlo, ejecuta:

yarn add @react-three/rapier

¡Ahora estamos listos para empezar!

Mundo de Física

Antes de crear el juego, cubramos los conceptos esenciales.

Primero, necesitamos crear un mundo de física. Este mundo contendrá todos los objetos de física de nuestra escena. Con react-three-rapier, simplemente necesitamos envolver todos nuestros objetos con un componente <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;

Nuestro mundo ya está listo pero ¡no pasa nada! Eso es porque aún no tenemos objetos de física.

Rigidbody

Para agregar física a un objeto, necesitamos agregar un rigidbody. Un rigidbody es un componente que hará que nuestro objeto se mueva en el mundo físico.

¿Qué puede desencadenar el movimiento de un objeto? Fuerzas, como la gravedad, colisiones o interacciones del usuario.

Vamos a decirle a nuestro mundo de física que nuestro cubo ubicado en Player.jsx es un objeto físico añadiéndole un rigidbody:

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

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

Ahora nuestro cubo está respondiendo a la gravedad y se está cayendo. ¡Pero está cayendo para siempre!

Necesitamos hacer que el suelo también sea un objeto físico para que el cubo pueda colisionar con él y dejar de caer.

Añadamos un rigidbody al suelo en Experience.jsx, pero como no queremos que se mueva y caiga como el cubo, añadiremos el 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 on top of the ground

De vuelta al punto de partida, tenemos un cubo inmóvil sobre el suelo. Pero, bajo el capó, tenemos un cubo reaccionando a la gravedad y detenido por su colisión con el suelo.

Fuerzas

Ahora que tenemos un mundo físico y objetos físicos, podemos empezar a jugar con fuerzas.

Haremos que el cubo se mueva con las flechas del teclado. Para eso, usemos KeyboardControls que descubrimos en la lección de eventos:

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

Ahora podemos obtener las teclas presionadas en nuestro componente 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() es una manera alternativa de obtener las teclas presionadas con el componente KeyboardControls.

Ahora que tenemos las teclas presionadas, podemos aplicar fuerzas al cubo. Podemos hacerlo con dos métodos:

  • applyImpulse: aplica una fuerza instantánea al objeto
  • setLinVel: establece la velocidad lineal del objeto

Vamos a descubrir ambos.

Añadamos un useRef al RigidBody, y usémoslo para aplicar un impulso para mover el cubo en la dirección correcta:

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

Asegúrate de asignar la referencia al RigidBody y no al mesh.

Funciona, pero está acelerando demasiado rápido y se desliza en el suelo. Podemos añadir más fricción al suelo para solucionar esto:

// ...

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

La fricción hace que el cubo gire porque agarra el suelo. Podemos solucionar esto bloqueando la rotación del cubo:

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

Ahora está mucho mejor, pero el cubo sigue deslizando un poco. Podemos solucionar esto ajustando la amortiguación lineal del cubo, pero no seguiremos este camino en esta lección.

También necesitaríamos ajustar la velocidad máxima del cubo para evitar que acelere constantemente. Nos enfrentaríamos a problemas cuando usemos las teclas izquierda y derecha para rotar el cubo en lugar de moverlo.

Cambiemos nuestro sistema para usar setLinVel en lugar 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>;
};

También puedes eliminar la fricción del suelo porque ya no es necesaria.

F2 es tu amigo para renombrar rápidamente variables.

¡Genial! Ahora tenemos un cubo que puede moverse con las flechas del teclado.

Añadamos una fuerza de salto cuando el usuario presione la barra espaciadora:

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

Tenemos dos problemas:

  • El cubo no reacciona correctamente a la gravedad (compáralo con el cubo que cae al principio de la lección)
  • El cubo puede saltar cuando ya está en el aire

El problema de la gravedad es porque establecemos manualmente la velocidad del cubo en el eje y. Necesitamos cambiarla solo cuando saltamos, y dejar que el motor de físicas maneje la gravedad el resto del tiempo:

// ...

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

Obtenemos la velocidad actual del cubo con rb.current.linvel() y si no estamos saltando, establecemos la velocidad y a la actual.

Para evitar que el cubo salte cuando ya está en el aire, podemos comprobar si el cubo ha tocado el suelo antes de poder saltar de nuevo:

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

Ahora solo podemos saltar una vez, y la gravedad está funcionando, pero un poco lenta.

Antes de solucionarlo, veamos cómo funciona nuestro sistema de colisiones.

End of lesson preview

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