别卷了,快来玩 | React+Three.js 实现一个超好玩的3D游戏:美女与龙珠

语言: CN / TW / HK

theme: cyanosis

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

这两天我想写了个小游戏,参加这个征文活动 👆

游戏故事

游戏讲述的是一个小女生被恶魔诅咒找不到家了,她听说收集七龙珠可以召唤神龙,神龙可是帮她实现回家的愿望,于是她开启了她的冒险故事

使用技术

这个游戏使用了以下技术 1. vite + React 2. 基于 Three.jslingo3d

以及使用了以下工具: 1. sketchfab: 3D模型下载 2. mixamo:3D人物动作绑定及动画 3. readyplayer:3D角色生产工具 4. gltf.report:模型压缩 5. polyhaven:hdr素材库(环境贴图) 6. textures:材质贴图素材

部分效果实现如下 👇

20.gif

22.gif

24.gif

image.png

image.png

image.png

image.png

25.gif

开始

其实我也不知道从哪里开始,也不知道写什么好?

但我好像需要一个角色,那就从创建角色开始

创建一个角色

下载角色

首先到 sketchfab 中下载一个角色模型

当然如果你是首次使用 sketchfab ,记得先注册和登录

注册好之后,就会看到以下界面

image.png

箭头指的输入框输入关键字,即可筛选出相关的模型,然后我们从中进行挑选

注意,挑选人物模型时,最好选择 T-pose 类型,比如 这样可以方便之后绑定骨骼

我这里选择了这下面这个小妹妹,我觉得还不错

image.png

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

image.png

一般fbx使用的比较多,但是有的时候不是fbx的格式,我们需要使用其他工具转格式,或者处理模型

为了方便,我们这里选择第二个:glTF 格式,点击 DOWNLOAD

下载好之后是个压缩包,我们进行解压,解压后目录如下

image.png

处理角色模型

接着我们选择使用模型处理软件来处理我们的模型,比如我这里选择 blender

当然得先下载软件,我们进入官网,然后点击如下按钮进行下载

image.png

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

image.png

开始进来中间是有一个立方体的,我们需要移除。步骤是鼠标选中,按x删除

之后按如下步骤导入我们刚刚下载的模型

image.png

image.png

结果如下,表示导入成功

image.png

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

1.gif

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

2.gif

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

image.png

可以跟之前一样选择中,按x即可删除

下面开始将它转成fbx格式,直接按以下步骤导出即可

image.png

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

image.png

制作动画

模型有了之后,我们我们的让模型动起来,这是我们需要使用动画库相关的软件,这里选择使用我选择 Mixamo,他是Adobe公司出品的免费动画库,它提供了大量的人物动画

进入http://www.mixamo.com/#/ 然后上传刚刚我们自己的3d模型,步骤如下

image.png

之后等待上传

上传成功会显示如下界面

image.png

然后点击 NEXT,绑定骨骼

3.gif

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

4.gif

这时,我们先将T-pose下载下来 image.png

之后,我们可以在左边选择我们需要的动画,比如选个跳舞

5.gif

没错就是这么简单

之后可以 点击 DOWNLOAD 开始下载动画

image.png

类似步骤,我们可以下载很多我们需要的动画

准备好动画等素材之后,开始准备搭建项目,然后将相关模型渲染到我们的页面中

搭建项目

这是选择使用vite来搭建一个react项目: yarn create vite 创建项目之后,进入项目、安装依赖 cd [项目名称] yarn yarn dev 然后安装一个lingo3d-react yarn add lingo3d-react 接着我们修改src\App.jsx文件,将多余的东西都去掉,同时添加<World>场景标签。代码变成如下样子:

```jsx import { World } from "lingo3d-react"

function App() {

return (

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

进去就可以看到一个默认的一个黑的场景,如下:

image.png

到这,说明项目已经构建成功!下面就是要加载我们的角色模型和场景模型

加载模型

加载角色

首先需要创建一个public文件夹,并将我们下载好的模型放进去

接着使用 <Model> 加载我们的模型

```jsx import { Model, World } from "lingo3d-react"

