如何在一小時內用 React 編寫出簡單的小遊戲?

關鍵時刻,第一時間送達!

最近有個很火的視頻叫做“5 分鐘編寫貪吃蛇”。視頻很不錯,這種快速編程的方法也很有意思,所以我決定自己也做一個。

如何在一小時內用 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 上啟動一個開發服務器,並且會自動啟動瀏覽器打開該地址。

實現過程

我們需要實現的最終遊戲畫面如下所示:

如何在一小時內用 React 編寫出簡單的小遊戲?

一個簡單的格子棋盤,加上一些白色的方塊(生命),點擊格子可以放置或移除方塊。Run 按鈕可以按照給定的時間間隔開始回合迭代。

看起來很簡單吧?想一想在 React 中怎麼做.必須明確的是,React 不是圖形框架,所以這裡不會使用 canvas。

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

整個棋盤可以做成一個組件,並渲染成一個

。格子怎麼辦呢?我們不能用一個個
來畫格子,那樣效率太低,而且由於格子是靜態的,這樣做也沒必要。實際上可以用CSS3的linear-gradient畫格子。

至於生命則可以用

來畫。我們將其做成獨立的組件,它接收參數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 (  
); }}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 行:

刷新瀏覽器就能看到漂亮的格子。

如何在一小時內用 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 可以給

指定 onClick 事件處理函數,該函數可以通過點擊事件的屬性來獲得點擊發生的座標。但問題是這個事件的座標是相對於整個客戶端區域(即瀏覽器的可視區域)的,所以需要一些額外的代碼將其轉換成相對於棋盤的座標。

向 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;}

刷新瀏覽器,試著點一下棋盤。現在可以添加或刪除生命了!

如何在一小時內用 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 ( ... 
Update every msec {isRunning ? : }
... ); }}

這些代碼會在頁面底部添加一個時間間隔輸入框,以及一個 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() 的代碼,源代碼在這裡:

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 按鈕,就能看到漂亮的動畫了!

如何在一小時內用 React 編寫出簡單的小遊戲?

總結

最後的項目裡我還加了個 Random 和 Clear 按鈕,讓操作更容易些。完整的代碼可以在我的 GitHub 上找到:https://github.com/charlee/react-gameoflife。


分享到:


相關文章: