用 Three.js, React 和 WebGL 开发游戏 — SitePoint

3,050 阅读7分钟
原文链接: www.zcfy.cc

我正在制作一款名为 “Charisma The Chameleon” 的游戏,它使用 Three.js,React 和 WebGL 开发。这是一篇使用 react-three-renderer (简称 R3R) 结合这些框架的介绍。

SitePoint 上有关于 React 和 WebGL 的介绍。查阅请访问: WebGL 入门手册React + JSX 起步。这两篇文章及附带的源码使用的是 ES6 语法.

手里有一片红色的药丸和一片蓝色的药丸,手臂上趴着一条变色龙

如何开始

前段时间, Pete Hunt 在 IRC #reactjs 板块说了一个关于 React 开发游戏的笑话:

我打赌,我们能用 React 开发第一人称射击游戏!

敌人有 `` 之类.

我和他都笑了,每个人都很开心。“到底谁会那样做?”我想知道。

一年后,这就是我正在做的事情。

魅力变色龙 游戏图片

魅力变色龙 ,是一款通过收集“能量电源”,使自己收缩以解决无尽的分形迷宫的游戏。作为一个有多年经验的 React 开发者,我很好奇是否有一种方式能够很好的在 React 中使用 Three.js。这时候,R3R 吸引了我的眼球。

为什么用 React?

我知道你在想什么:为什么?让我幽默一会。下面是考虑使用 React 驱动 3D 场景的几个理由:

  • “声明“视图让你能很清晰的把场景渲染从游戏逻辑里分离出来。

  • 组件设计起来很容易, 比如 ,, ``, 之类。
  • 游戏资源热加载。实时更新场景中纹理和模型的变化!

  • 使用浏览器工具(如:Chrome inspector),像标记一样检查和调试 3D 场景。

  • 使用 Webpack 依赖管理游戏资源,例: ``

让我们来搭建一个场景,搞明白它们是如何工作的。

推荐课程

初学者学习 React 最好的方式 Wes Bos 一个循序渐进的训练课程,让你花几个下午的时间,就能够使用 React.js + Firebase 搭建真实的网站和应用。付款时使用优惠码'SITEPOINT' 获得 25% 的折扣

React 和 WebGL

伴随这篇文章,我建了一个 GitHub 示例仓库。克隆仓库,参照 README.md 里的指令运行代码并跟着来。它启动了 SitePointy 3D 机器人!

SitePointy 3D 机器人截图

警告: R3R 还是测试版,它的 API 还不太稳定,而且将来可能会更改;目前只处理 Three.js 的子集。在我看来它完全足够去开发一个完整的游戏,但是因个人可能会有差异。

组织视图代码

使用 React 驱动 WebGL 最大的好处,是能够讲我们的视图代码与游戏逻辑 解耦,这意味着我们所呈现的实体可以是很方便导出的小的组件。

R3R 通过包裹 Three.js,暴露了一个声明的 API。举个例子,我们可以这样写:

