Fisica

Starter pack

La fisica sblocca un mondo completamente nuovo di possibilità per i tuoi progetti 3D. Puoi creare universi realistici, interazioni utente e persino giochi.

In questa lezione, scopriremo i concetti essenziali mentre costruiamo un semplice gioco.

Non preoccuparti, non è richiesta alcuna conoscenza preliminare; inizieremo da zero. (Per informarti, ero un pessimo studente in fisica a scuola, quindi se posso farlo io, puoi farlo anche tu!)

Motori fisici

Per aggiungere fisica ai nostri progetti 3D, utilizzeremo un motore fisico. Un motore fisico è una libreria che gestirà tutta la matematica complessa per noi, come gravità, collisioni, forze, ecc.

Nell'ecosistema JavaScript, ci sono molti motori fisici disponibili.

Due molto popolari sono Cannon.js e Rapier.js.

Poimandres (ancora una volta) ha realizzato due grandi librerie per utilizzare questi motori con React Three Fiber: react-three-rapier e use-cannon.

In questa lezione, useremo react-three-rapier, ma sono abbastanza simili e i concetti che impareremo qui possono essere applicati a entrambi.

Per installarlo, esegui:

yarn add @react-three/rapier

Ora siamo pronti per iniziare!

Mondo Fisico

Prima di creare il gioco, copriamo i concetti essenziali.

Per prima cosa, dobbiamo creare un mondo fisico. Questo mondo conterrà tutti gli oggetti fisici della nostra scena. Con react-three-rapier, dobbiamo semplicemente avvolgere tutti i nostri oggetti 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;

Il nostro mondo è ora pronto ma non accade nulla! Questo perché non abbiamo ancora oggetti fisici.

Corpo rigido

Per aggiungere fisica a un oggetto, dobbiamo aggiungere un rigidbody. Un corpo rigido è un componente che farà muovere il nostro oggetto nel mondo fisico.

Cosa può innescare il movimento di un oggetto? Forze, come ad esempio la gravità, le collisioni o le interazioni dell'utente.

Informiamo il nostro mondo fisico che il nostro cubo situato in Player.jsx è un oggetto fisico aggiungendo un rigidbody ad esso:

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

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

Ora il nostro cubo risponde alla gravità e cade. Ma sta cadendo all'infinito!

Dobbiamo fare in modo che anche il terreno sia un oggetto fisico, in modo che il cubo possa collidere con esso e smettere di cadere.

Aggiungiamo un rigidbody al terreno in Experience.jsx, ma poiché non vogliamo che si muova e cada come il cubo, aggiungeremo 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>
      {/* ... */}
    </>
  );
};

Cubo sulla superficie

Tornato al punto di partenza, abbiamo un cubo fermo sulla superficie. Ma, dietro le quinte, abbiamo un cubo che reagisce alla gravità e viene fermato dalla sua collisione con il terreno.

Forze

Ora che abbiamo un mondo fisico e oggetti fisici, possiamo iniziare a giocare con le forze.

Faremo muovere il cubo con le frecce della tastiera. Per farlo usiamo KeyboardControls che abbiamo scoperto nella lezione sugli eventi:

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

Possiamo ora ottenere i tasti premuti nel nostro 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() è un modo alternativo per ottenere i tasti premuti con il componente KeyboardControls.

Ora che abbiamo i tasti premuti, possiamo applicare forze al cubo. Possiamo farlo con due metodi:

  • applyImpulse: applica una forza istantanea all'oggetto
  • setLinVel: imposta la velocità lineare dell'oggetto

Scopriamoli entrambi.

Aggiungiamo un useRef al RigidBody, e usiamolo per applicare un impulso per muovere il cubo nella direzione giusta:

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

Assicurati di assegnare il ref al RigidBody e non al mesh.

Funziona, ma accelera troppo rapidamente e scivola sul terreno. Possiamo aggiungere più attrito al terreno per risolvere questo problema:

// ...

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

