别卷了,快来玩 | React+Three.js 实现一个超好玩的3D游戏:美女与龙珠
点击上方 三分钟学前端 ,关注公众号
回复 交流 ,加入前端编程面试算法每日一题群
面试官也在看的前端面试资料
游戏故事
游戏讲述的是一个小女生被恶魔诅咒找不到家了,她听说收集七龙珠可以召唤神龙,神龙可是帮她实现回家的愿望,于是她开启了她的冒险故事
使用技术
这个游戏使用了以下技术
-
Vite
+React
-
基于
Three.js
的 Lingo3D [2]
以及使用了以下工具:
-
sketchfab
:3D模型下载 -
mixamo
:3D人物动作绑定及动画 -
readyplayer
:3D角色生产工具 -
gltf.report
:模型压缩 -
polyhaven
:hdr素材库(环境贴图) -
textures
:材质贴图素材
部分效果实现如下 :point_down:








开始
其实我也不知道从哪里开始,也不知道写什么好?
但我好像需要一个角色,那就从创建角色开始
创建一个角色
下载角色
首先到 sketchfab [3] 中下载一个角色模型
当然如果你是首次使用 sketchfab
,记得先注册和登录
注册好之后,就会看到以下界面

箭头指的输入框输入关键字,即可筛选出相关的模型,然后我们从中进行挑选
注意,挑选人物模型时,最好选择 T-pose [4] 类型,比如 这样可以方便之后绑定骨骼
我这里选择了这下面这个小妹妹,我觉得还不错

然后点击右上角的下载按钮,进入下载界面

一般fbx使用的比较多,但是有的时候不是fbx的格式,我们需要使用其他工具转格式,或者处理模型
为了方便,我们这里选择第二个:glTF 格式,点击 DOWNLOAD
下载好之后是个压缩包,我们进行解压,解压后目录如下

处理角色模型
接着我们选择使用模型处理软件来处理我们的模型,比如我这里选择 blender
当然得先下载软件,我们进入 官网 [5] ,然后点击如下按钮进行下载

安装就直接点击下一步下一步就行,之后打开软件,看到以下界面

开始进来中间是有一个立方体的,我们需要移除。步骤是鼠标选中,按x删除
之后按如下步骤导入我们刚刚下载的模型


结果如下,表示导入成功

但看不到人,可以按x或者Delete将绑定的骨骼删除,即可展示人物

但是人物怎么没有颜色,这时因为这时展现的是实体,我们可以按z 甩狙,选择渲染即可

选择的模型比较正常,所以不用太多的处理,接着我们将它转成fbx格式,但是在转之前需要将灯光和相机移除,以防止模型导入页面时光线等问题

可以跟之前一样选择中,按x即可删除
下面开始将它转成fbx格式,直接按以下步骤导出即可

需要注意,导出时需要将路径模式改成复制,并打开内嵌纹理。否则导出的模型将没有颜色

制作动画
模型有了之后,我们我们的让模型动起来,这是我们需要使用动画库相关的软件,这里选择使用我选择 Mixamo
,他是Adobe公司出品的免费动画库,它提供了大量的人物动画
进入 www.mixamo.com/#/ [6] 然后上传刚刚我们自己的3d模型,步骤如下

之后等待上传
上传成功会显示如下界面

然后点击 NEXT,绑定骨骼

成功之后点击 NEXT,成功之后将看到如下界面

这时,我们先将T-pose下载下来
之后,我们可以在左边选择我们需要的动画,比如选个跳舞

没错就是这么简单
之后可以 点击 DOWNLOAD 开始下载动画

类似步骤,我们可以下载很多我们需要的动画
准备好动画等素材之后,开始准备搭建项目,然后将相关模型渲染到我们的页面中
搭建项目
这是选择使用vite来搭建一个react项目:
yarn create vite 复制代码
创建项目之后,进入项目、安装依赖
cd [项目名称] yarn yarn dev 复制代码
然后安装一个lingo3d-react
yarn add lingo3d-react 复制代码
接着我们修改src\App.jsx文件,将多余的东西都去掉,同时添加 <World>
场景标签。代码变成如下样子:
import { World } from "lingo3d-react" function App() { return ( <World> </World> ) } export default App 复制代码
接着启动项目
yarn dev 复制代码
成功之后,显示如下
vite v2.9.5 dev server running at: > Local: http://localhost:3000/ > Network: use `--host` to expose ready in 359ms. 复制代码
接着就可以进入 http://localhost:3000/ [7]
进去就可以看到一个默认的一个黑的场景,如下:

