物理

Starter pack

物理为您的3D项目开启了一个全新的可能性世界。您可以创建逼真的宇宙、用户交互,甚至是游戏。

在本课中,我们将通过构建一个简单的游戏来了解基本概念。

不用担心,不需要事先了解物理知识,我们将从头开始。(顺便说一下,我在学校的物理课成绩很差,所以如果我能做到,你也可以做到!)

物理引擎

要为我们的3D项目添加物理效果,我们将使用一个物理引擎。物理引擎是一个可以为我们处理复杂数学运算的库,比如重力、碰撞、力等。

在 JavaScript 生态系统中,有许多可用的物理引擎。

两个非常流行的是 Cannon.jsRapier.js

Poimandres(再次)开发了两个很棒的库,使这些引擎能够与 React Three Fiber一起使用:react-three-rapieruse-cannon

在本课中,我们将使用 react-three-rapier,但它们非常相似,我们在这里学习的概念可以应用于这两个库。

要安装它,请运行:

yarn add @react-three/rapier

现在我们准备开始了!

物理世界

在创建游戏之前,让我们先了解一些基本概念。

首先,我们需要创建一个物理世界。这个世界将包含我们场景中所有的物理对象。使用 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)。刚体是一个组件,它将使我们的对象在物理世界中移动。

是什么可以触发对象的移动呢?,例如重力碰撞用户交互

让我们将位于Player.jsx中的立方体告知我们的 物理世界 为一个物理对象,通过添加一个刚体

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

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

现在我们的立方体开始响应重力并向下坠落。但是它会一直掉下去!

我们需要让地面也成为一个物理对象,这样立方体就可以与其碰撞并停止下落。

让我们在Experience.jsx中为地面添加一个刚体,但由于我们不希望它像立方体那样移动和下落,我们将添加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

回到起点,我们有一个静止的立方体在地面上。但在底层,我们有一个对重力做出反应的立方体,并因与地面的碰撞而停止。

力量

现在我们有了一个物理世界和物理对象,可以开始玩弄力量了。

我们将通过键盘箭头键让立方体移动。为此,可以使用我们在事件课程中发现的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>;
};

小心确保引用是指向RigidBody的而不是mesh。

虽然有效,但加速太快并且在地面上滑行。我们可以通过给地面添加更多的摩擦力来解决这个问题:

// ...

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

摩擦力使立方体旋转,因为它抓住了地面。我们可以通过锁定立方体的旋转来解决这个问题:

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

现在好多了,但立方体仍然有一点滑动。我们可以通过调整立方体的线性阻尼来修复它,但我们不会在本课程中继续这条路径。

因为我们还需要调整立方体的最大速度以防止其不断加速。当我们使用左右键旋转立方体而不是移动它时,我们将面临问题。

让我们切换系统来使用setLinVel替代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>;
};

您还可以移除地面的摩擦,因为它不再需要。

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 会根据 mesh 的几何自动为 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;

由于我们对立方体和地面使用的是 box 几何,当前的碰撞体完美地包裹着它们,我们几乎看不到调试模式中的轮廓颜色。

让我们把立方体的碰撞体切换为 sphere 碰撞体:

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

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

这种方式是半自动的,我们告诉 rapier 我们想要哪种碰撞体,它将会根据 mesh 大小自动创建。

我们也可以手动添加碰撞体并微调它:

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

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

我们将 colliders 设置为 false 以阻止 rapier 自动创建碰撞体。

球形碰撞体包裹住我们的立方体

不同类型的碰撞体有:

  • box:一个 box 碰撞体
  • ball:一个 sphere 碰撞体
  • hull:可以视为包裹住 mesh 的礼物包装
  • trimesh:一个将完美包裹住 mesh 的碰撞体

始终使用最简单的碰撞体以提高性能。

现在我们知道立方体和地面的碰撞体,让我们来检测它们之间的碰撞,以了解立方体何时在地面上。

让我们从立方体中移除 球形碰撞体 并在 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 轴设置为较低的值来创建月球重力效应。

但默认重力是现实的,适合我们的游戏,所以我们将保持不变,并通过 RigidBody 上的 gravityScale 属性影响立方体重力:

// ...

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

我们需要手动创建球体碰撞器,因为即使它看起来像一个球,模型也更复杂,并且 Rapier 无法自动为其创建合适的碰撞器。