L'attrito fa ruotare il cubo perché aderisce al terreno. Possiamo risolvere questo bloccando la rotazione del cubo:

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

È decisamente meglio, ma il cubo scivola ancora un po'. Possiamo risolvere aggiustando il linear damping del cubo ma non seguiremo questa strada in questa lezione.

Perché dovremmo anche regolare la velocità massima del cubo per impedirgli di accelerare costantemente. Affronteremmo problemi quando useremo i tasti sinistra e destra per ruotare il cubo invece di muoverlo.

Passiamo al nostro sistema per usare setLinVel invece di 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>;
};

Puoi anche rimuovere l'attrito dal terreno perché non è più necessario.

F2 è il tuo amico per rinominare rapidamente le variabili.

Ottimo! Ora abbiamo un cubo che può muoversi con le frecce della tastiera.

Aggiungiamo una forza di salto quando l'utente preme la barra spaziatrice:

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

Abbiamo due problemi:

  • Il cubo non reagisce correttamente alla gravità (confronta con il cubo in caduta all'inizio della lezione)
  • Il cubo può saltare quando è già in aria

Il problema della gravità è dovuto al fatto che abbiamo impostato manualmente la velocità del cubo sull'asse y. Dobbiamo cambiarla solo quando saltiamo e lasciare che il motore fisico gestisca la gravità il resto del 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);
  });
  // ...
};

Otteniamo la velocità attuale del cubo con rb.current.linvel() e se non stiamo saltando, impostiamo la velocità y a quella attuale.

Per impedire al cubo di saltare quando è già in aria, possiamo controllare se il cubo ha toccato il terreno prima di essere in grado di saltare di nuovo:

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

Ora possiamo saltare solo una volta, e la gravità funziona ma è un po' troppo lenta.

Prima di sistemarli, vediamo come funziona il nostro sistema di collisione.

Colliders

I colliders sono responsabili del rilevamento delle collisioni tra oggetti. Sono collegati al componente RigidBody.

Rapier aggiunge automaticamente un collider al componente RigidBody basandosi sulla geometria del mesh, ma possiamo anche aggiungerli manualmente.

Per visualizzare i colliders, possiamo utilizzare la prop debug sul componente Physics:

// ...

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

export default App;

Poiché utilizziamo box geometries per il cubo e il terreno, i colliders correnti li racchiudono perfettamente e possiamo a malapena vedere i loro colori outline dalla modalità debug.

Passiamo il nostro collider del cubo a un collider sphere:

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

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

Questo metodo è semi automatico, diciamo a rapier quale tipo di collider vogliamo e lo creerà automaticamente basandosi sulla dimensione del suo mesh.

Possiamo anche aggiungere il collider manualmente e modificarlo:

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

export const Player = () => {
  // ...
  return (
    <RigidBody ref={rb} lockRotations colliders={false}>
      {/* ... */}
      <BallCollider args={[1.5]} />
    </RigidBody>
  );
};

Impostiamo colliders su false per impedire a rapier di creare automaticamente un collider.

Ball collider che racchiude il nostro cubo

I diversi tipi di colliders sono:

  • box: un collider box
  • ball: un collider sphere
  • hull: pensalo come un involucro regalo attorno al tuo mesh
  • trimesh: un collider che racchiuderà perfettamente il tuo mesh

Usa sempre il collider più semplice possibile per migliorare le prestazioni.

Ora che conosciamo i nostri cube e ground colliders, rileviamo la collisione tra loro per sapere quando il cubo è sul terreno.

Rimuoviamo il ball collider dal cubo e aggiungiamo la prop onCollisionEnter sul RigidBody:

// ...

export const Player = () => {
  // ...
  return (
    <RigidBody
    {/* ... */}
      onCollisionEnter={({ other }) => {
        if (other.rigidBodyObject.name === "ground") {
          inTheAir.current = false;
        }
      }}
    >
      {/* ... */}
    </RigidBody>
  );
};

Possiamo accedere all'altro collider con other.rigidBodyObject e controllarne il nome per sapere se è il terreno.

