Fisika
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> {/* ... */} </> ); };
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 objeksetLinVel
: 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.
Jenis-jenis collider yang berbeda adalah:
box
: collider boxball
: collider spherehull
: mirip bungkus hadiah di sekitar meshtrimesh
: 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.
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 propertireceiveShadow
dancastShadow
ke mesh yang digunakan.
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">
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 darionCollisionEnter
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!
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
daripadauseFrame
, 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> ); }
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.
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.
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.
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
One-time payment. Lifetime updates included.