到这,说明项目已经构建成功!下面就是要加载我们的角色模型和场景模型
加载模型
加载角色
首先需要创建一个public文件夹,并将我们下载好的模型放进去
接着使用 <Model>
加载我们的模型
import { Model, World } from "lingo3d-react" function App() { return ( <World> <Model src="girl.fbx" animations={{ idle: "Standing Idle.fbx"}} animation="idle" scale={3} > </Model> </World > ) } export default App 复制代码
属性说明:
-
src 是我们的T-pose
-
animations 是所要用的动画
-
animation 是当前动画
-
scale 是缩放
这里有个坑,就是src中一定得是T-pose,所以在最开始时一定要导出T-pose,否则动画加载不出来。
到这,我们的角色动画就出来了

有了小妹妹,接下来就是给她一个家
加载房子
更上述类似的步骤,我们添加一个房子
import { Model, World } from "lingo3d-react" function App() { return ( <World> <Model src="girl.fbx" animations={{ idle: "Standing Idle.fbx"}} animation="idle" scale={4} > </Model> <Model src="Home.glb" scale={7} > </Model> </World > ) } export default App 复制代码
结果如下

但大小需要调整,另外视角需要调整,可以使用第一人称或者第三人称相机来切换视角
使用第三人称相机 ThirdPersonCamera
使用ThirdPersonCamera表示已第三人称的角度看到人物,代码如下
import { Model, ThirdPersonCamera, World } from "lingo3d-react" function App() { return ( <World> <ThirdPersonCamera active mouseControl> <Model src="girl.fbx" animations={{ idle: "Standing Idle.fbx" }} animation="idle" scale={3} /> </ThirdPersonCamera> <Model src="Home.glb" scale={5} /> </World > ) } export default App 复制代码
其中
-
active 属性表示激活
-
mouseControl 表示鼠标的控制
效果如下:

但是此时小妹妹和房子捏在一起了,这是可以设置模型的物理属性
物理属性 physics
物理属性physics的值有很多,这里设置角色physics="character",设置房子physics="map"
-
character 表示主角
-
map 表示地图
代码如下:
import { Model, ThirdPersonCamera, World } from "lingo3d-react" function App() { return ( <World> <ThirdPersonCamera active mouseControl> <Model src="girl.fbx" physics="character" animations={{ idle: "Standing Idle.fbx" }} animation="idle" scale={1} /> </ThirdPersonCamera> <Model src="Home.glb" scale={10} physics="map" /> </World > ) } export default App 复制代码
效果如下

可以看到人物就和房子分离开了,但还有一个问题是:天空是黑色的
设置天空我们需要用到天空盒 Skybox
添加 Skybox
查找天空背景
查找天空背景可以google或者百度输关键字:equirectangular sky / skybox background
添加Skybox
然后再 <World >
中添加 <Skybox texture="xxx" />
,比如:
<Skybox texture="skybox.jpeg" /> 复制代码
这样就有天空啦,效果如下:


但我发现人物有点浮在空中,应该是模型的物理碰撞结构的原因
我们将map换成map-debug检查一下


发现,这个模型里好多小雪花,被计算成碰撞体积了。
所以得调好人物的初始位置
调好之后再来换个场景,比如我看到下面这个场景感觉不错

接着我们将场景下载并导入
import { Model, Skybox, ThirdPersonCamera, World } from "lingo3d-react" function App() { return ( <World> <ThirdPersonCamera active mouseControl> <Model src="girl.fbx" physics="character" animations={{ idle: "idle.fbx",walking:"walking.fbx" }} animation="idle" scale={1} /> </ThirdPersonCamera> <Model src="map/scene.gltf" scale={30} physics="map" /> <Skybox texture="skybox.jpg" /> </World > ) } export default App 复制代码
显示结果如下:


接着我想让角色可以用键盘wsad控制上、下、左、右的移动
角色移动
首先得在 mixamo
准备好移动相关的动画,比如走路、跑步等
将人物模型的animation设置成对应的动画,比如animation="walking",这是就会用走路的动画了