Dobbiamo aggiungere un nome al collider del terreno:

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

Adesso possiamo saltare di nuovo quando tocchiamo il terreno.

Gravità

La gravità è stata scoperta da Isaac Newton nel 1687... 🍎

Beh, sto scherzando, sappiamo cos'è la gravità.

Abbiamo due opzioni per modificare la gravità, possiamo cambiarla globalmente con la prop gravity sul componente Physics:

<Physics debug gravity={[0, -50, 0]}>

Accetta un array di tre numeri, uno per ciascun asse. Ad esempio, potresti creare un effetto di vento che influenzerebbe solo oggetti a bassa massa. Potresti anche creare un effetto di gravità lunare impostando l'asse y a un valore inferiore.

Ma la gravità predefinita è realistica e funziona bene per il nostro gioco, quindi la manterremo e influenzeremo la gravità del nostro cubo con la prop gravityScale su RigidBody:

// ...

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

Ora il nostro movimento di salto sembra piuttosto convincente!

Aggiungiamo una palla per vedere come interagiscono tra loro:

// ...
export const Experience = () => {
  return (
    <>
      {/* ... */}
      <RigidBody
        colliders={false}
        position-x={3}
        position-y={3}
        gravityScale={0.2}
        restitution={1.2}
        mass={1}
      >
        <Gltf src="/models/ball.glb" castShadow />
        <BallCollider args={[1]} />
      </RigidBody>
      {/* ... */}
    </>
  );
};

Dobbiamo creare manualmente il collider della palla perché, anche se sembra una palla, il modello è più complesso e Rapier non crea automaticamente il collider giusto per esso.

Modifica il livello di restitution per far rimbalzare di più o di meno la palla, e gravityScale per farla cadere più velocemente o più lentamente.

Sembra buono!

È ora di creare il nostro gioco!

Gioco

Per costruire questo gioco, ho preparato una mappa in Blender utilizzando le risorse dal Mini-Game Variety Pack di Kay Lousberg.

Kay-kit mini game variety pack

È un pacchetto incredibile e senza royalties con un sacco di risorse, lo consiglio vivamente!

Area di Gioco

Usiamo il componente Playground nella nostra Experience e rimuoviamo il suolo:

import { Playground } from "./Playground";

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

Nulla di speciale qui, è il codice generato da gltfjsx e il modello 3D, ho solo aggiunto manualmente le proprietà receiveShadow e castShadow ai mesh.

Playground

Abbiamo la nostra area di gioco, ma non abbiamo più il suolo. Dobbiamo avvolgere i nostri playground meshes con un RigidBody:

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

export function Playground(props) {
  // ...
  return (
    <group {...props} dispose={null}>
      <RigidBody type="fixed" name="ground">
        {/* ... */}
      </RigidBody>
    </group>
  );
}

// ...

Ha avvolto tutti i mesh con un RigidBody e ha aggiunto un box collider a ciascuno di essi. Ma poiché abbiamo forme più complesse, useremo invece un trimesh collider:

<RigidBody type="fixed" name="ground" colliders="trimesh">

Trimesh collider

Ora tutti i mesh sono avvolti perfettamente.

Controller in terza persona

Per rendere il nostro gioco giocabile, dobbiamo aggiungere un controller in terza persona al nostro cubo.

Facciamo in modo che la camera segua il nostro giocatore. Mentre muoviamo il suo RigidBody, possiamo inserire la camera all'interno di esso:

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

export const Player = () => {
  // ...
  return (
    <RigidBody
    // ...
    >
      <PerspectiveCamera makeDefault position={[0, 5, 8]} />
      {/* ... */}
    </RigidBody>
  );
};

La sua posizione funziona perfettamente, ma di default, la camera guarda verso l'origine [0, 0, 0] e vogliamo che guardi il cubo.

Dovremo aggiornarla ogni frame. Per farlo, possiamo creare un ref sulla camera e utilizzare il nostro hook useFrame:

// ...

export const Player = () => {
  const camera = useRef();
  const cameraTarget = useRef(new Vector3(0, 0, 0));
  // ...

  useFrame(() => {
    cameraTarget.current.lerp(vec3(rb.current.translation()), 0.5);
    camera.current.lookAt(cameraTarget.current);
    // ...
  });
  return (
    <RigidBody
    // ...
    >
      <PerspectiveCamera makeDefault position={[0, 5, 8]} ref={camera} />
      {/* ... */}
    </RigidBody>
  );
};

Per ottenere la posizione del cubo, dato che è un oggetto Rapier, dobbiamo utilizzare rb.current.translation(). Utilizziamo il metodo vec3 per convertire il vettore Rapier in un vettore three.js.

Utilizziamo lerp e un cameraTarget per spostare dolcemente la camera verso la posizione del cubo. Utilizziamo lookAt per fare in modo che la camera guardi verso il cubo.

Ora la camera segue correttamente il cubo, ma il nostro sistema di movimento non è il più adatto per questo tipo di gioco.

Invece di utilizzare le frecce direzionali su/giù per muoverci sull'asse z, e le frecce sinistra/destra per muoverci sull'asse x, useremo le frecce sinistra/destra per ruotare il nostro giocatore e le frecce su/giù per muoverci avanti/indietro:

// ...
import { euler, quat } from "@react-three/rapier";
// ...

const ROTATION_SPEED = 5;

export const Player = () => {
  // ...
  useFrame(() => {
    cameraTarget.current.lerp(vec3(rb.current.translation()), 0.5);
    camera.current.lookAt(cameraTarget.current);

    const rotVel = {
      x: 0,
      y: 0,
      z: 0,
    };

    const curVel = rb.current.linvel();
    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]) {
      rotVel.y += ROTATION_SPEED;
    }
    if (get()[Controls.right]) {
      rotVel.y -= ROTATION_SPEED;
    }

    rb.current.setAngvel(rotVel, true);
    // applica la rotazione a x e z per andare nella giusta direzione
    const eulerRot = euler().setFromQuaternion(quat(rb.current.rotation()));
    vel.applyEuler(eulerRot);

    if (get()[Controls.jump] && !inTheAir.current) {
      vel.y += JUMP_FORCE;
      inTheAir.current = true;
    } else {
      vel.y = curVel.y;
    }

    rb.current.setLinvel(vel, true);
  });
  // ...
};

Analizziamolo nel dettaglio:

  • Creiamo una variabile rotVel per memorizzare la velocità di rotazione
  • Cambiamo la velocità di rotazione sull'asse y quando l'utente preme la freccia sinistra o destra
  • Applichiamo la velocità di rotazione al cubo con rb.current.setAngvel(rotVel, true)
  • Otteniamo la rotazione corrente del cubo con rb.current.rotation()
  • La convertiamo in angoli euler con euler().setFromQuaternion
  • Applichiamo la rotazione alla velocità con applyEuler per convertirla nella direzione corretta

Respawn

Attualmente, se cadiamo dal playground, continuiamo a cadere all'infinito. Dobbiamo aggiungere un sistema di respawn.

Quello che faremo è aggiungere degli RigidBodies molto grandi sotto il playground. Quando il giocatore entrerà in contatto con essi, verrà teletrasportato al punto di spawn:

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

export const Experience = () => {
  return (
    <>
      {/* ... */}
      <RigidBody
        type="fixed"
        colliders={false}
        sensor
        name="space"
        position-y={-5}
      >
        <CuboidCollider args={[50, 0.5, 50]} />
      </RigidBody>
      {/* ... */}
    </>
  );
};

Abbiamo chiamato il nostro RigidBody space e lo abbiamo impostato come sensor. Un sensor non ha impatto sul mondo fisico, viene utilizzato solo per rilevare le collisioni.

Ora, nel nostro componente Player, possiamo utilizzare la prop onIntersectionEnter sul RigidBody e chiamare una funzione respawn:

// ...

