用 jQuery 手写一个小游戏
今天给大家带来一个小游戏。
要求:熟悉 JavaScript 继承的概念。
游戏预览
玩法:
开局一个球,两块板子,其中最上方是电脑板子,会跟着球跑,球达到板子上回弹回来,打到你的板子上也是回弹出去,如果达到了上下边界,游戏结束。控制你的板子就用方向左右键。接到一个球+10分。
基础布局部分(HTML+CSS)
游戏部分,我们按照以下图示尺寸设定:
HTML:
<div class="game"> //游戏本体
<div class="board b1"> //电脑的板子(上板子)
</div>
<div class="board b2"> //你的板子(下板子)
</div>
<div class="ball"></div> //球体
<div class="info">
<h2 class="infoText">开始游戏</h2>
<button class="start">点击这里</button>
</div>
<div class="grade">0</div> //左上角分数卡
</div>
CSS:
.game {
width: 500px;
height: 500px;
position: relative;
border: 5px solid #fff;
background-color: #222;
}
.board {
background-color: #FF644E;
}
.ball {
background-color: #fff;
}
.info {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
color: white;
background-color: #222;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
逻辑部分(JavaScript)
我们采用 JavaScript 继承的方式来写这个游戏。
首先先定义一个 GameObject:
let GameObject = function (position,size,selector){
this.$el = $(selector) //选择器
this.position = position //游戏物体位置
this.size = size //游戏物体大小
this.$el.css("position","absolute") //设置其Css为绝对定位
this.updateCss()
}
首先,$el为选择器,代表 jQuery 的元素选择器。position 为元素定位的位置。size为元素大小。
设置在原型链上的 updateCss 方法为元素位置、大小更新方法。按照当前对象的属性数值更新。
那我们先创建一个球(Ball)对象,继承 GameObject:
let Ball = function () {
this.size = {width: 15, height: 15}; //球的大小
this.position = {x: 250, y: 250}; //球的位置
this.velocity = {x: 5, y: 5}; //球的速度
GameObject.call(this,this.position,{width: 15, height: 15},".ball") //继承GameObject。并将参数和自身传入
};
Ball.prototype = Object.create(GameObject.prototype); //将Ball的原型链连接GameObjecr的原型链
Ball.prototype.constructor = Ball.constructor //因为连接,所以需要重新指向构造函数。将原型链的构造函数指向自己的构造函数
因为球只有一个,所以参数我们写死在对象里面。
我们实例化一个球对象:
let ball = new Ball();
接着屏幕中央就会有一个球:
接下来开始绘制两个可移动的板子:
let Board = function (position, sel) {
this.size = { //锁定板子大小
width: 100,
height: 15
};
GameObject.call(this, position, this.size, sel); //对接父对象
};
Board.prototype = Object.create(GameObject.prototype); //对接父对象原型链
Board.prototype.constructor = Board.constructor; //更改原型链上的构造为自己的构造
然后 new 两块板子:
let board1 = new Board({x: 0, y: 30}, '.b1');
let board2 = new Board({x: 0, y: 455}, '.b2');
然后,我们让球动起来。
我们在 Ball的原型链上定义一个 update 方法来移动小球:
Ball.prototype.update = function () {
this.position.x += this.velocity.x; //x轴按速度移动
this.position.y += this.velocity.y; //Y轴按速度移动
this.updateCss(); //调用父对象的updateCss方法更新界面
if (this.position.x < 0 || this.position.x > 500) { //如果撞到了左右墙壁
this.velocity.x = -this.velocity.x; // 回弹
}
if (this.position.y < 0 || this.position.y > 500) { //如果撞到了上下墙壁
this.velocity.y = -this.velocity.y; // 回弹
}
};
如果球的横向边界小于 0 或者大于 500,说明球碰到了左右墙壁。
如果球的纵向边界小于 0 或者大于 500,说明球碰到了上下墙壁。
然后我们每隔 30ms 调用一下小球的 update 函数,使其位置更新:
setInterval(function () {
ball.update();
}, 30)
如图 :
这样小球就有了碰到障碍物反弹的能力了。
接着我们删掉这个定时器的代码。
然后,我们定义一个 Game 对象。这个对象不会继承任何父对象,因为它只负责控制其他物体对象。
let Game = function () {
this.timer = null; //唯一timer 负责开始游戏结束游戏的timer
this.grade = 0; //分数
this.initControl(); //键盘监听事件
this.control = {}; //这个放置各个键盘按键情况的对象
};
因为我们有键盘要控制板子的移动,所以我们要加监听事件。
Game.prototype.initControl = function () {
let _this = this; //防止this作用域混淆
$(window).keydown(function (evt) { //按键按下
_this.control[evt.key] = true; //设置当前的key value为true
});
$(window).keyup(function (evt) { //按键抬起
_this.control[evt.key] = false; //设置当前的key value为false
})
};
根据我们的游戏规则,小球碰到上下墙壁要判别输赢,碰到上下板子要回弹。所以我们在 GameObject 的原型链上定义一个碰撞方法 collide。
GameObject.prototype.collide = function (otherObject) {
let inRangeX = otherObject.position.x > this.position.x &&
otherObject.position.x < this.position.x + this.size.width;
let inRangeY = otherObject.position.y > this.position.y &&
otherObject.position.y < this.position.y + this.size.height;
return inRangeX && inRangeY;
};
其参数是另一个物体对象。
inRangeX 的判别式:当另一个物体的 X 值大于你的 X 值,且另一个物体 X 值小于你的 X 值+你宽度的时候,返回 true,否则 false。
inRangeY 的判别式:当另一个物体的 Y 值大于你的 Y 值,且另一个物体 Y 值小于你的 Y 值+你高度的时候,返回 true,否则 false。
然后返回两个判别式的情况。如果都为 true,说明两个物体相撞了。
这样我们在 Game 对象定义一个 startGameMain 方法,代表是我们游戏控制器主体。
Game.prototype.startGameMain = function () {
let _this = this; //作用域!!!
this.timer = setInterval(function () { //唯一定时器
if (board1.collide(ball)) { //如果一号板子撞到了球
console.log("碰到了1号板子");
ball.velocity.y = -ball.velocity.y; //Y反向运动
}
if (board2.collide(ball)) { //如果二号板子撞到了球
console.log("碰到了2号板子");
_this.grade += 10; //自己的分数+10
ball.velocity.y = -ball.velocity.y;
}
ball.update(); //球体更新方法
$(".grade").text(this.grade); //jQuery更新分数
}, 30) //每隔30ms走一次
};
然后,
let game = new Game();
game.startGameMain();
看一看效果:
接着在 startGameMain 函数内,继续编写。
如果球碰到上板子,说明上板子输了,如果碰到下板子,下板子输了。
if (ball.position.y < 0) {
console.log("第一个板子输了");
_this.endGame("你赢了"); //后面的结束游戏方法
}
if (ball.position.y > 500) {
console.log("第二个板子输了");
_this.endGame("你输了");
}
接着我们让上板子跟着球跑。我们在板子 Board 对象内定义一个 update 方法,更新板子的坐标和 UI。
Board.prototype.update = function () {
this.updateCss();
};
然后继续在 startGameMain 函数内写板子跟球跑的逻辑:
board1.position.x += ball.position.x > board1.position.x + board1.size.width / 2 ? 12 : 0;
board1.position.x += ball.position.x < board1.position.x + board1.size.width / 2 ? -12 : 0;
board1.update();
如果球的 X 坐标 > 板子 X 坐标+板子宽度/2,那么板子 X +12,向右跑。
如果球的 X 坐标 < 板子 X 坐标+板子宽度/2,那么板子 X -12,向左跑。
如图:
但是细心的朋友可能会发现,板子超出边界了。
所以我们就限制板子最小 X 为 0,最大 X 为容器 width-板子 width。
于是我们重写一下 Board 的 update 方法。
Board.prototype.update = function () {
if (this.position.x < 0) {
this.position.x = 0;
}
if (this.position.x + this.size.width > 500) {
this.position.x = 500 - this.size.width;
}
this.updateCss();
};
这样再看看:
接着写我方板子键盘控制事件,还是在 startGameMain 函数内。
if (_this.control["ArrowLeft"]) { //如果左键
board2.position.x -= 8; //二号板子左移8
}
board2.update();
这样我们的键盘也可以操控了,球也能正常回弹。
我们继续写 endGame 函数:
Game.prototype.endGame = function (res) {
clearInterval(this.timer); //清除定时器
$(".infoText").html(res + '<br>分数:' + this.grade); //展示分数
$(".info").show(); //展示信息
};
然后我们再加一个 startGame 的函数:
Game.prototype.startGame = function () {
let time = 3; //倒计时3秒
let _this = this;
this.grade = 0; //初始化分数0
ball.init(); //稍后用到
let timer = setInterval(function () {
$(".infoText").text(time);
time--;
if (time < 0) { //如果时间<0
clearInterval(timer); //清除定时器
$(".info").hide(); //隐藏信息
_this.startGameMain(); //开始主要的游戏函数
}
}, 1000)
};
我们在 HTML 里面新增 info 信息的元素:
<div class="game">
<div class="board b1">
</div>
<div class="board b2">
</div>
<div class="ball"></div>
<div class="info"> //新增的地方
<h2 class="infoText">开始游戏</h2>
<button class="start">点击这里</button>
</div>
<div class="grade">0</div>
</div>
下面我们调用一下 startGame:
let game = new Game();
$(".start").click(function () {
game.startGame();
})
这样一个比较完整的游戏完成了:
但是这看起来有点傻,因为它每次只向一个方向去发车。
我们可以使用 JavaScript 中的三角函数解决这个问题。
首先我们找到 Ball 对象,把里面的速度参数抽出为一个函数,取名叫 init。
Ball.prototype.init = function () {
this.position = {x: 250, y: 250};
let randomDeg = Math.random() * 2 * Math.PI;
this.velocity = {
x: Math.cos(randomDeg) * 8,
y: Math.sin(randomDeg) * 8
}
};
然后 Ball 对象内只剩下:
let Ball = function () {
this.size = {width: 15, height: 15};
this.init();
GameObject.call(this, this.position, this.size, '.ball');
};
我们来仔细讲一下这个 init 函数。首先我们先锁定速度为 10,这个是首要条件,所以我们先产生一个随机角度。
let randomDeg = Math.random() * 2 * Math.PI;
1PI 为 180 度,2PI 为 360 度。然后我们再随机一个小数,可以得到一个 360 度以内的任意角。
接着,我们可以根据三角函数 cos 和 sin。sin 是斜边/对边,cos 是斜边/邻边。我们如果知道了角度和长度,就可以知道 XY 的速度分别是多少。
X 长度 = 斜边长 * Cos(角度) Y 长度 = 斜边长 * Sin(角度)
如图:
这也就是数学中向量的概念。
然后我们再看看游戏:
显然比之前合理多了。
原文:
https://juejin.im/post/6844903687307919373
体验链接 CodePen:
https://codepen.io/884181317/pen/wYzvVd
码云源码链接 Gitee:
https://gitee.com/Janlaywss/juejin-doc/blob/master/JsExtendGame.vue
非营利组织 freeCodeCamp.org 自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
你也想成为
freeCodeCamp 社区的贡献者吗
欢迎点击以下文章了解
点击“阅读原文”
在 freeCodeCamp 专栏阅读更多