其他相关的动画也是这样设置
下面整理一下如何让角色移动的思路
-
初始是站立状态
-
当我们按下键盘wsad键时,触发走路动画,角色分别向前、后、左、右的移动;如果短时间连续按两下,触发跑步动画
-
当松开按键时,角色进入初始站立状态
实现前后移动
当按下w时,切换向前移动动画,并向前移动;当按下s键时,切换倒退动画,人物向后移动;
代码如下
import { Model, Skybox, ThirdPersonCamera, useKeyboard, useLoop, World } from "lingo3d-react" import { createRef, useRef } from "react" function App() { // useKeyboard用于监控当前按键 const key = useKeyboard() const characterRef = createRef() //声明motion,用于表示当前角色应该对应的动画,默认为站立idle let motion = "idle"; // 前 if (key === "w") { motion = "walking" } // 后 if (key === "s") { motion = "walking_backwards" } // useLoop 帧循环勾子 useLoop(() => { characterRef.current.moveForward(-3) }, key === "w"); useLoop(() => { characterRef.current.moveForward(3) }, key === "s"); return ( <World> <ThirdPersonCamera active> <Model ref={characterRef} src="girl.fbx" physics="character" animations={{ idle: "idle.fbx", walking: "walking.fbx", walking_backwards: "walking-backwards.fbx" }} animation={motion} scale={1} /> </ThirdPersonCamera> <Model src="map/scene.gltf" scale={40} physics="map" /> <Skybox texture="skybox.jpg" /> </World > ) } export default App 复制代码
实现效果如下
按w时,向前 :point_down:

按a时,向后 :point_down:

左转、右转
左转右转的改变,可以通过鼠标来控制,即mouseControl属性
<ThirdPersonCamera active mouseControl> 复制代码
效果如下

添加跑步动画
由于上面是用w表示走路,这里就用w加e的时候实现跑步吧,代码如下:
import { Model, Skybox, ThirdPersonCamera, useKeyboard, useLoop, World } from "lingo3d-react" import { createRef, useRef } from "react" function App() { // useKeyboard用于监控当前按键 const key = useKeyboard() console.log(key); const characterRef = createRef() //声明motion,用于表示当前角色应该对应的动画,默认为站立idle let motion = "idle"; // 前 if (key === "w") { motion = "walking" } // 后 if (key === "s") { motion = "walkingBackwards" } // 按下w和e时,开始跑 if (key === "w e") { motion = "running" } // useLoop 帧循环勾子 useLoop(() => { characterRef.current.moveForward(-4) }, key === "w"); useLoop(() => { characterRef.current.moveForward(1.8) }, key === "s"); useLoop(() => { characterRef.current.moveForward(-10) }, key === "w e"); return ( <World> <Skybox texture="skybox.jpg" /> <ThirdPersonCamera active mouseControl> <Model ref={characterRef} src="girl.fbx" physics="character" animations={{ idle: "idle.fbx", walking: "walking.fbx", walkingBackwards: "walking-backwards.fbx", running: "running.fbx" }} animation={motion} scale={1} /> </ThirdPersonCamera> <Model src="map/scene.gltf" scale={40} physics="map" /> <Model src="dragon_ball/scene.gltf" scale={10} physics="character" /> </World > ) } export default App 复制代码
效果如下

接着,要不找点什么东西吧,比如收集七颗龙珠
收集七龙珠
引入7龙珠
首先得下载龙珠,步骤跟之前一样,之后引入

移动角色和龙珠到合适的位置
下面我希望将角色和龙珠的初始位置移动到合适的地方
可以通过model身上的x、y、z来控制,但是具体多少呢?可以使用Editor组件开启编辑模式,来测
xxx return ( <> <World> </World > //开启编辑模式 <Editor/> </> ) 复制代码
于是就可以看到以下的编辑模式,可以用鼠标进行移动

移动好之后可以记下右边的数据,主要position和rotation
比如
<ThirdPersonCamera active mouseControl> <Model ref={characterRef} src="girl.fbx" physics="character" animations={{ idle: "idle.fbx", walking: "walking.fbx", walkingBackwards: "walking-backwards.fbx", running: "running.fbx" }} animation={motion} scale={1} x={-221.30} y={-1300.07} z={-6722.07} rotationY={-180} /> </ThirdPersonCamera> <Model ref={ballRef} src="dragon_ball.fbx" physics="character" x={516.29} y={-1198.63} z={173.60} scale={.5} /> 复制代码
寻找龙珠
我们可以通过w和s和w+e以及配合鼠标来控制角色移动来寻找龙珠,当寻找到龙珠时,即准心对准龙珠时,让龙珠有变化,比如高亮或者啥的
所以,首先弄个准心吧
准心
通过blender找到龙珠对应的名字

然后在龙珠模型中添加一个Find标签
<Model id="ball" ref={ballRef} src="dragon_ball.fbx" physics="character" x={516.29} y={-1198.63} z={173.60} scale={.5} > <Find name="Two Star_02 - Default_0" outline></Find> </Model> 复制代码
表示的是在模型中找到的东西,我们让他outline,以示区分,效果可以之后调
展示如下

但是默认情况,我们应该是FALSE,不显示的
这里引入状态来控制,如下代码
//... const [mouseOver,setMouseOver] = useState(false); //.... <Find name="Two Star_02 - Default_0" outline={mouseOver} onMouseOver={() => { setMouseOver(true) }} ></Find> 复制代码
这样的话,默认就是不显示发光特效,只有鼠标准心对准过后才发光,表示找到了的
接着我们需要添加准心,来方便找,通过Reticle
<World > </World > <Reticle color="white" variant={1}/> //xxx 复制代码
-
color是准心的颜色;
-
variant是准心的形状,可以通过不同数字选择自己喜欢的形状
上述代码效果如下 :point_down:

但是此时 瞄准器 在人物的屁股上,希望的是瞄准器在人物的头顶上,相当于是人物的目光
所以我的调整相机的位置,让相机升到人物的头顶上去
调整相机位置
其实可以通过相机的innerX、innerY、innerZ来设置,如下代码
<ThirdPersonCamera active mouseControl innerY={66}> 复制代码
效果如下

找到龙珠
当找到龙珠并点击时,龙珠亮
代码如下
<Model id="ball" ref={ballRef} src="dragon_ball.fbx" physics="character" x={516.29} y={-1198.63} z={173.60} scale={.5} > <Find name="Two Star_02 - Default_0" outline={mouseOver} onClick={() => { setMouseOver(true) }} ></Find> </Model> 复制代码
效果如下

类似的方式将其他龙珠添加到合适的位置
另一种寻找方式
除了看到点击可以点亮龙珠以外,当角色和龙珠碰撞时也应点亮龙珠
那首先要检测角色碰撞
步骤如下:
-
先给龙珠添加一个id
-
给角色添加intersectID属性,值为一个数组,数组的每一项是每个龙珠的id名
-
再给角色添加onIntersect属性,值为一个回调函数,这个回调函数会在角色和龙珠发生碰撞时触发
所以,需要做的事情都是这个回调函数中执行。
这步之后加上吧
神龙出现
神龙出现,小龙消失
当所有龙珠都找到时,地图上某个地方就会出现龙,你需要找到它

找到之后点击它,小龙消失,真神龙就会现身了

神龙许愿
点击神龙,过一会就会送我回到家里,并且回去之后可以看到7颗龙珠
完整的游戏过程
1. 通过按键和鼠标控制角色移动
w键:向前跑

s键:向后走

鼠标:移动鼠标可控制方向

空格:跳跃

2. 寻找七龙珠
当看到龙珠时,对准并按下鼠标即可标记此龙珠已经找到,然后继续找下一颗

当所有龙珠被找到时,会提示地图某处会出现龙

3. 寻找龙
当提示地图某处出现龙时,就去寻找龙
此龙如图所示

但是到这没有结束,此龙非真的神龙
4. 真神龙出现
点击小龙,小龙会消失,真的神龙出现


点击神龙,一会就会实现回家的愿望
5. 回到家
到这就会回到家了,如下

而且家附近会出现我们找到的龙珠

最后
最后能回到家,肯定开心啦
所以按住d键,开始跳舞吧

参考
Lingo3D 官方地址(GitHub): github.com/lingo3d/lin… [8]
Lingo3D B站地址: b23.tv/R7T8md5 [9]
小红旗 Hubert: b23.tv/0ewcIiw [10]
结语
以上就是本文的所有内容啦,源码这两天完善后回上传github,敬请期待~
关于本文
作者:LBJ
http://juejin.cn/post/7087730315531141128
最后
欢迎关注「 三分钟学前端 」
号内回复:
「 网络 」,自动获取三分钟学前端网络篇小书(90+页)
「 JS 」,自动获取三分钟学前端 JS 篇小书(120+页)
「 算法 」,自动获取 github 2.9k+ 的前端算法小书
「 面试 」,自动获取 github 23.2k+ 的前端面试小书
「 简历 」,自动获取程序员系列的 120
套模版
》》面试官也在看的前端面试资料《《
“在看和转发” 就是最大的
- Vue 可视化大屏适配插件之过程篇
- Vue3生命周期Hooks的原理及其与调度器(Scheduler)的关系
- 10 个不错的 CSS 小技巧
- 教你使用 koa2 vite ts vue3 pinia 构建前端 SSR 企业级项目
- 别卷了,快来玩 | React Three.js 实现一个超好玩的3D游戏:美女与龙珠
- 手动实现Vue3 & 原理解析:setup环境 & reactive函数 & effect函数(一)
- 前端代码的三种设计模式
- 觉得自己的页面不够花哨吗,试试clip-path吧
- 简易版 useState 实现
- 回溯算法汇总一
- CSS 的 Filter属性竟然如此好玩
- 轻轻松松拿下 JS 浅拷贝、深拷贝
- 2022 前端应该掌握的 10 个 JS 小技巧
- 一文搞懂 Vue3.0 为什么采用 Proxy
- 字节飞书面试——请实现 Promise.all
- 我把 Vue3 项目中的 Vuex 去除了,改用 Pinia
- 从0到1400star,从阮一峰周刊到尤雨溪推荐,小透明开源项目的2021年总结
- type 和 interface的区别知多少?
- 当webpack有了vite的速度你会喜欢吗?
- 前端面试百问(含解答)