Fisika

Starter pack

Fisika membuka dunia kemungkinan baru untuk proyek 3D Anda. Anda dapat menciptakan alam semesta yang realistis, interaksi pengguna, dan bahkan permainan.

Dalam pelajaran ini, kita akan menemukan konsep-konsep penting sambil membangun permainan sederhana.

Jangan khawatir, tidak diperlukan pengetahuan sebelumnya, kita akan mulai dari awal. (Untuk memberi tahu Anda, saya adalah murid yang sangat buruk dalam fisika di sekolah, jadi jika saya bisa melakukannya, Anda juga bisa!)

Mesin fisika

Untuk menambahkan fisika ke proyek 3D kita, kita akan menggunakan mesin fisika. Sebuah mesin fisika adalah perpustakaan yang akan menangani semua matematika kompleks untuk kita, seperti gravitasi, tabrakan, gaya, dll.

Dalam ekosistem JavaScript, ada banyak mesin fisika yang tersedia.

Dua yang sangat populer adalah Cannon.js dan Rapier.js.

Poimandres (lagi) telah membuat dua pustaka hebat untuk menggunakan mesin-mesin ini dengan React Three Fiber: react-three-rapier dan use-cannon.

Dalam pelajaran ini, kita akan menggunakan react-three-rapier tetapi mereka cukup mirip dan konsep yang kita pelajari di sini dapat diterapkan pada keduanya.

Untuk menginstalnya, jalankan:

yarn add @react-three/rapier

Sekarang kita siap untuk memulai!

Dunia Fisika

Sebelum membuat permainan, mari kita bahas konsep-konsep dasar.

Pertama, kita perlu membuat dunia fisika. Dunia ini akan berisi semua objek fisika dari adegan kita. Dengan react-three-rapier, kita hanya perlu membungkus semua objek kita dengan komponen <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;

Dunia kita sekarang sudah siap tetapi tidak ada yang terjadi! Itu karena kita belum memiliki objek fisika.

Rigidbody

Untuk menambahkan fisika ke sebuah objek, kita perlu menambahkan rigidbody. Rigidbody adalah komponen yang akan membuat objek kita bergerak dalam dunia fisika.

Apa yang dapat memicu pergerakan suatu objek? Gaya, seperti gravitasi, tabrakan, atau interaksi pengguna.

Mari beri tahu dunia fisika kita bahwa kubus kita yang berada di Player.jsx adalah sebuah objek fisika dengan menambahkan rigidbody ke dalamnya:

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

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

Sekarang kubus kita merespons gravitasi dan jatuh. Tapi itu jatuh selamanya!

Kita perlu membuat tanah menjadi objek fisika juga agar kubus dapat menabraknya dan berhenti jatuh.

Mari tambahkan rigidbody ke tanah di Experience.jsx, tetapi karena kita tidak ingin tanah bergerak dan jatuh seperti kubus, kita akan menambahkan 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

Kembali ke titik awal, kita memiliki kubus yang tidak bergerak di atas tanah. Tetapi, di balik layar, kita memiliki kubus yang bereaksi terhadap gravitasi dan berhenti karena tabrakannya dengan tanah.

Gaya

Sekarang kita memiliki dunia fisika dan objek fisika, kita bisa mulai bermain dengan gaya.

Kita akan membuat kubus bergerak dengan tombol panah pada keyboard. Untuk melakukannya, mari gunakan KeyboardControls yang kita temukan dalam pelajaran events:

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

Kita sekarang bisa mendapatkan tombol yang ditekan dalam komponen Player.jsx kita:

// ...
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() adalah cara alternatif untuk mendapatkan tombol yang ditekan dengan komponen KeyboardControls.

Sekarang kita memiliki tombol yang ditekan, kita bisa menerapkan gaya pada kubus. Kita bisa melakukannya dengan dua metode:

  • applyImpulse: menerapkan gaya seketika pada objek
  • setLinVel: menetapkan kecepatan linier pada objek

Mari kita temukan keduanya.

Mari tambahkan useRef ke RigidBody, dan gunakan untuk menerapkan impuls agar kubus bergerak ke arah yang benar:

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

Pastikan untuk menetapkan ref ke RigidBody dan bukan mesh.

Ini bekerja, tetapi akselerasinya terlalu cepat dan meluncur di tanah. Kita dapat menambahkan lebih banyak gesekan ke tanah untuk memperbaiki itu:

// ...

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

Gesekan membuat kubus berputar karena lebih mencengkeram tanah. Kita dapat memperbaikinya dengan mengunci rotasi kubus:

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

