用React 编写出简单的小游戏

来源:程序思维浏览:2041次
我小时候刚开始接触编程时学过一个游戏叫做“康威生命游戏”。它是一个简单的元胞自动机的例子,只需几条非常简单的规则,就可以演化出极其复杂的变化。其内容是,在一个格子棋盘上有许多生命,每个回合这些生命按照一定的规则繁殖或死亡:

某个格子的“相邻”格子指它周围的八个格子;

如果一个生命的相邻的格子中包含少于两个生命,则该生命下一回合死亡(人口过少孤独而死);

如果一个生命的相邻格子中包含两个或三个生命,则该生命下一回合存活;

如果一个生命的相邻格子中包含三个以上生命,则该生命下一回合死亡(过于拥挤);

如果一个空格子的相邻格子中包含正好三个生命,则该格子下一回合产生一个生命(繁殖)。

不算第一条关于“相邻”的定义,我们只有四条非常简单的规则。游戏的图像显示很也简单,只是方格的颜色变化而已,所以不需要操作 canvas,用 React就可以很容易地做出来。

如此说来这篇文章也可以算作一篇简单的 React 入门教程。让我们开始吧!

不算第一条关于“相邻”的定义,我们只有四条非常简单的规则。游戏的图像显示很也简单,只是方格的颜色变化而已,所以不需要操作 canvas,用 React就可以很容易地做出来。

如此说来这篇文章也可以算作一篇简单的 React 入门教程。让我们开始吧!

设置 React 环境

首先需要设置 React 环境。

