点击上方“CSDN”,选择“置顶公众号”
关键时刻,第一时间送达!
最近有个很火的视频叫做“5 分钟编写贪吃蛇”。视频很不错,这种快速编程的方法也很有意思,所以我决定自己也做一个。
我小时候刚开始接触编程时学过一个游戏叫做“康威生命游戏”。它是一个简单的元胞自动机的例子,只需几条非常简单的规则,就可以演化出极其复杂的变化。其内容是,在一个格子棋盘上有许多生命,每个回合这些生命按照一定的规则繁殖或死亡:
某个格子的“相邻”格子指它周围的八个格子;
如果一个生命的相邻的格子中包含少于两个生命,则该生命下一回合死亡(人口过少孤独而死);
如果一个生命的相邻格子中包含两个或三个生命,则该生命下一回合存活;
如果一个生命的相邻格子中包含三个以上生命,则该生命下一回合死亡(过于拥挤);
如果一个空格子的相邻格子中包含正好三个生命,则该格子下一回合产生一个生命(繁殖)。
不算第一条关于“相邻”的定义,我们只有四条非常简单的规则。游戏的图像显示很也简单,只是方格的颜色变化而已,所以不需要操作 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 还没有任何效果,因为我们还没有写游戏规则。下面就开始写游戏规则吧。
这个游戏中,每个回合都会更新棋盘状态。因此我们需要一个方法 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。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved