1. 程式人生 > >ReactJS實戰之簡易彈球遊戲的實現

ReactJS實戰之簡易彈球遊戲的實現

這一篇記錄的是使用ReactJS完成一個簡易的彈球遊戲,遊戲在瀏覽器中執行的效果圖如下所示:


滑鼠在遊戲面板中左右移動控制擋板的水平移動。下面一步一步實現這個簡單的遊戲。

首先,我們需要知道如何在瀏覽器中繪圖,這裡的小球和擋板,都是通過繪圖畫出來的,瀏覽器中的繪圖主要使用了html5的canvas標籤,定義一個canvas標籤並指定大小,然後獲取canvas的context,通過context物件完成繪圖,示例程式碼如下:

<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方法,該方法的使用方法如下:
context.arc(x,y,r,sAngle,eAngle,counterclockwise);

引數 描述
x 圓的中心的 x 座標。
y 圓的中心的 y 座標。
r 圓的半徑。
sAngle 起始角,以弧度計。(弧的圓形的三點鐘位置是 0 度)。
eAngle 結束角,以弧度計。
counterclockwise 可選。規定應該逆時針還是順時針繪圖。False = 順時針,true = 逆時針。

下面放上關於小球的JavaScript程式碼:

//小球類
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定義了一個Ball類,構造方法裡傳入的三個引數分別是小球的起始x,y座標和小球的半徑,注意這裡的x,y不是小球的中心點的座標,而是小球對應的矩形的左上角的座標。Ball類中還有originX和originY兩個變數,代表小球的初始位置,這兩個量用於在遊戲結束後重置小球的位置,speedX和speedY代表小球在水平和垂直方向上的速度,drawMe方法是畫小球的方法,需要傳入一個context引數,代表canvas中的context物件,move方法是移動小球的方法,即讓小球的當前x,y座標變為上次x,y座標加上速度值,因為小球的運動就是通過不斷的改變小球的座標,然後不停地重繪畫布來實現的,reset方法是重置小球位置的方法,當遊戲結束重新開始時,需要呼叫這個方法重置小球位置。

畫完了小球,再需要畫擋板了,擋板就是一個矩形,比較好畫,使用的是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為本篇博文的原始碼。