Ini sekarang jauh lebih baik, tetapi kubus masih sedikit meluncur. Kita dapat memperbaikinya dengan mengatur linear damping pada kubus, namun kita tidak akan mengejar jalur ini dalam pelajaran ini.

Karena kita juga perlu menyesuaikan kecepatan maksimum kubus untuk mencegahnya terus menerus berakselerasi. Kita akan menghadapi masalah ketika kita akan menggunakan tombol kiri dan kanan untuk memutar kubus daripada untuk menggerakkannya.

Mari kita ubah sistem kita untuk menggunakan setLinVel daripada 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>;
};

Anda juga dapat menghapus gesekan dari tanah karena tidak diperlukan lagi.

F2 adalah temanmu untuk mengganti nama variabel dengan cepat.

Bagus! Sekarang kita memiliki kubus yang dapat bergerak dengan tombol panah pada keyboard.

Mari tambahkan gaya lompat ketika pengguna menekan tombol spasi:

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

Kita memiliki dua masalah:

  • Kubus tidak bereaksi dengan benar terhadap gravitasi (bandingkan dengan kubus yang jatuh di awal pelajaran)
  • Kubus dapat melompat ketika sudah berada di udara

Masalah gravitasi terjadi karena kita menetapkan kecepatan kubus secara manual pada sumbu y. Kita perlu mengubahnya hanya ketika kita melompat, dan membiarkan mesin fisika menangani gravitasi di sisa waktu:

// ...

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

Kita mendapatkan kecepatan saat ini dari kubus dengan rb.current.linvel() dan jika kita tidak melompat, kita menetapkan kecepatan y ke kecepatan saat ini.

Untuk mencegah kubus melompat ketika sudah berada di udara, kita dapat memeriksa apakah kubus sudah menyentuh tanah sebelum dapat melompat lagi:

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

Sekarang kita bisa melompat hanya sekali, dan gravitasi bekerja tetapi agak terlalu lambat.

Sebelum memperbaikinya, mari kita lihat bagaimana sistem tabrakan kita bekerja.

Collider

Collider bertanggung jawab untuk mendeteksi tabrakan antara objek. Mereka dilampirkan pada komponen RigidBody.

Rapier secara otomatis menambahkan collider ke komponen RigidBody berdasarkan geometris mesh tetapi kita juga dapat menambahkannya secara manual.

Untuk memvisualisasikan collider, kita dapat menggunakan prop debug pada komponen 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;

Karena kita menggunakan geometris box untuk kubus dan tanah, collider saat ini membungkus mereka dengan sempurna dan kita hampir tidak dapat melihat warna garis luarnya dari mode debug.

Mari kita ubah collider kubus kita menjadi collider sphere:

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

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

Cara ini semi otomatis, kita memberi tahu rapier jenis collider apa yang kita inginkan dan secara otomatis akan membuatnya berdasarkan ukuran mesh.

Kita juga dapat menambahkan collider secara manual dan menyesuaikannya:

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

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

Kita atur colliders ke false untuk mencegah rapier membuat collider secara otomatis.

Ball collider membungkus kubus kita

Jenis-jenis collider yang berbeda adalah:

  • box: collider box
  • ball: collider sphere
  • hull: mirip bungkus hadiah di sekitar mesh
  • trimesh: collider yang akan membungkus mesh dengan sempurna

Selalu gunakan collider yang paling sederhana untuk meningkatkan performa.

Sekarang setelah kita mengetahui collider kubus dan tanah, mari deteksi tabrakan antara mereka untuk mengetahui kapan kubus berada di tanah.

Mari kita hapus ball collider dari kubus dan tambahkan prop onCollisionEnter pada RigidBody:

// ...

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

Kita dapat mengakses collider lainnya dengan other.rigidBodyObject dan memeriksa namanya untuk mengetahui apakah itu tanah.

Kita perlu menambahkan nama ke collider tanah:

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

Kita sekarang bisa melompat lagi saat menyentuh tanah.

Gravitasi

Gravitasi ditemukan oleh Isaac Newton pada tahun 1687... 🍎

Yah, saya bercanda, kita tahu apa itu gravitasi.

Kita memiliki dua opsi untuk mengubah gravitasi, kita bisa mengubahnya secara global dengan prop gravity pada komponen Physics:

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

Ini mengambil array dari tiga angka, masing-masing untuk setiap sumbu. Misalnya, Anda bisa membuat efek angin yang hanya akan mempengaruhi objek bermassa rendah. Anda juga bisa menciptakan efek gravitasi bulan dengan mengatur sumbu y ke nilai yang lebih rendah.

