如何在一小时内用 React 编写出简单的小游戏?

点击上方“

CSDN

”,选择“置顶公众号”

关键时刻,第一时间送达!

最近有个很火的视频叫做“5 分钟编写贪吃蛇”。视频很不错,这种快速编程的方法也很有意思,所以我决定自己也做一个。



如何在一小时内用 React 编写出简单的小游戏?

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

某个格

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

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

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

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

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

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

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

设置 React 环境

首先需要设置 React 环境。

通过 create-react-app(

http://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 上启动一个开发服务器,并且会自动启动浏览器打开该地址。

实现过程

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

如何在一小时内用 React 编写出简单的小游戏?

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

看起来很简单吧?想一想在 React 中怎么做.

必须明确的是,React 不是图形框架,所以这里不会使用 canvas。

如果想用canvas做,可以参考下PIXI(http://www.pixijs.com/)或Phaser(http://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上分享的完整代码 http://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

>

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

如何在一小时内用 React 编写出简单的小游戏?

创建表示生命的方块

下一步我们要允许用户通过点击棋盘的方式来创建方块。下面的代码中使用 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;

}

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

如何在一小时内用 React 编写出简单的小游戏?

运行游戏

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

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=http://www.gunmi.cn/v/{

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 按钮。

如何在一小时内用 React 编写出简单的小游戏?

现在点击 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() 的代码,源代码在这里:

    http://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 按钮,就能看到漂亮的动画了!

    如何在一小时内用 React 编写出简单的小游戏?

    总结

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

    http://github.com/charlee/react-gameoflife。



    如何在一小时内用 React 编写出简单的小游戏?


    如何在一小时内用 React 编写出简单的小游戏?