export const Player = () => {
  // ...

  const respawn = () => {
    rb.current.setTranslation({
      x: 0,
      y: 5,
      z: 0,
    });
  };
  return (
    <RigidBody
      // ...
      onIntersectionEnter={({ other }) => {
        if (other.rigidBodyObject.name === "space") {
          respawn();
        }
      }}
    >
      {/* ... */}
    </RigidBody>
  );
};

Usiamo il metodo setTranslation per teletrasportare il cubo al punto di spawn.

onIntersectionEnter è l'equivalente di collisione di onCollisionEnter per i sensori.

Pronto a saltare nel vuoto?

Il nostro sistema di respawn funziona! Sta cominciando ad assomigliare a un gioco.

Swiper

Questo simpatico swiper è pensato per farci uscire dal parco giochi!

Swiper

Poiché dobbiamo applicare una forza per animarlo, lo sposteremo nel suo RigidBody:

import React, { useRef } from "react";

export function Playground(props) {
  // ...
  const swiper = useRef();
  return (
    <group {...props} dispose={null}>
      <RigidBody
        type="kinematicVelocity"
        colliders={"trimesh"}
        ref={swiper}
        restitution={3}
        name="swiper"
      >
        <group
          name="swiperDouble_teamRed"
          rotation-y={Math.PI / 4}
          position={[0.002, -0.106, -21.65]}
        >
          {/* ... */}
        </group>
      </RigidBody>
      {/* ... */}
    </group>
  );
}

Il tipo kinematicVelocity è un tipo speciale di RigidBody che può essere mosso con il metodo setLinvel e setAngvel ma non sarà influenzato da forze esterne. (Impedisce al nostro giocatore di muovere lo swiper)

Definiamo la velocità angolare dello swiper nel nostro componente Experience:

import React, { useEffect, useRef } from "react";

export function Playground(props) {
  // ...
  useEffect(() => {
    swiper.current.setAngvel({ x: 0, y: 3, z: 0 }, true);
  });
  // ...
}

Se ti chiedi perché usiamo useEffect invece di useFrame, è perché la velocità è costante, finché non la cambiamo, continuerà a ruotare.

Ruota! Sentiti libero di regolare il valore di y per cambiare la velocità di rotazione.

L'effetto quando veniamo espulsi dal parco giochi non è naturale. Questo è uno svantaggio dell'utilizzo di setLinvel sul nostro player invece di applyImpulse ma ha anche semplificato molte cose.

Quello che vediamo è: Il cubo viene colpito e proiettato, e si ferma immediatamente perché il nostro setLinvel lo annulla.

Un rapido rimedio è disabilitare il nostro setLinvel quando veniamo colpiti per un breve periodo di tempo:

// ...
export const Player = () => {
  // ...
  const punched = useRef(false);

  useFrame(() => {
    // ...

    if (!punched.current) {
      rb.current.setLinvel(vel, true);
    }
  });

  // ...
  return (
    <RigidBody
      // ...
      onCollisionEnter={({ other }) => {
        if (other.rigidBodyObject.name === "ground") {
          inTheAir.current = false;
        }
        if (other.rigidBodyObject.name === "swiper") {
          punched.current = true;
          setTimeout(() => {
            punched.current = false;
          }, 200);
        }
      }}
      // ...
    >
      {/* ... */}
    </RigidBody>
  );
};

L'effetto ora è molto meglio!

Cancelli

Concludiamo il nostro gioco teletrasportando il giocatore al traguardo quando entra nei cancelli.

Iniziamo separando i cancelli dal rigidbody principale del campo di gioco e creiamo un nuovo rigidbody sensor con un collider personalizzato:

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

export function Playground(props) {
  // ...
  return (
    <group {...props} dispose={null}>
      {/* ... */}
      <RigidBody
        type="fixed"
        name="gateIn"
        sensor
        colliders={false}
        position={[-20.325, -0.249, -28.42]}
      >
        <mesh
          receiveShadow
          castShadow
          name="gateLargeWide_teamBlue"
          geometry={nodes.gateLargeWide_teamBlue.geometry}
          material={materials["Blue.020"]}
          rotation={[0, 1.571, 0]}
        />
        <CuboidCollider position={[-1, 0, 0]} args={[0.5, 2, 1.5]} />
      </RigidBody>
      {/* ... */}
    </group>
  );
}