<scene>
  <perspectiveCamera
    position={ new THREE.Vector3( 1, 1, 1 )
  />
</scene>

现在,我们有了一个空的 3D 场景和相机。在场景里添加网格,就像引入 component, and give it 和 `` 一样简单。

<scene>
  …
  <mesh>
    <boxGeometry
      width={ 1 }
      height={ 1 }
      depth={ 1 }
    />
    <meshBasicMaterial
      color={ 0x00ff00 }
    />
</mesh>

在代码底层,它创建了一个 THREE.Scene(场景),并通过 THREE.BoxGeometry 自动添加了网格。 R3R 会处理场景的变化。如果你添加一个网格到场景中,原网格不会被重建。就像普通的 React DOM 一样,3D 场景 只更新有差异的地方

因为使用 React 开发,我们可以把游戏整体分离至组件文件。示例仓库里的 Robot.js 文件 演示了如何用纯 React 视图代码去表示主角。它是一个 “无状态组件”, 意味着它没有自己的状态要管理:

const Robot = ({ position, rotation }) => <group
  position={ position }
  rotation={ rotation }
>
  <mesh rotation={ localRotation }>
    <geometryResource
      resourceId="robotGeometry"
    />
    <materialResource
      resourceId="robotTexture"
    />
  </mesh>
</group>;

现在,我们将 `` 加载到 3D 场景中来!

<scene>
  …
  <mesh>…</mesh>
  <Robot
    position={…}
    rotation={…}
  />
</scene>

您可以在 R3R GitHub 仓库 中看到更多关于 API 的示例,或者在 附带工程 中查看完整的示例代码。

组织代码逻辑

问题的第二部分是处理游戏逻辑。咱们来给 SitePointy 机器人加一些简单的动画。

SitePointy 有生命的时间

传统游戏是如何工作的?它们接收用户输入、分析现有的 “游戏世界的状态”,然后返回新的状态用来渲染。为了方便起见,咱们将“游戏状态”对象保存到组件里。在更成熟的工程中,建议将游戏状态放到 Redux 或 Flux 的 store 里。

我们使用浏览器的 requestAnimationFrame API 回调作为游戏循环的驱动函数,并在 GameContainer.js 中运行。为了让机器人动起来,我们要基于传给requestAnimationFrame的时间戳来计算新的位置, 然后将新位置保存到状态里。

// …
gameLoop( time ) {
  this.setState({
    robotPosition: new THREE.Vector3(
      Math.sin( time * 0.01 ), 0, 0
    )
  });
}

调用 setState() 触发子组件重绘,并且 3D 场景会更新。我们将状态从容器组件传给展示组件:

render() {
  const { robotPosition } = this.state;
  return <Game
    robotPosition={ robotPosition }
  />;
}

咱们可以使用一个非常有用的模式来帮助组织这些代码。更新机器人的位置是很简单的基于时间的计算,将来,也会考虑用来从前一个游戏状态里记录之前的机器人位置。一个函数接收数据并处理,然后返回新的数据,通常被称为 reducer。我们可以把移动位置的代码抽象至 reducer 函数中!

现在我们可以写一个干净、简单的游戏循环,只有函数的调用在里面:

import robotMovementReducer from './game-reducers/robotMovementReducer.js';

// …

gameLoop() {
  const oldState = this.state;
  const newState = robotMovementReducer( oldState );
  this.setState( newState );
}

为了给游戏循环添加更多的逻辑(比如处理物理现象),需要创建另外一个 reducer 函数,然后将它的结果传给前一个函数:

`const newState = physicsReducer( robotMovementReducer( oldState ) );`

由于游戏引擎不断的变得复杂,将游戏逻辑组织到分离的函数中很关键。这种组织在 reducer 模式里会很简单。

资源管理

这仍然是 R3R 不断进化的部分。纹理组件需要在 JSX 标签上指定 url 属性,使用 webpack,可以直接通过本地路径引入图片:

`<texture url={ require( '../local/image/path.png' ) } />`

基于这种方式,如果修改了本地图片,您的 3D 场景将会热更新!对于快速迭代游戏设计和内容来说,这是非常有帮助的。

至于另外的资源,如 3D 模型,您可能还是需要使用 Three.js 内置的组件来处理它们;比如 JSONLoader. 我曾尝试使用一个定制的 webpack 加载器来载入 3D 模型文件,但是最后做了很多工作却没有带来好处。使用 file-loader 将模型作为二进制数据处理并载入它们会更容易一些,它还会为模型数据提供热更新。您在 示例代码 中可以看到。

调试

R3R 同时支持支持 ChromeFirefox 的 React 开发者工具扩展。如果场景是普通 DOM 元素的话,您可以检查它!通过移动鼠标到检查器的组件上,显示场景的边界框。您还可以移动鼠标到纹理的定义上,以查看场景中的哪一个物体使用了这个纹理。

使用 react-three-renderer 和 React 开发工具调试

您同时也可以加入 react-three-renderer Gitter 聊天室,寻找应用调试相关的帮助。

性能注意事项

通过构建 “魅力变色龙”,我碰到了一些因工作流造成的性能问题:

  • 我的 Webpack 热重载时间 长达 30 秒!这是因为在重载的时候,大量的资源被重写了。解决方案是实施 Webpack’s DLLPlugin,可以将重载时间减少至 5 秒以内。
  • 理想情况下,场景在渲染时候,每一帧的只能调用 一次 setState()。 通过分析我的游戏,React 自身是最主要的瓶颈,每一帧调用 setState() 超过一次会导致双重渲染,并且会降低性能。
  • 当超过一定数量的物体时,R3R 会比普通的 Three.js 代码表现更糟。在我的工程里,大概是 1000 个物体。你可以通过 “Benchmarks” 示例代码 来比较 R3R 和 Three.js 的区别。

Chrome DevTools 的 Timeline 功能,对于调试性能来说是一款非常好的工具。用它可以很容易直观的检查你的游戏循环,相比于“Profile” 功能,它也更具有可读性。

这就行了!

查看 魅力变色龙 以了解这个工程的实现。即使这个工具还很年轻,我发现 React 和 R3R 完全可以干净的组织游戏代码。你也可以查看这个还在不断增加的 R3R 示例页面 去查看有组织的代码示例。

这篇文章由 Mark BrownKev Zettler 审稿。感谢 SitePoint 上所有的审稿人,是你们让 SitePoint 内容质量更高!

了解作者

Andrew Ray

Hello!我是来自 Bay Area 的软件工程师。