function App() {

return ( ) }

export default App ``` 属性说明: - src 是我们的T-pose - animations 是所要用的动画 - animation 是当前动画 - scale 是缩放

这里有个坑,就是src中一定得是T-pose,所以在最开始时一定要导出T-pose,否则动画加载不出来。

到这,我们的角色动画就出来了

image.png

有了小妹妹,接下来就是给她一个家

加载房子

更上述类似的步骤,我们添加一个房子

```jsx import { Model, World } from "lingo3d-react"

function App() {

return (

  <Model
    src="Home.glb"
    scale={7}
  >
  </Model>
</World >

) }

export default App ```

结果如下

image.png

但大小需要调整,另外视角需要调整,可以使用第一人称或者第三人称相机来切换视角

使用第三人称相机 ThirdPersonCamera

使用ThirdPersonCamera表示已第三人称的角度看到人物,代码如下

```jsx import { Model, ThirdPersonCamera, World } from "lingo3d-react"

function App() {

return (

  <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 表示鼠标的控制

效果如下:

6.gif

但是此时小妹妹和房子捏在一起了,这是可以设置模型的物理属性

物理属性 physics

物理属性physics的值有很多,这里设置角色physics="character",设置房子physics="map"

  • character 表示主角
  • map 表示地图

代码如下:

```js import { Model, ThirdPersonCamera, World } from "lingo3d-react"

function App() {

return (

  <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 ```

效果如下

image.png

可以看到人物就和房子分离开了,但还有一个问题是:天空是黑色的

设置天空我们需要用到天空盒 Skybox

添加 Skybox

查找天空背景

查找天空背景可以google或者百度输关键字:equirectangular sky / skybox background

添加Skybox

然后再<World >中添加<Skybox texture="xxx" />,比如: html <Skybox texture="skybox.jpeg" />

这样就有天空啦,效果如下:

image.png

7.gif

但我发现人物有点浮在空中,应该是模型的物理碰撞结构的原因

我们将map换成map-debug检查一下

image.png

image.png

发现,这个模型里好多小雪花,被计算成碰撞体积了。

所以得调好人物的初始位置

调好之后再来换个场景,比如我看到下面这个场景感觉不错

image.png

接着我们将场景下载并导入

```jsx import { Model, Skybox, ThirdPersonCamera, World } from "lingo3d-react"

function App() {

return (

  <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 ```

显示结果如下:

image.png

image.png

接着我想让角色可以用键盘wsad控制上、下、左、右的移动

角色移动

首先得在mixamo准备好移动相关的动画,比如走路、跑步等

将人物模型的animation设置成对应的动画,比如animation="walking",这是就会用走路的动画了

8.gif

其他相关的动画也是这样设置

下面整理一下如何让角色移动的思路

  1. 初始是站立状态
  2. 当我们按下键盘wsad键时,触发走路动画,角色分别向前、后、左、右的移动;如果短时间连续按两下,触发跑步动画
  3. 当松开按键时,角色进入初始站立状态

实现前后移动

当按下w时,切换向前移动动画,并向前移动;当按下s键时,切换倒退动画,人物向后移动;

代码如下

```jsx 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 (

  <Model
    src="map/scene.gltf"
    scale={40}
    physics="map"
  />
  <Skybox texture="skybox.jpg" />
</World >

) }

export default App ``` 实现效果如下

按w时,向前 👇

9.gif

按a时,向后 👇

11.gif

左转、右转

左转右转的改变,可以通过鼠标来控制,即mouseControl属性

html <ThirdPersonCamera active mouseControl> 效果如下

13.gif

添加跑步动画

由于上面是用w表示走路,这里就用w加e的时候实现跑步吧,代码如下: ```jsx 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 (

  <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 ``` 效果如下

15.gif

接着,要不找点什么东西吧,比如收集七颗龙珠

收集七龙珠

引入7龙珠

首先得下载龙珠,步骤跟之前一样,之后引入

image.png

移动角色和龙珠到合适的位置

下面我希望将角色和龙珠的初始位置移动到合适的地方

可以通过model身上的x、y、z来控制,但是具体多少呢?可以使用Editor组件开启编辑模式,来测 jsx xxx return ( <> <World> </World > //开启编辑模式 <Editor/> ) 于是就可以看到以下的编辑模式,可以用鼠标进行移动

image.png

移动好之后可以记下右边的数据,主要position和rotation

比如

```jsx

```

寻找龙珠

我们可以通过w和s和w+e以及配合鼠标来控制角色移动来寻找龙珠,当寻找到龙珠时,即准心对准龙珠时,让龙珠有变化,比如高亮或者啥的

所以,首先弄个准心吧

准心

通过blender找到龙珠对应的名字

image.png

然后在龙珠模型中添加一个Find标签 ```jsx <Model id="ball" ref={ballRef} src="dragon_ball.fbx" physics="character" x={516.29} y={-1198.63} z={173.60} scale={.5}

``` 表示的是在模型中找到的东西,我们让他outline,以示区分,效果可以之后调

展示如下

image.png

但是默认情况,我们应该是FALSE,不显示的

这里引入状态来控制,如下代码 ```jsx //... const [mouseOver,setMouseOver] = useState(false); //....

{ setMouseOver(true) }} > ``` 这样的话,默认就是不显示发光特效,只有鼠标准心对准过后才发光,表示找到了的

接着我们需要添加准心,来方便找,通过Reticle ```jsx

//xxx ``` - color是准心的颜色; - variant是准心的形状,可以通过不同数字选择自己喜欢的形状

上述代码效果如下 👇

image.png

但是此时瞄准器在人物的屁股上,希望的是瞄准器在人物的头顶上,相当于是人物的目光

所以我的调整相机的位置,让相机升到人物的头顶上去

调整相机位置

其实可以通过相机的innerX、innerY、innerZ来设置,如下代码

jsx <ThirdPersonCamera active mouseControl innerY={66}>

效果如下

image.png

找到龙珠

当找到龙珠并点击时,龙珠亮

代码如下

```jsx <Model id="ball" ref={ballRef} src="dragon_ball.fbx" physics="character" x={516.29} y={-1198.63} z={173.60} scale={.5}

{ setMouseOver(true) }} > ``` 效果如下

16.gif

类似的方式将其他龙珠添加到合适的位置

另一种寻找方式

除了看到点击可以点亮龙珠以外,当角色和龙珠碰撞时也应点亮龙珠

那首先要检测角色碰撞

步骤如下: 1. 先给龙珠添加一个id 2. 给角色添加intersectID属性,值为一个数组,数组的每一项是每个龙珠的id名 3. 再给角色添加onIntersect属性,值为一个回调函数,这个回调函数会在角色和龙珠发生碰撞时触发

所以,需要做的事情都是这个回调函数中执行。

这步之后加上吧

神龙出现

神龙出现,小龙消失

当所有龙珠都找到时,地图上某个地方就会出现龙,你需要找到它

image.png

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

image.png

神龙许愿

点击神龙,过一会就会送我回到家里,并且回去之后可以看到7颗龙珠

完整的游戏过程

1. 通过按键和鼠标控制角色移动

w键:向前跑

20.gif

s键:向后走

21.gif

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

23.gif

空格:跳跃

22.gif

2. 寻找七龙珠

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

24.gif

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

image.png

3. 寻找龙

当提示地图某处出现龙时,就去寻找龙

此龙如图所示

image.png

但是到这没有结束,此龙非真的神龙

4. 真神龙出现

点击小龙,小龙会消失,真的神龙出现

image.png

image.png

点击神龙,一会就会实现回家的愿望

5. 回到家

到这就会回到家了,如下

image.png

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

image.png

最后

最后能回到家,肯定开心啦

所以按住d键,开始跳舞吧

25.gif

结语

以上就是本文的所有内容啦,源码这两天完善后回上传github,敬请期待~