關鍵時刻,第一時間送達!
最近有個很火的視頻叫做“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/)。
整個棋盤可以做成一個組件,並渲染成一個
至於生命則可以用
第一步:棋盤
首先來畫棋盤。在 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 (); }}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 行:
刷新瀏覽器就能看到漂亮的格子。
創建表示生命的方塊
下一步我們要允許用戶通過點擊棋盤的方式來創建方塊。下面的代碼中使用 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 可以給
向 render() 方法中添加以下事件處理函數。我們同時還保存了棋盤元素的引用,以便稍後獲取棋盤的位置。
render() { return ();}{ this.boardRef = n; }}>
還需要再加幾個函數。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 ( ); }}class Game extends React.Component { ... render() { const { cells } = this.state; return (); } ...}{ this.boardRef = n; }}> {cells.map(cell => ())} |
別忘了給 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 ( ...Update every msec {isRunning ? : }... ); }}
這些代碼會在頁面底部添加一個時間間隔輸入框,以及一個 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。
閱讀更多 CSDN 的文章