调整 restitution 级别以使球弹跳多或少,以及 gravityScale 以使球落下得更快或更慢。

看起来不错!

是时候创建我们的游戏了!

游戏

为了创建这个游戏,我在 Blender 中使用了 Kay Lousberg 的 Mini-Game Variety Pack 中的素材准备了一个 地图

Kay-kit mini game variety pack

这是一个非常棒的免版税资源包,里面有很多素材,我强烈推荐!

操场

让我们在 Experience 中使用 Playground 组件,并移除地面:

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模型,我只是手动给 meshes 添加了 receiveShadowcastShadow 属性。

Playground

我们有了 操场,但我们没有 地面 了。我们需要用 RigidBody 包装 操场的 meshes

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

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

// ...

它用 RigidBody 包装了每个 meshes 并为其添加了一个 box collider。但是因为我们有更复杂的形状,所以我们将使用 trimesh collider

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

Trimesh collider

现在每个 meshes 都被完美地包裹住了。

第三人称控制器

为了使我们的游戏能够玩,我们需要为我们的立方体添加一个第三人称控制器

让我们使相机跟随我们的角色。在移动其RigidBody时,我们可以将相机放入其中:

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

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

其位置完美无缺,但默认情况下,相机是看向原点 [0, 0, 0] ,我们希望它看向立方体。

我们需要在每个 frame 更新它。为此,我们可以在相机上创建一个ref并使用我们的 useFrame hook:

// ...

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()。我们使用 vec3 方法将Rapier向量转换为three.js向量。

我们使用 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);
    // apply rotation to x and z to go in the right direction
    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组件中,我们可以使用 RigidBody 上的onIntersectionEnter属性并调用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 的碰撞等价物。

准备好跳入虚空了吗?

我们的重生系统生效了!开始有游戏的感觉了。

Swiper

这个漂亮的 swiper 是为了把我们从游乐场中踢出去!

Swiper

因为我们需要施加一个力来使其动画化,我们需要将其移动到自己的 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 类型是一个特殊类型的 RigidBody,它可以通过 setLinvelsetAngvel 方法移动,但不会受到外部力量的影响。(它阻止了我们的玩家移动swiper)

让我们在 Experience 组件中定义 swiper 的 angular velocity

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

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

如果你想知道为什么我们使用 useEffect 而不是 useFrame,那是因为速度是恒定的,只要我们不更改它,它将保持旋转。

它旋转了!可以自由调整 y 的值来改变旋转速度。

当我们被踢出游乐场时的效果并不自然。这是因为在我们的 player 上使用 setLinvel 而不是 applyImpulse 所造成的不足,但它也简化了很多事情。

我们看到的是:立方体被踢出并抛出,但它立即停止,因为我们的 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>
  );
};

效果现在好多了!

当玩家进入门时,将其传送到终点,让我们完成这个游戏。

我们首先将门从主要游乐场的刚体中分离出来,并创建一个带有自定义碰撞器的新传感器刚体:

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

我们可以看到将检测碰撞的区域。

现在在我们的 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 获取门的位置并将玩家传送到那里。

我们的门系统有效!

阴影

您可能已经注意到,我们的阴影并没有正常工作。我们的游乐场对默认的阴影设置来说太大了。

阴影切割

阴影被切割了。

为了解决这个问题,我们需要调整阴影相机设置:

// ...
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,我们将无法找到合适的值。

为了找到那些值,你需要使整个场景位于 helper 绘制的近剪裁面和远剪裁面之间。

如果您需要复习关于阴影的工作原理,请参考阴影课程

阴影 helper

我们的阴影现在正常工作了...

...并且我们的游戏完成了!🎉

结论

您现在拥有使用 React Three Fiber 创建自己物理仿真和游戏的基础知识!

不要止步于此,还有很多事情可以做来改进这个游戏并让它更有趣:

  • 添加计时器和最高得分系统
  • 使用一组资源创建其他关卡
  • 玩家按下最后一个按钮时创建一个酷炫的动画
  • 添加其他障碍物
  • 添加 NPCs

探索其他物理引擎和库,以找到最适合您需求的那个。

我在我的 YouTube 频道上使用 React Three Fiber 制作了游戏教程。如果您想了解更多关于物理游戏多人游戏的内容,可以查看这些教程。

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.