Namun gravitasi bawaan sudah realistis dan bekerja dengan baik untuk permainan kita, jadi kita akan mempertahankannya dan mempengaruhi gravitasi kubus kita dengan prop gravityScale pada RigidBody:

// ...

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

Sekarang gerakan lompat kita terlihat cukup meyakinkan!

Mari tambahkan bola untuk melihat bagaimana mereka saling berinteraksi:

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

Kita perlu membuat ball collider secara manual karena meskipun terlihat seperti bola, modelnya lebih kompleks dan Rapier tidak secara otomatis membuat collider yang tepat untuknya.

Sesuaikan tingkat restitution untuk membuat bola memantul lebih banyak atau sedikit, dan gravityScale untuk membuatnya jatuh lebih cepat atau lebih lambat.

Terlihat bagus!

Saatnya membuat permainan kita!

Game

Untuk membangun game ini, saya menyiapkan map di Blender menggunakan aset dari Mini-Game Variety Pack dari Kay Lousberg.

Kay-kit mini game variety pack

Ini adalah paket luar biasa bebas royalti dengan banyak aset, sangat saya rekomendasikan!

Playground

Mari kita gunakan komponen Playground dalam Experience kita, dan hapus ground-nya:

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

Tidak ada yang spesial di sini, ini adalah kode yang dihasilkan dari gltfjsx dan 3D model, saya hanya secara manual menambahkan properti receiveShadow dan castShadow ke mesh yang digunakan.

Playground

Kita memiliki playground kita, tetapi kita tidak memiliki ground lagi. Kita perlu membungkus playground meshes kita dengan RigidBody:

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

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

// ...

Ini membungkus setiap mesh dengan RigidBody dan menambahkan box collider ke masing-masing. Namun karena kita memiliki bentuk yang lebih kompleks, kita akan menggunakan trimesh collider sebagai gantinya:

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

Trimesh collider

Sekarang setiap mesh dibungkus dengan sempurna.

Kontroler orang ketiga

Untuk membuat game kita dapat dimainkan, kita perlu menambahkan kontroler orang ketiga ke kubus kita.

Mari kita buat kamera mengikuti pemain kita. Saat kita memindahkan RigidBody-nya, kita bisa memasukkan kamera ke dalamnya:

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

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

Posisinya bekerja dengan sempurna, tetapi secara default, kamera menghadap ke asal [0, 0, 0] dan kita ingin menghadap ke kubus.

Kita perlu memperbaruinya setiap frame. Untuk melakukannya, kita bisa membuat ref pada kamera dan menggunakan hook useFrame kita:

// ...

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

Untuk mendapatkan posisi kubus, karena itu adalah objek Rapier, kita perlu menggunakan rb.current.translation(). Kita menggunakan metode vec3 untuk mengonversi vektor Rapier menjadi vektor three.js.

Kita menggunakan lerp dan cameraTarget untuk menggerakkan kamera secara lembut ke posisi kubus. Kita menggunakan lookAt untuk membuat kamera melihat ke kubus.

Sekarang kamera mengikuti kubus dengan benar, tetapi sistem pergerakan kita tidak paling cocok untuk jenis permainan ini.

Alih-alih menggunakan panah atas/bawah untuk bergerak di sumbu z, dan panah kiri/kanan untuk bergerak di sumbu x, kita akan menggunakan panah kiri/kanan untuk memutar pemain kita dan panah atas/bawah untuk bergerak maju/mundur:

// ...
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);
    // terapkan rotasi ke x dan z untuk pergi ke arah yang benar
    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);
  });
  // ...
};

Mari kita jelaskan secara rinci:

  • Kita membuat variabel rotVel untuk menyimpan kecepatan rotasi
  • Kita mengubah kecepatan rotasi pada sumbu y saat pengguna menekan panah kiri atau kanan
  • Kita menerapkan kecepatan rotasi pada kubus dengan rb.current.setAngvel(rotVel, true)
  • Kita mendapatkan rotasi saat ini dari kubus dengan rb.current.rotation()
  • Kita mengonversinya menjadi sudut euler dengan euler().setFromQuaternion
  • Kita menerapkan rotasi pada kecepatan dengan applyEuler untuk mengonversinya ke arah yang benar

Respawn

Saat ini, jika kita jatuh dari playground, kita akan jatuh selamanya. Kita perlu menambahkan sistem respawn.

