如何在一小時內用 React 編寫出簡單的小遊戲?
點擊上方「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"
stylex={{ 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"
stylex={{ 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"
stylex={{ 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" stylex={{
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"
stylex={{ 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。
※雅虎輝煌不再,紫色血液永存
※IBM 推出世界最小電腦,應用區塊鏈技術防偷騙!
TAG:CSDN |