Física

Starter pack

A física desbloqueia um mundo totalmente novo de possibilidades para seus projetos 3D. Você pode criar universos realistas, interações com usuários e até jogos.

Nesta lição, vamos descobrir os conceitos essenciais enquanto construímos um jogo simples.

Não se preocupe, nenhum conhecimento prévio é necessário, começaremos do zero. (Para que você saiba, eu era um péssimo aluno em física na escola, então se eu consegui, você também pode conseguir!)

Motores de física

Para adicionar física aos nossos projetos 3D, usaremos um motor de física. Um motor de física é uma biblioteca que lidará com toda a matemática complexa para nós, como gravidade, colisões, forças, etc.

No ecossistema JavaScript, existem muitos motores de física disponíveis.

Dois muito populares são Cannon.js e Rapier.js.

Poimandres (novamente) criou duas ótimas bibliotecas para usar esses motores com React Three Fiber: react-three-rapier e use-cannon.

Nesta lição, usaremos react-three-rapier, mas eles são bem parecidos, e os conceitos que aprenderemos aqui podem ser aplicados a ambos.

Para instalá-lo, execute:

yarn add @react-three/rapier

Agora estamos prontos para começar!

Mundo de Física

Antes de criar o jogo, vamos cobrir os conceitos essenciais.

Primeiro, precisamos criar um mundo de física. Este mundo conterá todos os objetos físicos da nossa cena. Com react-three-rapier, simplesmente precisamos envolver todos os nossos objetos com um 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;

Nosso mundo agora está pronto, mas nada acontece! Isso porque ainda não temos objetos físicos.

Rigidbody

Para adicionar física a um objeto, precisamos adicionar um rigidbody. Um rigidbody é um componente que fará o nosso objeto se mover no mundo da física.

O que pode desencadear o movimento de um objeto? Forças, como gravidade, colisões ou interações do usuário.

Vamos informar ao nosso mundo da física que o nosso cubo localizado em Player.jsx é um objeto de física adicionando um rigidbody a ele:

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

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

Agora o nosso cubo está respondendo à gravidade e caindo. Mas ele está caindo para sempre!

Precisamos fazer o chão ser um objeto de física também para que o cubo possa colidir com ele e parar de cair.

Vamos adicionar um rigidbody ao chão em Experience.jsx, mas como não queremos que ele se mova e caia como o cubo, adicionaremos a 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>
      {/* ... */}
    </>
  );
};

Cubo sobre o chão

Voltando ao ponto inicial, temos um cubo imóvel sobre o chão. Mas, por trás das cortinas, temos um cubo reagindo à gravidade e parado por sua colisão com o chão.

Forces

Agora que temos um mundo físico e objetos físicos, podemos começar a brincar com forças.

Vamos fazer o cubo se mover com as setas do teclado. Para isso, vamos usar o KeyboardControls que descobrimos na aula 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;

Agora podemos obter as teclas pressionadas no nosso 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() é uma maneira alternativa de obter as teclas pressionadas com o componente KeyboardControls.

Agora que temos as teclas pressionadas, podemos aplicar forças ao cubo. Podemos fazer isso com dois métodos:

  • applyImpulse: aplica uma força instantânea ao objeto
  • setLinVel: define a velocidade linear do objeto

Vamos descobrir ambos.

Vamos adicionar um useRef ao RigidBody, e usá-lo para aplicar um impulso para mover o cubo na direção correta:

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

Cuidado para atribuir o ref ao RigidBody e não ao mesh.

Funciona, mas está acelerando muito rápido e desliza no chão. Podemos adicionar mais fricção ao chão para corrigir isso:

// ...

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

A fricção faz com que o cubo gire porque adere ao solo. Podemos corrigir isso bloqueando a rotação do cubo:

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

Está muito melhor agora, mas o cubo ainda desliza um pouco. Podemos corrigir isso ajustando o linear damping do cubo, mas não vamos seguir esse caminho nesta lição.

Porque também precisaríamos ajustar a velocidade máxima do cubo para evitar que ele acelere constantemente. Teríamos problemas quando usássemos as teclas esquerda e direita para rotacionar o cubo em vez de movê-lo.

Vamos mudar nosso sistema para usar setLinVel em vez 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>;
};

Você também pode remover a fricção do solo, pois não é mais necessária.

F2 é seu amigo para renomear variáveis rapidamente.

Ótimo! Agora temos um cubo que pode se mover com as setas do teclado.

Vamos adicionar uma força de pulo quando o usuário pressionar a barra de espaço:

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

Temos dois problemas:

  • O cubo não está reagindo corretamente à gravidade (compare com o cubo caindo no início da lição)
  • O cubo pode pular mesmo quando já está no ar

O problema da gravidade é porque definimos manualmente a velocidade do cubo no eixo y. Precisamos mudá-la somente quando pulamos e deixar o motor de física lidar com a gravidade o resto do tempo:

// ...

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

Pegamos a velocidade atual do cubo com rb.current.linvel() e, se não estivermos pulando, definimos a velocidade y para a atual.

Para impedir que o cubo pule quando já está no ar, podemos verificar se o cubo tocou o chão antes de poder pular novamente:

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

Agora só podemos pular uma vez e a gravidade está funcionando, mas um pouco devagar.

Antes de corrigir isso, vamos ver como nosso sistema de colisão funciona.

End of lesson preview

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