ReactJS實戰之簡易彈球遊戲的實現
這一篇記錄的是使用ReactJS完成一個簡易的彈球遊戲,遊戲在瀏覽器中執行的效果圖如下所示:
滑鼠在遊戲面板中左右移動控制擋板的水平移動。下面一步一步實現這個簡單的遊戲。
首先,我們需要知道如何在瀏覽器中繪圖,這裡的小球和擋板,都是通過繪圖畫出來的,瀏覽器中的繪圖主要使用了html5的canvas標籤,定義一個canvas標籤並指定大小,然後獲取canvas的context,通過context物件完成繪圖,示例程式碼如下:
為了畫出小球,我們需要使用context物件的arc方法,該方法的使用方法如下:<canvas id="myCanvas"></canvas> <script type="text/javascript"> var canvas=document.getElementById('myCanvas'); var ctx=canvas.getContext('2d'); ctx.fillStyle='#FF0000'; ctx.fillRect(0,0,80,100); </script>
context.arc(x,y,r,sAngle,eAngle,counterclockwise);
引數 | 描述 |
---|---|
x | 圓的中心的 x 座標。 |
y | 圓的中心的 y 座標。 |
r | 圓的半徑。 |
sAngle | 起始角,以弧度計。(弧的圓形的三點鐘位置是 0 度)。 |
eAngle | 結束角,以弧度計。 |
counterclockwise | 可選。規定應該逆時針還是順時針繪圖。False = 順時針,true = 逆時針。 |
下面放上關於小球的JavaScript程式碼:
上面使用function定義了一個Ball類,構造方法裡傳入的三個引數分別是小球的起始x,y座標和小球的半徑,注意這裡的x,y不是小球的中心點的座標,而是小球對應的矩形的左上角的座標。Ball類中還有originX和originY兩個變數,代表小球的初始位置,這兩個量用於在遊戲結束後重置小球的位置,speedX和speedY代表小球在水平和垂直方向上的速度,drawMe方法是畫小球的方法,需要傳入一個context引數,代表canvas中的context物件,move方法是移動小球的方法,即讓小球的當前x,y座標變為上次x,y座標加上速度值,因為小球的運動就是通過不斷的改變小球的座標,然後不停地重繪畫布來實現的,reset方法是重置小球位置的方法,當遊戲結束重新開始時,需要呼叫這個方法重置小球位置。//小球類 function Ball(x, y, radius) { this.x = x; this.y = y; this.radius = radius; this.originX = x; this.originY = y; this.speedX = 5; this.speedY = 5; //畫小球的方法 this.drawMe = function(context) { context.beginPath(); context.fillStyle = "#000000"; context.arc(this.x + this.radius, this.y + this.radius, this.radius, 0, 2 * Math.PI); context.closePath(); context.fill(); } //小球移動 this.move = function() { this.x += this.speedX; this.y += this.speedY; } //小球位置重置 this.reset = function() { this.x = this.originX; this.y = this.originY; this.speedX = 5; this.speedY = 5; } }
畫完了小球,再需要畫擋板了,擋板就是一個矩形,比較好畫,使用的是context物件中的fillRect()函式,fillRect()的用法如下:
context.fillRect(x,y,width,height);
引數 | 描述 |
---|---|
x | 矩形左上角的 x 座標 |
y | 矩形左上角的 y 座標 |
width | 矩形的寬度,以畫素計 |
height | 矩形的高度,以畫素計 |
下面放上擋板類的JavaScript程式碼:
//擋板類
function Board(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.originX = x;
this.originY = y;
//畫擋板
this.drawMe = function(context) {
context.fillStyle = "#000000";
context.fillRect(this.x, this.y, this.width, this.height);
}
//重置擋板的位置
this.reset = function() {
this.x = this.originX;
this.y = this.originY;
}
}
擋板類的程式碼和小球類的程式碼很類似,構造方法傳入的四個引數分別是擋板的初始位置x,y座標和擋板的寬高,這裡就不細說了。
下面到了一個非常重要的時刻了,就是畫遊戲介面,遊戲介面是一個大的畫布,裡面處理小球的繪製,擋板的繪製和介面的不停重新整理,下面直接上程式碼:
//遊戲面板
var GameView = React.createClass({
getInitialState: function() { /** 生命週期方法,這裡做一些初始化 */
//建立一個小球
var ballRadius = this.props.width / 30;
var ballX = this.props.width / 2 - ballRadius;
var ball = new Ball(ballX, 50, ballRadius);
//建立一個擋板
var boardWidth = this.props.width / 6;
var boardHeight = 10;
var boardX = (this.props.width - boardWidth) / 2;
var boardY = this.props.height - 100;
var board = new Board(boardX, boardY, boardWidth, boardHeight);
//返回state
return {
ball: ball,
board: board,
started: false,
stateCode: 0,
};
},
render: function() {
switch(this.state.stateCode) {
case -1:
return this.renderGameOverView();
break;
case 0:
return this.renderStartView();
break;
case 1:
return this.renderGameView();
break;
}
},
renderStartView: function() { /** 渲染遊戲開始的檢視 */
return (
<button className="start-game-btn" onClick={this.handleStartGameBtnClick}>開始遊戲</button>
);
},
renderGameView: function() { /** 渲染遊戲面板 */
return (
<canvas id="canvas"
onMouseMove={this.handleMouseMove}
width={this.props.width}
height={this.props.height} />
);
},
renderGameOverView: function() { /** 渲染遊戲結束的檢視 */
return (
<div>
<p>遊戲結束</p>
<button className="start-game-btn" onClick={this.handleStartGameBtnClick}>開始遊戲</button>
</div>
);
},
componentDidUpdate: function() { /** 生命週期方法,元件更新完成後呼叫,可呼叫多次 */
if(this.state.stateCode == 1) {
this.state.context = document.getElementById('canvas').getContext('2d');
console.log('did update, start game...');
this.startGame();
}
},
clear: function() { /** 清除遊戲區域的背景 */
this.state.context.clearRect(0, 0, this.props.width, this.props.height);
},
refreshGameView: function() { /** 重新整理遊戲區域 */
//每次重新整理前都需要清除背景,不然小球和擋板上次的位置會被保留
this.clear();
this.state.ball.move();
//判斷move後的小球位置
var ballX = this.state.ball.x;
var ballY = this.state.ball.y;
if(ballX < 0) {
this.state.ball.x = 0;
this.state.ball.speedX *= -1;
}
if(ballY < 0) {
this.state.ball.y = 0;
this.state.ball.speedY *= -1;
}
if(ballX + 2 * this.state.ball.radius > this.props.width) {
this.state.ball.x = this.props.width - 2 * this.state.ball.radius;
this.state.ball.speedX *= -1;
}
var ballBottomY = this.state.ball.y + 2 * this.state.ball.radius;
var ballCenterX = this.state.ball.x + this.state.ball.radius;
if(ballBottomY >= this.state.board.y) {
if(ballCenterX >= this.state.board.x && ballCenterX <= this.state.board.x + this.state.board.width) {
//反彈
this.state.ball.speedY *= -1;
}else {
//遊戲結束
var timer = this.state.timer;
if(timer != undefined) {
clearInterval(timer);
this.setState({stateCode: -1});
}
}
}
//畫小球和擋板
this.state.ball.drawMe(this.state.context);
this.state.board.drawMe(this.state.context);
},
handleStartGameBtnClick: function() {
this.setState({stateCode: 1});
},
startGame: function() {
this.state.ball.reset();
this.state.board.reset();
this.refreshGameView();
var timer = setInterval(this.refreshGameView, this.props.refreshInterval);
this.state.timer = timer;
this.state.started = true;
},
handleMouseMove: function(event) { /** 處理滑鼠的移動事件,移動滑鼠的同時移動擋板 */
var x = event.clientX;
//將擋板的水平中心位置移到x處
var boardX = x - this.state.board.width / 2;
if(boardX < 0) {
boardX = 0;
}
if(boardX + this.state.board.width > this.props.width) {
boardX = this.props.width - this.state.board.width;
}
this.state.board.x = boardX;
}
});
GameView類的程式碼很長,下面一點點的說明:
(1)getInitialState()方法。該方法是React元件的生命週期方法,主要用來初始化一些資料。在該方法中,我們建立了一個小球物件和一個擋板物件,其中的this.props.width和this.props.height是在建立GameView標籤的時候傳入的,代表了GameView的寬高,建立了小球和擋板後,getInitialState()方法返回了state物件,該物件包含了ball,board,started和stateCode四個屬性,其中ball和board屬性就代表我們建立的小球和擋板物件,started屬性代表遊戲是否開始,初始值為false,當我們點選開始按鈕後才為true,stateCode是當前的遊戲狀態,分為三個狀態:待開始(0)、已開始(1)、遊戲結束(-1),初始狀態值為0,GameView會根據這個狀態值的不同來渲染不同的介面並顯示。
(2)render()方法。該方法主要處理介面的渲染,和getInitialState()方法一樣,是React元件的生命週期方法,該方法的內部通過this.state.stateCode的值,渲染不同的檢視並顯示出來。
(3)renderStartView()方法。該方法渲染遊戲待開始的檢視,即下面這個檢視:
(4)renderGameView()方法。該方法渲染的是遊戲進行中的檢視,就是一個<canvas>標籤而已,在canvas標籤中,添加了處理滑鼠移動事件的方法handleMouseMove(),handleMouseMove()方法內部就是通過獲取滑鼠當前的x座標,然後將擋板中心點移動到滑鼠的水平位置。handleMouseMove()方法中需要注意的是,擋板不能移到遊戲面板的外面去了,所以需要判斷一下擋板的橫座標,如果左邊或者右邊超出了遊戲區域(this.state.width),則要讓擋板的位置恢復到最左邊或最右邊。
(5)renderGameOverView()方法。該方法在遊戲結束後呼叫,渲染的是遊戲結束檢視,如下圖所示:
(6)componentDidUpdate()方法。該方法也是React元件的生命週期方法,在元件介面更新完成後自動被呼叫。在該方法中,我們判斷this.state.stateCode值是否為1,即遊戲是否開始,如果當前遊戲狀態是已開始,則獲取canvas標籤裡的context物件並呼叫startGame()方法開始遊戲。startGame()方法內部的處理步驟是:先重置小球和擋板的位置,然後呼叫refreshGameView()畫出遊戲介面,最後通過一個定時器,不斷地呼叫refreshGameView()方法,通過不停地重繪讓小球和擋板動起來。
(7)refreshGameView()方法。該方法是個非常重要的方法,因為小球的碰撞處理都在這個裡面。該方法的處理步驟如下:
*首先呼叫clear()方法清除掉介面上的圖形,如果不呼叫這個方法,則每次繪製新圖形時,上次繪製的圖形會保留在介面上;
*清除掉介面上的圖形後,再呼叫小球的move()方法讓小球運動一步,但是這步運動完之後,可能超出螢幕區域或者撞到擋板,所以下面要處理這些情況;
*當小球左、右、上三個方向碰撞到了遊戲面板的邊緣時,需要讓小球改變方向,即下面的程式碼處理過程:
if(ballX < 0) {
this.state.ball.x = 0;
this.state.ball.speedX *= -1;
}
if(ballY < 0) {
this.state.ball.y = 0;
this.state.ball.speedY *= -1;
}
if(ballX + 2 * this.state.ball.radius > this.props.width) {
this.state.ball.x = this.props.width - 2 * this.state.ball.radius;
this.state.ball.speedX *= -1;
}
*對於小球向下的方向,要區分小球撞到擋板和小球沒撞到擋板兩種情況,主要處理程式碼如下:
var ballBottomY = this.state.ball.y + 2 * this.state.ball.radius;
var ballCenterX = this.state.ball.x + this.state.ball.radius;
if(ballBottomY >= this.state.board.y) {
if(ballCenterX >= this.state.board.x && ballCenterX <= this.state.board.x + this.state.board.width) {
//反彈
this.state.ball.speedY *= -1;
}else {
//遊戲結束
var timer = this.state.timer;
if(timer != undefined) {
clearInterval(timer);
this.setState({stateCode: -1});
}
}
}
ballBottomY代表小球的下邊Y座標,ballCenterX代表小球的中心點X座標,當小球的ballBottomY大於等於擋板的Y座標時,即需要判斷小球是碰到了擋板還是結束遊戲了,如果小球的ballCenterX在擋板的範圍內,即ballCenterX >= this.state.board.x && ballCenterX <= this.state.board.x + this.state.board.width,就代表小球碰到了擋板,否則就是遊戲結束了,如果遊戲結束,需要停止介面的重新整理,並修改stateCode值,如果小球反彈,就需要修改小球Y方向的速度。到這裡基本上就說完了整個遊戲的實現過程,下面放上所有程式碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MoonyGame</title>
<script type="text/javascript" src="build/react.min.js"></script>
<script type="text/javascript" src="build/react-dom.min.js"></script>
<script type="text/javascript" src="build/browser.min.js"></script>
<style type="text/css">
#canvas {
border: 1px solid black;
}
.start-game-btn {
width: 150px;
height: 50px;
line-height: 50px;
text-align: center;
border-radius: 5px;
border: 1px solid black;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="text/babel">
//小球類
function Ball(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.originX = x;
this.originY = y;
this.speedX = 5;
this.speedY = 5;
//畫小球的方法
this.drawMe = function(context) {
context.beginPath();
context.fillStyle = "#000000";
context.arc(this.x + this.radius, this.y + this.radius, this.radius, 0, 2 * Math.PI);
context.closePath();
context.fill();
}
//小球移動
this.move = function() {
this.x += this.speedX;
this.y += this.speedY;
}
//小球位置重置
this.reset = function() {
this.x = this.originX;
this.y = this.originY;
this.speedX = 5;
this.speedY = 5;
}
}
//擋板類
function Board(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.originX = x;
this.originY = y;
//畫擋板
this.drawMe = function(context) {
context.fillStyle = "#000000";
context.fillRect(this.x, this.y, this.width, this.height);
}
//重置擋板的位置
this.reset = function() {
this.x = this.originX;
this.y = this.originY;
}
}
//遊戲面板
var GameView = React.createClass({
getInitialState: function() { /** 生命週期方法,這裡做一些初始化 */
//建立一個小球
var ballRadius = this.props.width / 30;
var ballX = this.props.width / 2 - ballRadius;
var ball = new Ball(ballX, 50, ballRadius);
//建立一個擋板
var boardWidth = this.props.width / 6;
var boardHeight = 10;
var boardX = (this.props.width - boardWidth) / 2;
var boardY = this.props.height - 100;
var board = new Board(boardX, boardY, boardWidth, boardHeight);
//返回state
return {
ball: ball,
board: board,
started: false,
stateCode: 0,
};
},
render: function() {
switch(this.state.stateCode) {
case -1:
return this.renderGameOverView();
break;
case 0:
return this.renderStartView();
break;
case 1:
return this.renderGameView();
break;
}
},
renderStartView: function() { /** 渲染遊戲開始的檢視 */
return (
<button className="start-game-btn" onClick={this.handleStartGameBtnClick}>開始遊戲</button>
);
},
renderGameView: function() { /** 渲染遊戲面板 */
console.error('render game view...');
return (
<canvas id="canvas"
onMouseMove={this.handleMouseMove}
width={this.props.width}
height={this.props.height} />
);
},
renderGameOverView: function() { /** 渲染遊戲結束的檢視 */
return (
<div>
<p>遊戲結束</p>
<button className="start-game-btn" onClick={this.handleStartGameBtnClick}>開始遊戲</button>
</div>
);
},
componentDidMount: function() { /** 生命週期方法,render結束後自動被呼叫,僅呼叫一次 */
},
componentDidUpdate: function() { /** 生命週期方法,元件更新完成後呼叫,可呼叫多次 */
if(this.state.stateCode == 1) {
this.state.context = document.getElementById('canvas').getContext('2d');
console.log('did update, start game...');
this.startGame();
}
},
clear: function() { /** 清除遊戲區域的背景 */
this.state.context.clearRect(0, 0, this.props.width, this.props.height);
},
refreshGameView: function() { /** 重新整理遊戲區域 */
//每次重新整理前都需要清除背景,不然小球和擋板上次的位置會被保留
this.clear();
this.state.ball.move();
//判斷move後的小球位置
var ballX = this.state.ball.x;
var ballY = this.state.ball.y;
if(ballX < 0) {
this.state.ball.x = 0;
this.state.ball.speedX *= -1;
}
if(ballY < 0) {
this.state.ball.y = 0;
this.state.ball.speedY *= -1;
}
if(ballX + 2 * this.state.ball.radius > this.props.width) {
this.state.ball.x = this.props.width - 2 * this.state.ball.radius;
this.state.ball.speedX *= -1;
}
var ballBottomY = this.state.ball.y + 2 * this.state.ball.radius;
var ballCenterX = this.state.ball.x + this.state.ball.radius;
if(ballBottomY >= this.state.board.y) {
if(ballCenterX >= this.state.board.x && ballCenterX <= this.state.board.x + this.state.board.width) {
//反彈
this.state.ball.speedY *= -1;
}else {
//遊戲結束
var timer = this.state.timer;
if(timer != undefined) {
clearInterval(timer);
this.setState({stateCode: -1});
}
}
}
//畫小球和擋板
this.state.ball.drawMe(this.state.context);
this.state.board.drawMe(this.state.context);
},
handleStartGameBtnClick: function() {
this.setState({stateCode: 1});
},
startGame: function() {
this.state.ball.reset();
this.state.board.reset();
this.refreshGameView();
var timer = setInterval(this.refreshGameView, this.props.refreshInterval);
this.state.timer = timer;
this.state.started = true;
},
handleMouseMove: function(event) { /** 處理滑鼠的移動事件,移動滑鼠的同時移動擋板 */
var x = event.clientX;
//將擋板的水平中心位置移到x處
var boardX = x - this.state.board.width / 2;
if(boardX < 0) {
boardX = 0;
}
if(boardX + this.state.board.width > this.props.width) {
boardX = this.props.width - this.state.board.width;
}
this.state.board.x = boardX;
}
});
var props = {
width: 300,
height: 500,
refreshInterval: 50
};
ReactDOM.render(
<GameView {...props} />,
document.getElementById('container')
);
</script>
</body>
</html>
原始碼已託管到GitHub,地址為:https://github.com/yubo725/MoonyGameH5,其中index2.html為本篇博文的原始碼。