在现代 Web 开发中,经典应用程序和 Web 应用程序之间的界限每天都在变得模糊。今天,我们不仅可以创建交互式网站,还可以在浏览器中创建成熟的游戏。让这成为可能的工具之一是库 - 一个使用React技术基于创建 3D 图形的强大工具。
React Three Fiber是Three.js 的包装器,它使用React的结构和原理在 Web 上创建 3D 图形。该堆栈允许开发人员将Three.js的强大功能与React的便利性和灵活性结合起来,使创建应用程序的过程更加直观和有组织。
React Three Fiber的核心理念是,您在场景中创建的所有内容都是React组件。这允许开发人员应用熟悉的模式和方法。
React Three Fiber的主要优点之一是它易于与React生态系统集成。使用此库时仍然可以轻松集成任何其他React工具。
Web-GameDev近年来发生了重大变化,从简单的 2D 游戏发展到可与桌面应用程序相媲美的复杂 3D 项目。受欢迎程度和功能的增长使得 Web-GameDev 成为一个不容忽视的领域。
现代浏览器已经走过了漫长的道路,从相当简单的网络浏览工具发展到用于运行复杂应用程序和游戏的强大平台。 Chrome 、 Firefox 、 Edge等主流浏览器都在不断优化和开发,以确保高性能,使其成为开发复杂应用程序的理想平台。
是推动基于浏览器的游戏发展的关键工具之一。该标准允许开发人员使用硬件图形加速,从而显着提高了 3D 游戏的性能。与其他 webAPI 一起, WebGL为直接在浏览器中创建令人印象深刻的 Web 应用程序开辟了新的可能性。
首先,我们需要一个React项目模板。那么让我们从安装开始吧。
npm create vite@latest
npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js
在main.jsx文件中,添加将作为范围显示在页面上的 div 元素。插入Canvas组件并设置相机的视野。在Canvas组件内放置App组件。
让我们向index.css添加样式,以将UI 元素拉伸到屏幕的整个高度,并将范围显示为屏幕中心的圆圈。
在App组件中我们添加了一个Sky组件,它将以天空的形式显示在我们的游戏场景中作为背景。
让我们创建一个Ground组件并将其放置在App组件中。
在Ground中,创建一个平坦的表面元素。在 Y 轴上向下移动,使该平面位于相机的视野内。并在 X 轴上翻转平面,使其水平。
默认情况下,场景中没有照明,因此让我们添加一个光源ambientLight ,它从各个方向照亮对象,并且没有定向光束。作为参数设置发光强度。
在资源文件夹中添加带有纹理的 PNG 图像。
要在场景中加载纹理,让我们使用@react- Three/drei包中的useTexture钩子。作为钩子的参数,我们将传递导入到文件中的纹理图像。设置图像在水平轴上的重复次数。
使用@react-two/drei包中的PointerLockControls组件,将光标固定在屏幕上,这样当你移动鼠标时它不会移动,但会改变相机在场景中的位置。
让我们对Ground组件进行一些小的编辑。
<mesh position={[0, 3, -5]}> <boxGeometry /> </mesh>
使用@react-two/rapier包中的Physics组件将“物理”添加到场景中。作为参数,配置重力场,我们在其中设置沿轴的重力。
<Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>
然而,我们的立方体位于物理组件内部,但它什么也没发生。为了使立方体表现得像一个真实的物理对象,我们需要将其包装在@react- Three/rapier包中的RigidBody组件中。
让我们回到Ground组件并添加一个RigidBody组件作为地板表面的包装。
让我们创建一个Player组件来控制场景中的角色。
该角色与添加的立方体是同一物理对象,因此它必须与地板表面以及场景中的立方体进行交互。这就是我们添加RigidBody组件的原因。让我们将角色制作成胶囊的形式。
将Player组件放置在Physics 组件内。
角色将使用WASD键进行控制,并使用空格键进行跳跃。
经过公司的各自的react-hook,公司的达到了走动的角色的逻缉。
让我们创建一个hooks.js文件并在其中添加一个新的usePersonControls函数。
实现usePersonControls钩子后,应该在控制角色时使用它。在Player组件中,我们将添加运动状态跟踪并更新角色运动方向的向量。
要更新角色的位置,让我们使用@react- Three/Fiber包提供的Frame 。该钩子的工作原理与requestAnimationFrame类似,每秒执行函数主体约 60 次。
代码说明:
1. const playerRef = useRef();为玩家对象创建链接。此链接将允许与场景中的玩家对象直接交互。
2. const { 向前、向后、向左、向右、跳跃 } = usePersonControls();当使用钩子时,会返回一个具有布尔值的对象,该布尔值指示玩家当前按下了哪些控制按钮。
3. useFrame((状态) => { ... });在动画的每一帧上都会调用该钩子。在此钩子内,玩家的位置和线速度会更新。
4. if (!playerRef.current) 返回;检查玩家对象是否存在。如果没有玩家对象,该函数将停止执行以避免错误。
5. const速度=playerRef.current.linvel();获取玩家当前的线速度。
6. frontVector.set(0, 0, 向后-向前);根据按下的按钮设置向前/向后运动矢量。
7. sideVector.set(左-右, 0, 0);设置左/右移动矢量。
8. Direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED);通过减去运动向量、对结果进行归一化(使向量长度为 1)并乘以运动速度常数来计算玩家运动的最终向量。
9.playerRef.current.wakeUp(); “唤醒”玩家对象以确保它对更改做出反应。如果不使用此方法,一段时间后对象将“休眠”并且不会对位置变化做出反应。
10.playerRef.current.setLinvel({ x: 方向.x, y: 速度.y, z: 方向.z });根据计算出的运动方向设置玩家新的线速度,并保持当前的垂直速度(以免影响跳跃或跌倒)。
结果,当按下WASD键时,角色开始在场景中移动。他还可以与立方体进行交互,因为它们都是物理对象。
为了实现跳跃,我们使用@dimforge/rapier3d-compat和@react- Three/rapier包中的功能。在此示例中,我们检查角色是否在地面上并且已按下跳跃键。在本例中,我们在 Y 轴上设置角色的方向和加速力。
对于玩家,我们将在所有轴上添加质量和块旋转,以便他在与场景中的其他物体碰撞时不会在不同方向上摔倒。
代码说明:
- const world = 剑杆.world;访问Rapier物理引擎场景。它包含所有物理对象并管理它们的交互。
- const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 }));这就是“光线投射”(raycasting)发生的地方。创建一条从玩家当前位置开始并指向 y 轴的射线。该光线被“投射”到场景中,以确定它是否与场景中的任何对象相交。
- const 接地 = ray && ray.collider && Math.abs(ray.toi) <= 1.5;如果玩家在地面上,则检查条件:
- ray -射线是否被创建;
- ray.collider - 射线是否与场景中的任何物体发生碰撞;
- Math.abs(ray.toi) - 光线的“曝光时间”。如果该值小于或等于给定值,则可能表明玩家距离表面足够近,可以被视为“在地面上”。
您还需要修改Ground组件,以便通过添加将与场景中其他对象交互的物理对象来确定“着陆”状态的光线追踪算法正常工作。
为了移动相机,我们将获取玩家的当前位置,并在每次刷新帧时更改相机的位置。为了使角色精确地沿着相机所指向的轨迹移动,我们需要添加applyEuler 。
代码说明:
applyEuler方法根据指定的欧拉角对向量应用旋转。在这种情况下,相机旋转应用于方向矢量。这用于匹配相对于相机方向的运动,以便玩家沿着相机旋转的方向移动。
让我们稍微调整Player的大小,使其相对于立方体更高,增加CapsuleCollider的大小并修复“跳跃”逻辑。
为了使场景不会感觉完全空虚,让我们添加立方体生成。在 json 文件中,列出每个立方体的坐标,然后将它们显示在场景上。为此,创建一个文件cubes.json ,我们将在其中列出一个坐标数组。
[ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]
在Cube.jsx文件中,创建一个Cubes组件,它将循环生成立方体。而Cube组件将直接生成对象。
import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); }
让我们通过删除之前的单个立方体来将创建的立方体组件添加到应用程序组件中。
为了获得将模型导入场景所需的格式,我们需要安装gltf-pipeline附加包。
npm i -D gltf-pipeline
使用gltf-pipeline包,将模型从GLTF 格式重新转换为GLB 格式,因为在此格式中,所有模型数据都放置在一个文件中。我们指定公共文件夹作为生成文件的输出目录。
gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb
然后我们需要生成一个包含该模型标记的反应组件,以将其添加到场景中。让我们使用@react- Three/Fiber开发人员的。
转到转换器将要求您加载转换后的Weapon.glb文件。
在转换器中,我们将看到生成的反应组件,我们将其代码传输到新文件WeaponModel.jsx中的项目,将组件的名称更改为与文件相同的名称。
现在让我们将创建的模型导入到场景中。在App.jsx文件中添加WeaponModel组件。
要在场景上启用阴影,您需要将阴影属性添加到Canvas组件。
接下来,我们需要添加一个新的光源。尽管场景中已经有了环境光,但它无法为对象创建阴影,因为它没有定向光束。因此,让我们添加一个名为orientationLight 的新光源并对其进行配置。启用“投射”阴影模式的属性是castShadow 。正是这个参数的添加,表明这个物体可以给其他物体投射阴影。
之后,我们给Ground组件添加另一个属性receiveShadow ,这意味着场景中的组件可以接收并显示自身的阴影。
类似的属性应该添加到场景中的其他对象:立方体和玩家。对于立方体,我们将添加castShadow和receiveShadow ,因为它们都可以投射和接收阴影,而对于玩家,我们将仅添加castShadow 。
让我们为Player添加castShadow 。
为Cube添加castShadow和receiveShadow 。
原因是默认情况下相机仅捕获来自orientationLight的显示阴影的一小部分区域。我们可以通过为orientationLight组件添加额外的属性shadow-camera-(top,bottom,left,right)来扩展这个区域的可见性。添加这些属性后,阴影会变得稍微模糊。为了提高质量,我们将添加shadow-mapSize属性。
现在让我们添加第一人称武器显示。创建一个新的武器组件,其中将包含武器行为逻辑和 3D 模型本身。
import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }
让我们将此组件放置在与角色的RigidBody相同的水平上,并在useFrame挂钩中,我们将根据相机值的位置设置位置和旋转角度。
为了使角色的步态更加自然,我们将在移动时添加武器的轻微摆动。为了创建动画,我们将使用已安装的tween.js库。
Weapon组件将被包装在一个组标签中,以便您可以通过useRef挂钩添加对它的引用。
让我们添加一些useState来保存动画。
代码说明:
- const twSwayingAnimation = new TWEEN.Tween(currentPosition) ...创建对象从当前位置“摆动”到新位置的动画。
- const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ...创建第一个动画完成后对象返回到其起始位置的动画。
- twSwayingAnimation.chain(twSwayingBackAnimation);连接两个动画,以便当第一个动画完成时,第二个动画自动开始。
在useEffect中我们调用动画初始化函数。
代码说明:
- const isMoving = Direction.length() > 0;这里检查对象的运动状态。如果方向向量的长度大于0,则表示物体有运动方向。
- if (isMoving && isSwayingAnimationFinished) { ... }如果对象正在移动并且“摆动”动画已完成,则执行此状态。
在App组件中,我们添加一个useFrame来更新补间动画。
TWEEN.update()更新TWEEN.js库中的所有活动动画。在每个动画帧上调用此方法以确保所有动画顺利运行。
我们需要定义射击的时刻 - 即按下鼠标按钮的时刻。让我们添加useState来存储此状态, useRef来存储对武器对象的引用,以及两个用于按下和释放鼠标按钮的事件处理程序。
让我们在单击鼠标按钮时实现反冲动画。为此,我们将使用tween.js库。
让我们创建函数来获取反冲动画的随机向量-generateRecoilOffset和generateNewPositionOfRecoil 。
创建一个函数来初始化反冲动画。我们还将添加useEffect ,其中我们将指定“镜头”状态作为依赖项,以便在每次镜头时再次初始化动画并生成新的结束坐标。
在useFrame中,我们添加一个检查“按住”鼠标键以进行射击,以便在释放按键之前射击动画不会停止。
为此,我们通过useState添加一些新状态。