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为本篇博文的源码。 

体验该游戏点击这里

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yubo_725

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值