Yang akan kita lakukan adalah menambahkan RigidBodies yang sangat besar di bawah playground. Ketika pemain akan bersentuhan dengannya, kita akan memindahkannya ke titik 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>
      {/* ... */}
    </>
  );
};

Kita menamai RigidBody kita space dan kita mengaturnya sebagai sensor. Sensor tidak berdampak pada dunia fisik, hanya digunakan untuk mendeteksi tabrakan.

Sekarang pada komponen Player kita, kita dapat menambahkan prop onIntersectionEnter pada RigidBody dan memanggil fungsi 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>
  );
};

Kita menggunakan metode setTranslation untuk memindahkan cube ke titik spawn.

onIntersectionEnter adalah ekuivalen tabrakan dari onCollisionEnter untuk sensor.

Siap untuk melompat ke dalam kehampaan?

Sistem respawn kita berfungsi! Mulai terlihat seperti permainan.

Swiper

Swiper yang menarik ini dimaksudkan untuk mengeluarkan kita dari taman bermain!

Swiper

Karena kita perlu menerapkan gaya untuk menganimasi, kita akan memindahkannya ke dalam RigidBody sendiri:

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

Tipe kinematicVelocity adalah jenis khusus dari RigidBody yang dapat digerakkan dengan metode setLinvel dan setAngvel tetapi tidak akan dipengaruhi oleh gaya eksternal. (Ini mencegah pemain kita memindahkan swiper)

Mari kita definisikan kecepatan sudut dari swiper dalam komponen Experience kita:

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

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

Jika Anda bertanya-tanya mengapa kita menggunakan useEffect daripada useFrame, itu karena kecepatan adalah konstan, selama kita tidak mengubahnya, itu akan terus berputar.

Ia berputar! Jangan ragu untuk menyesuaikan nilai y untuk mengubah kecepatan rotasi.

Efek ketika kita dikeluarkan dari taman bermain tidak alami. Ini adalah kelemahan menggunakan setLinvel pada pemain kita alih-alih applyImpulse tetapi juga menyederhanakan banyak hal.

Yang kita lihat adalah: Kubus ditendang dan diproyeksikan, dan berhenti seketika karena setLinvel kita membatalkannya.

Cara cepat untuk mengatasinya adalah dengan menonaktifkan setLinvel kita ketika kita ditendang untuk jangka waktu singkat:

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

Efeknya sekarang jauh lebih baik!

Gerbang

Mari kita selesaikan permainan kita dengan mentransportasi pemain ke garis finish ketika memasuki gerbang.

Kita mulai dengan memisahkan gerbang dari rigidbody utama playground, dan membuat rigidbody sensor baru dengan collider khusus:

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

Kita bisa melihat zona di mana tabrakan akan terdeteksi.

Sekarang dalam kode Player kita bisa menangani skenario ini:

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

Kita menggunakan getObjectByName untuk mendapatkan posisi gerbang dan mentransportasikan pemain ke sana.

Sistem gerbang kita berhasil!

Shadows

Anda mungkin memperhatikan bahwa shadows kita tidak bekerja dengan baik. Playground kita terlalu besar untuk pengaturan shadow default.

Shadows cut

Bayangan terpotong.

Untuk memperbaikinya, kita perlu menyesuaikan pengaturan 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} />
      {/* ... */}
    </>
  );
};

Tidak mungkin menemukan nilai yang tepat tanpa CameraHelper.

Untuk menemukannya, Anda perlu memastikan seluruh scene berada di antara near dan far planes yang digambar oleh helper.

Rujuk ke pelajaran shadows jika Anda memerlukan penyegaran tentang bagaimana shadows bekerja.

Shadows helper

Bayangan kita sekarang bekerja dengan baik...

...dan permainan kita selesai! 🎉

Kesimpulan

Anda sekarang memiliki dasar-dasar untuk membuat simulasi fisika dan permainan Anda sendiri dengan React Three Fiber!

Jangan berhenti di sini, ada banyak hal yang bisa Anda lakukan untuk meningkatkan permainan ini dan membuatnya lebih menyenangkan:

  • Tambahkan sistem timer dan skor terbaik
  • Ciptakan level lain menggunakan pack assets
  • Ciptakan animasi keren saat pemain menekan tombol akhir
  • Tambahkan rintangan lain
  • Tambahkan NPC

Jelajahi engine fisika dan perpustakaan lainnya untuk menemukan yang paling cocok dengan kebutuhan Anda.

Saya membuat tutorial game di saluran YouTube saya menggunakan React Three Fiber. Anda dapat memeriksanya jika ingin belajar lebih lanjut tentang fisika, permainan, dan 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.