Gate collider

Possiamo vedere la zona in cui verrà rilevata la collisione.

Ora nel nostro codice Player possiamo gestire questo scenario:

// ...
import { useThree } from "@react-three/fiber";

export const Player = () => {
  // ...

  const respawn = () => {
    rb.current.setTranslation({
      x: 0,
      y: 5,
      z: 0,
    });
  };

  const scene = useThree((state) => state.scene);
  const teleport = () => {
    const gateOut = scene.getObjectByName("gateLargeWide_teamYellow");
    rb.current.setTranslation(gateOut.position);
  };

  return (
    <RigidBody
      // ...
      onIntersectionEnter={({ other }) => {
        if (other.rigidBodyObject.name === "space") {
          respawn();
        }
        if (other.rigidBodyObject.name === "gateIn") {
          teleport();
        }
      }}
    >
      {/* ... */}
    </RigidBody>
  );
};

Utilizziamo getObjectByName per ottenere la posizione del cancello e teletrasportare il giocatore ad essa.

Il nostro sistema di cancelli funziona!

Ombre

Potresti aver notato che le nostre ombre non funzionano correttamente. Il nostro playground è troppo grande per le impostazioni di ombra predefinite.

Ombre tagliate

Le ombre sono tagliate.

Per risolvere il problema, dobbiamo regolare le impostazioni della shadow camera:

// ...
import { useHelper } from "@react-three/drei";
import { useRef } from "react";
import * as THREE from "three";

export const Experience = () => {
  const shadowCameraRef = useRef();
  useHelper(shadowCameraRef, THREE.CameraHelper);

  return (
    <>
      <directionalLight
        position={[-50, 50, 25]}
        intensity={0.4}
        castShadow
        shadow-mapSize-width={1024}
        shadow-mapSize-height={1024}
      >
        <PerspectiveCamera
          ref={shadowCameraRef}
          attach={"shadow-camera"}
          near={55}
          far={86}
          fov={80}
        />
      </directionalLight>
      <directionalLight position={[10, 10, 5]} intensity={0.2} />
      {/* ... */}
    </>
  );
};

Sarebbe impossibile trovare i valori giusti senza il CameraHelper.

Per trovarli, è necessario avere l'intera scena tra i piani near e far disegnati dal helper.

Consulta la lezione sulle ombre se hai bisogno di un ripasso su come funzionano le ombre.

Ombre helper

Ora le nostre ombre funzionano correttamente...

...e il nostro gioco è finito! 🎉

Conclusione

Ora hai le basi per creare le tue simulazioni fisiche e i tuoi giochi con React Three Fiber!

Non fermarti qui, ci sono molte cose che puoi fare per migliorare questo gioco e renderlo più divertente:

  • Aggiungi un timer e un sistema di migliore punteggio
  • Crea altri livelli usando gli asset del pacchetto
  • Crea una bella animazione quando il giocatore preme il pulsante finale
  • Aggiungi altri ostacoli
  • Aggiungi NPC

Esplora altri motori fisici e librerie per trovare quello che meglio si adatta alle tue esigenze.

Ho fatto tutorial sul gioco sul mio canale YouTube usando React Three Fiber. Puoi dare un'occhiata se vuoi saperne di più su fisica, giochi e multiplayer.

Three.js logoReact logo

React Three Fiber: The Ultimate Guide to 3D Web Development

✨ You have reached the end of the preview ✨

Go to the next level with Three.js and React Three Fiber!

Get full access to this lesson and the complete course when you enroll:

  • 🔓 Full lesson videos with no limits
  • 💻 Access to the final source code
  • 🎓 Course progress tracking & completion
  • 💬 Invite to our private Discord community
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.