这一篇记录的是使用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(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为本篇博文的源码。