通过 create-react-app(https://github.com/facebook/create-react-app)来创建 React 项目非常方便:

$ npm install -g create-react-app
$ create-react-app react-gameoflife

不到一分钟的时间,react-gameoflife 就创建好了。接下来只需要启动它:

$ cd react-gameoflife
$ npm start

这条命令将在 http://localhost:3000 上启动一个开发服务器,并且会自动启动浏览器打开该地址。

实现过程

我们需要实现的最终游戏画面如下所示:

最终游戏画面

一个简单的格子棋盘,加上一些白色的方块(生命),点击格子可以放置或移除方块。Run 按钮可以按照给定的时间间隔开始回合迭代。

看起来很简单吧?想一想在 React 中怎么做.必须明确的是,React 不是图形框架,所以这里不会使用 canvas。

如果想用canvas做,可以参考下PIXI(http://www.pixijs.com/)或Phaser(https://phaser.io/)。

整个棋盘可以做成一个组件,并渲染成一个<div>。格子怎么办呢?我们不能用一个个<div>来画格子,那样效率太低,而且由于格子是静态的,这样做也没必要。实际上可以用CSS3的linear-gradient画格子。

至于生命则可以用<div>来画。我们将其做成独立的组件,它接收参数x, y,以确定它在棋盘上的位置。

第一步:棋盘

首先来画棋盘。在 src 目录下创建一个文件名为 Game.js,内容如下:

import React from 'react';
import './Game.css';
const CELL_SIZE = 20;
const WIDTH = 800;
const HEIGHT = 600;
class Game extends React.Component {
  render() {
    return (
      <div>
        <div className="Board"
          style={{ width: WIDTH, height: HEIGHT }}>
        </div>
      </div>
    );
  }
}
export default Game;

还需要 Game.css 来定义样式:

.Board {
  position: relative;
  margin: 0 auto;
  background-color: #000;
}

更新 App.js 导入 Game.js 并将 Game 组件显示出来(代码省略,请参见我在GitHub上分享的完整代码 https://github.com/charlee/react-gameoflife)。现在就能看到一个全黑的棋盘了。

下一步是画格子。只需要一行 linear-gradient 就可以做到(加到 Game.css 中):

background-image:
    linear-gradient(#333 1px, transparent 1px),
    linear-gradient(90deg, #333 1px, transparent 1px);

其实为了让格子能正确显示,我们还得定义 background-size 样式。但由于 Game.js 中定义了 CELL_SIZE 常量,我们希望能通过该常量来定义格子大小,而不是写死在 CSS 中,所以可以用行内样式来直接定义背景大小。

修改 Game.js 中的 style 行:

<div className="Board"
  style={{ width: WIDTH, height: HEIGHT,
    backgroundSize: `${CELL_SIZE}px ${CELL_SIZE}px`}}
></div>

刷新浏览器就能看到漂亮的格子。

漂亮的格子

创建表示生命的方块

下一步我们要允许用户通过点击棋盘的方式来创建方块。下面的代码中使用 this.board 二维数组来保存棋盘状态,this.state.cells 数组保存生命的位置列表。棋盘状态更新后,调用 this.makeCells() 根据棋盘状态生成新的生命位置列表。

向 Game 类添加以下代码:

class Game extends React.Component {
  constructor() {
    super();
    this.rows = HEIGHT / CELL_SIZE;
    this.cols = WIDTH / CELL_SIZE;
    this.board = this.makeEmptyBoard();
  }
  state = {
    cells: [],
  }
  // Create an empty board
  makeEmptyBoard() {
    let board = [];
    for (let y = 0; y < this.rows; y++) {
      board[y] = [];
      for (let x = 0; x < this.cols; x++) {
        board[y][x] = false;
      }
    }
    return board;
  }
  // Create cells from this.board
  makeCells() {
    let cells = [];
    for (let y = 0; y < this.rows; y++) {
      for (let x = 0; x < this.cols; x++) {
        if (this.board[y][x]) {
          cells.push({ x, y });
        }
      }
    }
    return cells;
  }
  ...
}

下一步要允许用户通过点击棋盘的方式添加或删除生命。React 可以给 <div> 指定 onClick 事件处理函数,该函数可以通过点击事件的属性来获得点击发生的坐标。但问题是这个事件的坐标是相对于整个客户端区域(即浏览器的可视区域)的,所以需要一些额外的代码将其转换成相对于棋盘的坐标。

向 render() 方法中添加以下事件处理函数。我们同时还保存了棋盘元素的引用,以便稍后获取棋盘的位置。

render() {
  return (
    <div>
      <div className="Board"
        style={{ width: WIDTH, height: HEIGHT,
          backgroundSize: `${CELL_SIZE}px ${CELL_SIZE}px`}}
        onClick={this.handleClick}
        ref={(n) => { this.boardRef = n; }}>
      </div>
    </div>
  );
}

还需要再加几个函数。getElementOffset() 计算棋盘元素的位置。handleClick() 获取点击的位置,转换成相对坐标,再计算被点击的格子所在的行和列。然后反转相应格子的状态。

class Game extends React.Component {
  ...
  getElementOffset() {
    const rect = this.boardRef.getBoundingClientRect();
    const doc = document.documentElement;
    return {
      x: (rect.left + window.pageXOffset) - doc.clientLeft,
      y: (rect.top + window.pageYOffset) - doc.clientTop,
    };
  }
  handleClick = (event) => {
    const elemOffset = this.getElementOffset();
    const offsetX = event.clientX - elemOffset.x;
    const offsetY = event.clientY - elemOffset.y;
    const x = Math.floor(offsetX / CELL_SIZE);
    const y = Math.floor(offsetY / CELL_SIZE);
    if (x >= 0 && x <= this.cols && y >= 0 && y <= this.rows) {
      this.board[y][x] = !this.board[y][x];
    }
    this.setState({ cells: this.makeCells() });
  }
  ...
}

最后,要将 this.state.cells 中方格渲染出来:

class Cell extends React.Component {
  render() {
    const { x, y } = this.props;
    return (
      <div className="Cell" style={{
        left: `${CELL_SIZE * x + 1}px`,
        top: `${CELL_SIZE * y + 1}px`,
        width: `${CELL_SIZE - 1}px`,
        height: `${CELL_SIZE - 1}px`,
      }} />
    );
  }
}
class Game extends React.Component {
  ...
  render() {
    const { cells } = this.state;
    return (
      <div>
        <div className="Board"
          style={{ width: WIDTH, height: HEIGHT,
            backgroundSize: `${CELL_SIZE}px ${CELL_SIZE}px`}}
          onClick={this.handleClick}
          ref={(n) => { this.boardRef = n; }}>
          {cells.map(cell => (
            <Cell x={cell.x} y={cell.y}
                key={`${cell.x},${cell.y}`}/>
          ))}
        </div>
      </div>
    );
  }
  ...
}

别忘了给 Cell 组件加一些样式(Game.css):

.Cell {
  background: #ccc;
  position: absolute;
}

刷新浏览器,试着点一下棋盘。现在可以添加或删除生命了!

棋盘

运行游戏

我们需要一些辅助的东西来运行游戏。首先添加一些控制元素。

class Game extends React.Component {
  state = {
    cells: [],
    interval: 100,
    isRunning: false,
  }
  ...
  runGame = () => {
    this.setState({ isRunning: true });
  }
  stopGame = () => {
    this.setState({ isRunning: false });
  }
  handleIntervalChange = (event) => {
    this.setState({ interval: event.target.value });
  }
  render() {
    return (
      ...
        <div className="controls">
          Update every <input value={this.state.interval}
              onChange={this.handleIntervalChange} /> msec
          {isRunning ?
            <button className="button"
              onClick={this.stopGame}>Stop</button> :
            <button className="button"
              onClick={this.runGame}>Run</button>
          }
        </div>
      ...
    );
  }
}

这些代码会在页面底部添加一个时间间隔输入框,以及一个 Run 按钮。

Run 按钮

现在点击 Run 还没有任何效果,因为我们还没有写游戏规则。下面就开始写游戏规则吧。

这个游戏中,每个回合都会更新棋盘状态。因此我们需要一个方法 runIteration(),该方法将以固定的时间间隔调用,比如每 100 毫秒调用一次。这可以通过 window.setTimeout() 实现。

点击 Run 按钮将调用 runIteration() 方法。该方法在结束之前会调用 window.setTimeout(),设置在 100ms 之后重新运行自己。这样 runIteration() 将反复执行。点击 Stop 按钮会调用 window.clearTimeout() 取消安排好的执行,这样就能打断反复执行。

class Game extends React.Component {
  ...
  runGame = () => {
    this.setState({ isRunning: true });
    this.runIteration();
  }

  stopGame = () => {
    this.setState({ isRunning: false });
    if (this.timeoutHandler) {
      window.clearTimeout(this.timeoutHandler);
      this.timeoutHandler = null;
    }
  }
  runIteration() {
    console.log('running iteration');
    let newBoard = this.makeEmptyBoard();
    // TODO: Add logic for each iteration here.
    this.board = newBoard;
    this.setState({ cells: this.makeCells() });
    this.timeoutHandler = window.setTimeout(() => {
      this.runIteration();
    }, this.state.interval);
  }
  ...
}

刷新浏览器并点击“Run”按钮。我们可以在控制台(按 Ctrl-Shift-I 可以调出控制台)中看到“running iteration”的调试信息。

接下来需要给runIteration()方法添加代码以实现游戏规则。回想一下我们的游戏规则:

  • 如果一个生命的相邻的格子中包含少于两个生命,则该生命下一回合死亡。

  • 如果一个生命的相邻格子中包含两个或三个生命,则该生命下一回合存活。

  • 如果一个生命的相邻格子中包含三个以上生命,则该生命下一回合死亡。

  • 如果一个空格子的相邻格子中包含正好三个生命,则该格子下一回合产生一个生命。

我们可以写一个方法 calculateNeighbors() 来计算给定 (x, y) 的相邻格子中的生命数量。

这里省略了 calculateNeighbors() 的代码,源代码在这里:

https://github.com/charlee/react-gameoflife/blob/master/src/Game.js#L134

然后规则就很容易实现了:

for (let y = 0; y < this.rows; y++) {
  for (let x = 0; x < this.cols; x++) {
    let neighbors = this.calculateNeighbors(this.board, x, y);
    if (this.board[y][x]) {
      if (neighbors === 2 || neighbors === 3) {
        newBoard[y][x] = true;
      } else {
        newBoard[y][x] = false;
      }
    } else {
      if (!this.board[y][x] && neighbors === 3) {
        newBoard[y][x] = true;
      }
    }
  }
}

刷新浏览器,放置一些生命,然后点击 Run 按钮,就能看到漂亮的动画了!

漂亮的动画

总结

最后的项目里我还加了个 Random 和 Clear 按钮,让操作更容易些。完整的代码可以在我的 GitHub 上找到:https://github.com/charlee/react-gameoflife。

精品好课
jQuery视频教程从入门到精通
jquery视频教程从入门到精通,课程主要包含:jquery选择器、jquery事件、jquery文档操作、动画、Ajax、jquery插件的制作、jquery下拉无限加载插件的制作等等......
最新完整React+VUE视频教程从入门到精,企业级实战项目
React和VUE是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React和VUE并应用到实战,教你如何解决内存泄漏,常用库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习Re...
HTML5基础入门视频教程易学必会
HTML5基础入门视频教程,教学思路清晰,简单易学必会。适合人群:创业者,只要会打字,对互联网编程感兴趣都可以学。课程概述:该课程主要讲解HTML(学习HTML5的必备基础语言)、CSS3、Javascript(学习...
VUE2+VUE3视频教程从入门到精通(全网最全的Vue课程)
VUE是目前最火的前端框架之一,就业薪资很高,本课程教您如何快速学会VUE+ES6并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习VUE高薪就...
React实战视频教程仿京东移动端电商
React是前端最火的框架之一,就业薪资很高,本课程教您如何快速学会React并应用到实战,对正在工作当中或打算学习React高薪就业的你来说,那么这门课程便是你手中的葵花宝典。
HTML5视频播放器video开发教程
适用人群1、有html基础2、有css基础3、有javascript基础课程概述手把手教你如何开发属于自己的HTML5视频播放器,利用mp4转成m3u8格式的视频,并在移动端和PC端进行播放支持m3u8直播格式,兼容...
Vue2+Vue3+ES6+TS+Uni-app开发微信小程序从入门到实战视频教程
2021年最新Vue2+Vue3+ES6+TypeScript和uni-app开发微信小程序从入门到实战视频教程,本课程教你如何快速学会VUE和uni-app并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己...
最新完整React视频教程从入门到精通纯干货纯实战
React是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习React高薪就...
收藏
扫一扫关注我们