Physics
물리는 3D 프로젝트에 새로운 가능성을 열어줍니다. 현실적인 우주, 사용자 상호작용, 심지어 게임도 만들 수 있습니다.
이 강의에서는 간단한 게임을 만들면서 필수 개념을 알아보겠습니다.
걱정하지 마세요. 사전 지식은 필요 없습니다. 기초부터 시작할 것입니다. (참고로 저는 학교에서 물리를 매우 잘 못했기 때문에 제가 할 수 있다면, 여러분도 할 수 있습니다!)
물리 엔진
3D 프로젝트에 물리를 추가하기 위해, 우리는 물리 엔진을 사용할 것입니다. 물리 엔진은 중력, 충돌, 힘 등의 복잡한 수학을 처리해주는 라이브러리입니다.
자바스크립트 생태계에서는 많은 물리 엔진을 사용할 수 있습니다.
두 가지 매우 인기 있는 엔진은 Cannon.js와 Rapier.js입니다.
Poimandres는 React Three Fiber에서 이러한 엔진을 사용할 수 있도록 두 개의 훌륭한 라이브러리인 react-three-rapier와 use-cannon을 만들었습니다.
이번 강의에서는 react-three-rapier를 사용할 것이지만, 두 라이브러리는 매우 유사하며 여기서 배울 개념들은 두 라이브러리 모두에 적용할 수 있습니다.
설치를 위해, 다음을 실행하세요:
yarn add @react-three/rapier
이제 시작할 준비가 되었습니다!
Physics World
게임을 만들기 전에 필수 개념을 다루어보겠습니다.
먼저, physics world를 만들어야 합니다. 이 세계는 우리의 장면에 있는 모든 물리 객체를 포함할 것입니다. react-three-rapier를 사용하면, 모든 객체를 <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;
우리의 세계는 이제 준비되었지만 아무 일도 일어나지 않습니다! 그것은 우리가 아직 물리 객체를 만들지 않았기 때문입니다.
Rigidbody
객체에 물리학을 추가하려면 **리짓바디(rigidbody)**를 추가해야 합니다. 리짓바디는 객체가 물리 세계에서 움직이도록 하는 구성 요소입니다.
객체의 움직임을 유발할 수 있는 것은 무엇일까요? 중력, 충돌, 또는 사용자 상호작용과 같은 힘입니다.
Player.jsx
에 위치한 큐브를 **리짓바디(rigidbody)**를 추가하여 _물리 세계_에 있는 물리 객체라고 알려줍시다:
import { RigidBody } from "@react-three/rapier"; export const Player = () => { return <RigidBody>{/* ... */}</RigidBody>; };
이제 우리 큐브는 중력에 반응하여 아래로 떨어집니다. 하지만 계속 떨어지고 있습니다!
큐브가 충돌하여 멈출 수 있도록 바닥도 물리 객체로 만들어야 합니다.
Experience.jsx
의 땅에 리짓바디를 추가해 봅시다. 큐브처럼 움직이지 않고 떨어지지 않도록 하기 위해 type="fixed"
prop을 추가합니다:
// ... 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> {/* ... */} </> ); };
다시 출발점으로 돌아와서, 이제 바닥 위에 움직이지 않는 큐브가 있습니다. 하지만, 내부적으로는 큐브가 중력에 반응하며 땅과의 충돌로 인해 멈추고 있습니다.
Forces
이제 물리 세계와 물체가 준비되었으니, 힘을 가지고 놀아봅시다.
이제 우리는 키보드 화살표로 큐브를 움직이게 만들 것입니다. 그렇게 하기 위해서는 이벤트 강의에서 발견한 KeyboardControls를 사용합시다:
// ... 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;
이제 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()은 KeyboardControls 컴포넌트를 통해 눌린 키를 가져오는 대체 방법입니다.
이제 눌린 키를 가지고 있으므로 큐브에 힘을 적용할 수 있습니다. 두 가지 방법으로 할 수 있습니다:
applyImpulse
: 물체에 즉각적인 힘을 적용setLinVel
: 물체의 선형 속도를 설정
두 가지 방법을 알아봅시다.
RigidBody에 useRef
를 추가하고, 올바른 방향으로 큐브를 움직이기 위해 이를 사용하여 임펄스를 적용합니다:
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>; };
ref를 RigidBody에 할당하고 mesh에 할당하지 않도록 주의하세요.
작동합니다, 그러나 너무 빠르게 가속되고 지면에서 미끄러집니다. 이것을 해결하기 위해 지면에 더 많은 마찰을 추가할 수 있습니다:
// ... export const Experience = () => { // ... return ( <> {/* ... */} <RigidBody type="fixed" friction={5}> {/* ... */} </RigidBody> {/* ... */} </> ); };
마찰은 큐브가 지면에 붙어서 회전하게 만듭니다. 이를 해결하기 위해 큐브의 회전을 고정할 수 있습니다:
// ... export const Player = () => { // ... return ( <RigidBody ref={rb} lockRotations> {/* ... */} </RigidBody> ); };
이제 더 나아졌지만, 큐브는 여전히 약간 미끄러지고 있습니다. 우리는 이 강의에서는 이 문제를 해결하지 않을 것입니다.
왜냐하면 큐브가 계속해서 가속하는 것을 방지하기 위해 최대 속도를 조정해야 할 뿐만 아니라 앞으로 좌우 키를 사용하여 큐브를 회전시키는 것에도 문제가 생기기 때문입니다.
이제 applyImpulse
대신 setLinVel
을 사용하는 시스템으로 바꿔봅시다:
// ... 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>; };
지면에서 마찰을 제거해도 됩니다. 이제 필요하지 않기 때문입니다.
F2
는 변수를 빠르게 이름을 변경할 수 있어서 편리합니다.
좋습니다! 이제 우리는 키보드 화살표를 사용하여 움직일 수 있는 큐브를 만들었습니다.
사용자가 스페이스 바를 누르면 점프 힘을 추가합시다:
// ... 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> ); };
두 가지 문제가 있습니다:
- 큐브가 중력에 적절하게 반응하지 않습니다 (강의 시작 부분의 떨어지는 큐브와 비교하세요)
- 큐브가 이미 공중에 있는 상태에서 점프할 수 있습니다
중력 문제는 우리가 큐브의 y 축 속도를 수동으로 설정했기 때문입니다. 우리는 점프할 때만 속도를 변경하고, 나머지 시간 동안에는 물리 엔진이 중력을 처리하도록 해야 합니다:
// ... 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); }); // ... };
우리는 rb.current.linvel()
을 사용하여 현재 큐브의 속도를 얻으며, 점프하지 않을 때는 y
속도를 현재 속도로 설정합니다.
큐브가 이미 공중에 있을 때 점프하지 않도록 하려면, 큐브가 바닥에 닿았을 때 다음 점프가 가능하도록 해야 합니다:
// ... 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); }); // ... };
이제 한 번만 점프할 수 있으며, 중력은 작동하지만 조금 느립니다.
이 문제를 해결하기 전에, 우리의 충돌 시스템이 어떻게 작동하는지 살펴봅시다.
충돌체
충돌체는 객체 간의 충돌을 감지하는 역할을 합니다. 이들은 RigidBody 구성 요소에 부착됩니다.
Rapier는 메시 기하학을 기반으로 RigidBody 구성 요소에 충돌체를 자동으로 추가하지만, 수동으로 추가할 수도 있습니다.
충돌체를 시각화하려면 Physics 구성 요소의 debug
속성을 사용하면 됩니다:
// ... 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;
큐브와 지면에 박스 기하학을 사용하기 때문에 현재 충돌체는 이들을 완벽히 감싸고 있으며, 디버그 모드에서의 테두리 색상을 거의 볼 수 없습니다.
우리의 큐브 충돌체를 sphere 충돌체로 변경해봅시다:
import { vec3 } from "@react-three/rapier"; // ... export const Player = () => { // ... return ( <RigidBody ref={rb} lockRotations colliders={"ball"}> {/* ... */} </RigidBody> ); };
이 방법은 반자동 방식으로, 어떤 종류의 충돌체를 원하는지 rapier에 알려주면 메시 크기를 바탕으로 자동으로 생성됩니다.
충돌체를 수동으로 추가하고 조정할 수도 있습니다:
import { BallCollider } from "@react-three/rapier"; // ... export const Player = () => { // ... return ( <RigidBody ref={rb} lockRotations colliders={false}> {/* ... */} <BallCollider args={[1.5]} /> </RigidBody> ); };
자동으로 충돌체가 생성되지 않도록 colliders
에 false
를 설정합니다.
다양한 종류의 충돌체는 다음과 같습니다:
box
: 박스 충돌체ball
: 구형 충돌체hull
: 메시를 감싸는 포장지 같은 역할trimesh
: 메시를 완벽히 감싸는 충돌체
성능을 개선하려면 항상 가장 단순한 충돌체를 사용하세요.
이제 큐브와 지면 충돌체를 알았으니 큐브가 지면에 있는지 알기 위해 이들 간의 충돌을 감지해봅시다.
큐브에서 ball collider를 제거하고 RigidBody에 onCollisionEnter
속성을 추가해봅시다:
// ... export const Player = () => { // ... return ( <RigidBody {/* ... */} onCollisionEnter={({ other }) => { if (other.rigidBodyObject.name === "ground") { inTheAir.current = false; } }} > {/* ... */} </RigidBody> ); };
other.rigidBodyObject
를 통해 다른 충돌체에 접근할 수 있으며, 이름을 확인해 지면인지 알 수 있습니다.
지면 충돌체에 이름을 추가해야 합니다:
// ... export const Experience = () => { return ( <> {/* ... */} <RigidBody type="fixed" name="ground"> {/* ... */} </RigidBody> {/* ... */} </> ); };
이제 지면에 닿으면 다시 점프할 수 있습니다.
중력
중력은 1687년 아이작 뉴턴에 의해 발견되었습니다... 🍎
농담입니다. 중력이 뭔지 우리는 잘 알고 있죠.
중력을 변경하는 두 가지 방법이 있습니다. Physics 컴포넌트에서 gravity
속성을 사용하여 전역적으로 변경할 수 있습니다:
<Physics debug gravity={[0, -50, 0]}>
각 축에 대해 세 개의 숫자 배열을 사용합니다. 예를 들어, 낮은 질량의 객체에만 영향을 미치는 바람 효과를 만들 수 있습니다. 또는 y 축 값을 낮춰 달의 중력 효과를 만들 수도 있습니다.
그러나 기본 중력은 현실적이며 우리 게임에 잘 맞으므로 그대로 두고, gravityScale
속성을 RigidBody에 사용하여 큐브의 중력에 영향을 미치겠습니다:
// ... export const Player = () => { // ... return ( <RigidBody // ... gravityScale={2.5} > {/* ... */} </RigidBody> ); };
이제 점프 움직임이 꽤 설득력 있어 보이네요!
공 추가하여 서로 어떻게 상호작용하는지 살펴봅시다:
// ... 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> {/* ... */} </> ); };
모델이 공처럼 보이더라도 모델은 더 복잡하기 때문에 적절한 콜라이더를 자동으로 생성하지 않으므로, 공 콜라이더는 수동으로 생성해야 합니다.
restitution
의 수준을 조정하여 공이 더 많이 또는 덜 튀게 하고, gravityScale
을 조정하여 더 빠르게 또는 느리게 떨어지도록 합니다.
좋아 보입니다!
이제 게임을 만들어볼 시간입니다!
게임
이 게임을 만들기 위해, 저는 Kay Lousberg의 Mini-Game Variety Pack의 에셋을 사용하여 Blender에서 맵을 준비했습니다.
이것은 많은 에셋이 포함된 놀라운 로열티 프리 팩이며, 강력히 추천합니다!
놀이터
우리의 Experience
에서 Playground
컴포넌트를 사용하고, ground를 제거해 보겠습니다:
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 /> </> ); };
여기서는 특별한 것이 없습니다.
gltfjsx
로 생성된 코드이고 3D 모델로, 제가 수동으로receiveShadow
와castShadow
props를 mesh에 추가했습니다.
우리에게는 놀이터가 있지만, 더 이상 ground가 없습니다. 우리는 놀이터의 mesh를 RigidBody로 감싸야 합니다:
// ... import { RigidBody } from "@react-three/rapier"; export function Playground(props) { // ... return ( <group {...props} dispose={null}> <RigidBody type="fixed" name="ground"> {/* ... */} </RigidBody> </group> ); } // ...
모든 mesh를 RigidBody로 감싸고 각각에 box collider를 추가했습니다. 하지만 더 복잡한 모양이 있기 때문에, 대신 trimesh collider를 사용할 것입니다:
<RigidBody type="fixed" name="ground" colliders="trimesh">
이제 모든 mesh가 완벽하게 감싸져 있습니다.
Third person controller
우리 게임을 플레이 가능하게 만들기 위해, third person controller를 우리의 큐브에 추가해야 합니다.
카메라가 우리의 player를 따라다니도록 해 봅시다. RigidBody를 이동할 때, 그 안에 카메라를 넣을 수 있습니다:
// ... import { PerspectiveCamera } from "@react-three/drei"; export const Player = () => { // ... return ( <RigidBody // ... > <PerspectiveCamera makeDefault position={[0, 5, 8]} /> {/* ... */} </RigidBody> ); };
카메라의 위치는 완벽하게 동작하지만, 기본적으로 카메라는 원점 [0, 0, 0]
을 바라보고 있고 우리는 큐브를 바라보도록 하고 싶습니다.
카메라를 매 프레임마다 업데이트해야 합니다. 이를 위해, camera에 ref를 만들고 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> ); };
큐브의 위치를 얻기 위해, Rapier 객체로서 rb.current.translation()
을 사용해야 합니다. Rapier 벡터를 three.js 벡터로 변환하기 위해 vec3
메서드를 사용합니다.
lerp
와 cameraTarget
을 사용하여 카메라를 큐브 위치로 부드럽게 이동시킵니다. lookAt
을 사용하여 카메라가 큐브를 바라보도록 합니다.
이제 카메라는 제대로 큐브를 따라가지만, 현재의 이동 시스템은 이 게임 타입에 가장 적합하지 않습니다.
z
축에서 움직이기 위해 위/아래 화살표를 사용하고, x
축에서 움직이기 위해 왼쪽/오른쪽 화살표를 사용하는 대신, 플레이어를 회전시키기 위해 왼쪽/오른쪽 화살표를 사용하고, 앞으로/뒤로 움직이기 위해 위/아래 화살표를 사용할 것입니다:
// ... 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); // 회전이 x 및 z에 적용되어 올바른 방향으로 이동합니다. 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); }); // ... };
이를 분해해 볼까요:
- 회전 속도를 저장하기 위해
rotVel
변수를 만듭니다 - 사용자가 왼쪽이나 오른쪽 화살표를 누를 때
y
축에서 회전 속도를 변경합니다 rb.current.setAngvel(rotVel, true)
를 사용하여 큐브에 회전 속도를 적용합니다rb.current.rotation()
를 사용하여 큐브의 현재 회전을 얻습니다euler().setFromQuaternion
를 사용해 오일러 각도로 변환합니다- 속도에 회전을
applyEuler
로 적용하여 올바른 방향으로 변환합니다
리스폰
현재, 놀이터에서 떨어지면 계속해서 떨어지게 됩니다. 우리는 리스폰 시스템을 추가해야 합니다.
우리가 할 일은 놀이터 아래에 아주 큰 RigidBodies를 추가하는 것입니다. 플레이어가 그것과 접촉하게 되면, 스폰 지점으로 텔레포트할 것입니다:
// ... 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> {/* ... */} </> ); };
우리는 우리의 RigidBody에 space
라는 이름을 주었고 그것을 sensor로 설정했습니다. 센서는 물리적 세계에 영향을 주지 않으며, 오직 충돌 감지에만 사용됩니다.
이제 Player
컴포넌트에서 onIntersectionEnter
prop을 RigidBody에 사용하고 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> ); };
우리는 setTranslation
메소드를 사용하여 큐브를 스폰 지점으로 텔레포트합니다.
onIntersectionEnter
는 센서를 위한 충돌의onCollisionEnter
와 동등합니다.
빈 공간으로 뛰어들 준비가 되셨나요?
우리의 리스폰 시스템이 작동합니다! 게임처럼 보이기 시작하네요.
스와이퍼
이 멋진 스와이퍼는 우리를 놀이터에서 쫓아내기 위한 것입니다!
애니메이션을 적용하기 위해 힘을 가해야 하므로, 이를 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> ); }
kinematicVelocity
타입은 setLinvel
및 setAngvel
메서드로 움직일 수 있지만 외부 힘에는 영향을 받지 않는 특별한 타입의 RigidBody입니다. (이것은 플레이어가 스와이퍼를 움직이는 것을 방지합니다.)
Experience
컴포넌트에서 스와이퍼의 각 속도를 정의해봅시다:
import React, { useEffect, useRef } from "react"; export function Playground(props) { // ... useEffect(() => { swiper.current.setAngvel({ x: 0, y: 3, z: 0 }, true); }); // ... }
useEffect
를useFrame
대신 사용하는 이유가 궁금하다면, 속도가 일정하기 때문입니다. 값을 변경하지 않는 한 계속 회전할 것입니다.
회전합니다! 회전 속도를 변경하려면 y
값을 조정해보세요.
놀이터에서 쫓겨날 때의 효과가 자연스럽지 않습니다. 이것은 플레이어에 setLinvel
을 사용하는 단점인데, 동시에 많은 것들을 간소화했습니다.
우리가 보는 것은: 큐브가 차여서 날아가고 즉시 멈추는데, 이는 setLinvel
이 이를 취소하기 때문입니다.
빠른 해결책으로는 한동안 차였을 때 setLinvel
을 비활성화하는 것입니다:
// ... 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> ); };
효과가 훨씬 좋아졌습니다!
게이트
플레이어가 게이트를 통과할 때 결승선으로 순간 이동하게하여 게임을 마무리해봅시다.
우선, 게이트를 메인 운동장 rigidbody에서 분리하고, 커스텀 콜라이더를 사용한 새로운 sensor rigidbody를 생성합니다:
// ... 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> ); }
충돌이 감지될 영역을 볼 수 있습니다.
이제 Player
코드에서 이 시나리오를 처리할 수 있습니다:
// ... 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> ); };
getObjectByName
을 사용하여 게이트 위치를 얻고 플레이어를 그 위치로 순간 이동시킵니다.
우리의 게이트 시스템이 작동합니다!
그림자
그림자가 제대로 작동하지 않는 것을 눈치챘을 것입니다. 우리의 놀이터는 기본 그림자 설정에 비해 너무 큽니다.
그림자가 잘려 있습니다.
이를 해결하기 위해 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} /> {/* ... */} </> ); };
CameraHelper
없이는 올바른 값을 찾는 것이 불가능할 것입니다.
올바른 값을 찾기 위해서는, 헬퍼가 그린 것처럼 전체 장면이 near plane과 far plane 사이에 있어야 합니다.
그림자 작동 방식에 대한 복습이 필요하면 그림자 강의를 참조하세요.
이제 그림자가 제대로 작동합니다...
...그리고 우리의 게임이 완료되었습니다! 🎉
결론
이제 React Three Fiber로 자신만의 물리 시뮬레이션 및 게임을 만들 수 있는 기초를 갖추었습니다!
여기서 멈추지 마세요. 이 게임을 개선하고 더 재미있게 만들기 위해 할 수 있는 많은 것들이 있습니다:
- 타이머 및 최고 점수 시스템 추가
- 팩 자산을 사용하여 다른 레벨 생성
- 플레이어가 최종 버튼을 눌렀을 때 멋진 애니메이션 생성
- 다른 장애물 추가
- NPC 추가
다른 물리 엔진 및 라이브러리를 탐색하여 자신의 필요에 가장 잘 맞는 것을 찾으세요.
React Three Fiber를 사용하여 물리, 게임, 및 멀티플레이어에 대해 더 배우고 싶다면 내 YouTube 채널에서 게임 튜토리얼을 확인해 보세요.
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.