JS实现俄罗斯方块
视频教程:
JavaScript开发俄罗斯方块
代码地址:
Tetris-based-on-JS
1.初始化布局
我们设置Game为中介者类
你会发现我们的Game.js文件的最外层是用IIFE包裹起来的,因为我们需要知道实际工作中都是多个类进行并行开发的,由于为了上生产,会将代码进行打包部署,也就是将所有的js文件打包为个js文件,如果没有IIEF会造成作用域和命名的冲突,所以我们用IIFE隔离作用域。
(function () {
window.Game = function () {
this.row = 20;
this.col = 12;
// 初始化
this.init();
};
Game.prototype.init = function () {
// 初始化大表格
var $table = $("<table></table");
// 渲染表格
for (var i = 0; i < this.row; i++) {
// 创建tr
var $tr = $("<tr></tr>");
for (var j = 0; j < this.col; j++) {
// 创建td
var $td = $("<td></td>");
$td.appendTo($tr);
}
// tr放到table里
$tr.appendTo($table);
}
$table.appendTo("body");
};
})();
css样式
有利用到CSS中的linear-gradient() 函数实现颜色渐变效果
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
overflow: hidden;
background: url(./images/bg2.png) repeat-x center bottom, -webkit-linear-gradient(top, skyblue, white);
background-size: 20% auto;
}
table {
border-collapse: collapse;
margin: 10px auto;
}
td {
border: 1px solid red;
width: 25px;
height: 25px;
}
2.处理方块
表示方块
俄罗斯方块中,下落的方块形态一共有7种,每一种又有不同的状态(方向)
S型、Z型、J型、L型、O型、T型、I型,这些形态都用一个4*4的矩阵表示
我们使用二维数组去表示俄罗斯方块的状态
比如上图的T我们使用二维数组
[
[1, 1, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
我们会将所有的俄罗斯方块的类型和状态都放到一个JSON中,JSON是一个三维数组
var fangkuai = {
S: [
[
[0, 1, 1, 0],
[1, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 1, 0, 0],
[0, 1, 1, 0],
[0, 0, 1, 0],
[0, 0, 0, 0],
],
],
...
}
渲染方块
创建Block.js添加Block类,给其添加一个render函数用来渲染方块,下面测试来渲染一个顶部居中显示的方块
(function () {
window.Block = function () {
this.block = [
[0, 1, 1, 0],
[1, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
];
// 初始的行
this.row = 0;
// 初始的列,因为要居中显示,所以列要为4
this.col = 4;
};
Block.prototype.render = function () {
// 渲染四行四列的方块
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
// 如果四乘四矩阵中的某一项不为零,则设置颜色
if (this.block[i][j] != 0) {
// 加上row,col校准,使得方块被渲染到中间显示
game.setColor(i + this.row, j + this.col, this.block[i][j]);
}
}
}
};
})();
render函数使用了Game类的setColor方法,用于给某个特定的表格中的小格子设定颜色。
Game.prototype.setColor = function (row, col, num) {
// 给对应的有颜色的方块添加类名
$("tr")
.eq(row) // eq选择器选取带有指定index值的元素
.children("td")
.eq(col)
.addClass("c" + num);
};
但是我们发现this.block是写死的,始终只有一种状态,所以接下来我们要随机显示方块。
如何随机显示方块?我们一共有7种类型,每一种类型有不同的状态,所以我们要把所有的类型和所有的状态都随机,为此我们修改Block.js,随机获取某种类型方块的某个形状:
(function () {
window.Block = function () {
// 得到随机的方块
// 第一步罗列所有的类型
var allType = ["S", "T", "O", "L", "J", "I", "Z"];
// 第二步从所有的类型中随机得到一种
this.type = allType[parseInt(Math.random() * allType.length)];
// 第三步得到随机的类型方块,然后通过这个类型获取当前的类型所有形状总数量,因为不同类型的方块形状数量也不同
this.allDir = fangkuai[this.type].length;
// 第四步通过当前的allDir的length随机得到不同的数字
this.dir = parseInt(Math.random() * this.allDir);
// 第五步得到随机的方块
this.code = fangkuai[this.type][this.dir];
console.log(this.allDir);
// 初始的行
this.row = 0;
// 初始的列,因为要居中显示,所以列要为4
this.col = 4;
};
Block.prototype.render = function () {
// 渲染四行四列的方块
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
// 如果四乘四矩阵中的某一项不为零,则设置颜色
if (this.code[i][j] != 0) {
// 加上row,col校准,使得方块被渲染到中间显示
game.setColor(i + this.row, j + this.col, this.code[i][j]);
}
}
}
};
})();
3.设置地图类(渲染初始状态的地图)
我们需要一个地图,来显示已经到底的方块,此时我们的表格只是一个显示的dom,没有办法实时进行显示,因为每一帧要清除画布,所以要想持久,就需要让数据持久起来,所以我们维持一个地图类,用来持久已经到底的方块
(function () {
window.Map = function () {
// 地图矩阵
this.mapCode = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0],
[1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0],
];
};
Map.prototype.render = function (mapGame) {
for (var i = 0; i < mapGame.row; i++) {
for (var j = 0; j < mapGame.col; j++) {
if (this.mapCode[i][j] != 0) {
game.setColor(i, j, this.mapCode[i][j]);
}
}
}
};
})();
在start函数中,添加对地图的渲染,注意我们将self传入给了render函数,这样在Map中的render函数中可以获取到行列值
Game.prototype.start = function () {
var self = this;
this.timer = setInterval(() => {
// 渲染方块
self.block.render();
// 渲染地图
self.map.render(self);
}, 500);
};
4.方块的下落状态
此时我们的方块下落很简单,就是给Block.row++就可以了,但是怎么停止?
此时我们提出了一个“预判断”的概念,也就是每一次方块下落的时候需要进行一次预先判断,当前方块的下一次运动位置去和地图的(mapCode)进行位置判断,如果此时 mapCode在相同的位置也有方块存在,则停止下落。同时生成新的方块
// 能力判断方法,判读的是对应位置的方块和地图是否有都不为0的情况,如果有返回true,否则返回false
Block.prototype.check = function (row, col) {
// check函数的row和col指的是要校验的地图的row和col的位置
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (this.code[i][j] != 0 && game.map.mapCode[row + i][col + j] !== 0) {
return false;
}
}
}
return true;
};
// 方块下落,需要判断当前的方块能否下落
Block.prototype.checkDown = function () {
// 判断当前地图的位置和自己方块的位置是否有重合,this.row + 1指的是预判断
// 预判断就是在下一次方块将要到达的位置是否有对应的地图不为0
if (this.check(this.row + 1, this.col)) {
this.row++;
} else {
// 此时就是下落到底的状态,生成新方块
game.block = new Block();
}
};
5.渲染地图
当前我们的方块到底之后,不会渲染当前的状态,只会消失,因为是mapCode一直在维护底部的持久状态,所以一旦方块到底后,就要将数据给mapCode进行持久更新。
// 方块下落,需要判断当前的方块能否下落
Block.prototype.checkDown = function () {
// 判断当前地图的位置和自己方块的位置是否有重合,this.row + 1指的是预判断
// 预判断就是在下一次方块将要到达的位置是否有对应的地图不为0
if (this.check(this.row + 1, this.col)) {
this.row++;
} else {
// 此时就是下落到底的状态,生成新方块
game.block = new Block();
// 方块已经到底了,然后要渲染到地图的code中
this.renderMap();
}
};
// 将已经到底的方块渲染到地图中
Block.prototype.renderMap = function () {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
// 将现在已有的方块渲染到Map类的mapCode上
if (this.code[i][j] != 0) {
// 改变地图的mapCode数据
game.map.mapCode[this.row + i][this.col + j] = this.code[i][j];
}
}
}
};
6.方块的左右移动+一键到底
其实就是增加事件监听,然后通过check方法预判断是否有能力移动
// 给Game设置事件监听
this.bindEvent();
...
Game.prototype.bindEvent = function () {
var self = this;
$(document).keydown(function (event) {
console.log(event.keyCode);
switch (event.keyCode) {
case 37:
// 判断是否有向左移动的能力
self.block.checkLeft();
break;
case 39:
// 判断是否有向右移动的能力
self.block.checkRight();
break;
case 32:
// 实现按空格一键到底
self.block.checkBlockEnd();
break;
}
});
};
在Block.js中实现左右移动以及一键到底的功能
// 判断是否能够向左移动,如果可以则移动
Block.prototype.checkLeft = function () {
if (this.check(this.row, this.col - 1)) {
this.col--;
}
};
// 判断是否能够向右移动,如果可以则移动
Block.prototype.checkRight = function () {
if (this.check(this.row, this.col + 1)) {
this.col++;
}
};
// 使用while循环,如果当前的check返回的是true则代表能够下移,继续让row++
Block.prototype.checkBlockEnd = function () {
while (this.check(this.row + 1, this.col)) {
this.row++;
}
};
7.方块的旋转
我们可以按上键让方块按照顺时针的方向旋转
Block.prototype.checkRot = function () {
this.dir = (this.dir + 1) % this.allDir;
// 改变方向之后渲染新的方块方向
this.code = fangkuai[this.type][this.dir];
};
但是此时会发生一个问题,方块在旋转的过程中不会顾及左右是否有有的颜色格子,所以如果在下降的过程中切换形态,方块可能会直接和现存的地图发生重合。
此时我们可以对方块进行一次“备份”,将旧的方块备份一份,然后让新blck的和已有的mapCode进行一比对,如果新的有重合就打回原形
Block.prototype.checkRot = function () {
// 备份旧的形状方向
var oldDir = this.dir;
// 改变新的
this.dir = (this.dir + 1) % this.allDir;
// 改变方向之后渲染新的方块方向
this.code = fangkuai[this.type][this.dir];
// 渲染之后的新方块需要判断,是否有能力进行渲染
if (!this.check(this.row, this.col)) {
// 进入这里了就说明重合了,违规了,打回原形
this.dir = oldDir;
// 再次渲染方块
this.code = fangkuai[this.type][this.dir];
}
};
8.方块消行
方块行本质就是mapCode的每一项数组的遍历,如果某一项数组中有都不为,就说明该消行了。
Map.prototype.checkRemove = function () {
// 判断当前的mapCode是否该消行
// 消行规则:当前的 mapCode数组的每一项如果都不是0了,就说明该消行了
for (var i = 0; i < 20; i++) {
// 如果某一行没有0,则删除这一行
if (this.mapCode[i].indexOf(0) == -1) {
// splice()方法第一个参数,表示从哪一个开始删,第二个表示删除数量
this.mapCode.splice(i, 1);
// 删除一行补一行,unshift会在数组头部插入指定的参数
this.mapCode.unshift([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
}
}
};
9.判定游戏结束
地图的第一行的mapCode有不为0的了就判定死亡了,每次方块到底的时候进行一次判断即可
Block.prototype.checkOver = function () {
for (var j = 0; j < game.col; j++) {
if (game.map.mapCode[0][j] != 0) {
clearInterval(game.timer);
alert("游戏结束");
}
}
};
10.设置预览框
俄罗斯方块有一个重要的逻辑就是预览,下一次的方块会提前展示,所以我们可以设置一个nextBlock当做下一次出场的方块
渲染预览框的方法
Game.prototype.setNextColor = function (row, col, num) {
// 给对应的有颜色的方块添加类名
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (this.nextBlock.code[i][j] != 0) {
$(".tab2")
.find("tr")
.eq(i)
.children("td")
.eq(j)
.addClass("c" + this.nextBlock.code[i][j]);
}
}
}
};
初始化的时候增加预览框的功能
// 初始化预览窗口
var $table2 = $("<table></table");
$table2.addClass("tab2");
for (var i = 0; i < 4; i++) {
var $tr2 = $("<tr></tr>");
for (var j = 0; j < 4; j++) {
var $td2 = $("<td></td>");
$td2.appendTo($tr2);
}
$tr2.appendTo($table2);
}
$table.appendTo("body");
$table2.appendTo("body");
当方块到底的时候,渲染nextBlock状态
// 方块下落,需要判断当前的方块能否下落
Block.prototype.checkDown = function () {
// 判断当前地图的位置和自己方块的位置是否有重合,this.row + 1指的是预判断
// 预判断就是在下一次方块将要到达的位置是否有对应的地图不为0
if (this.check(this.row + 1, this.col)) {
this.row++;
} else {
// 此时就是下落到底的状态,渲染预览框的方块
game.block = game.nextBlock;
// 让预览框的方块再次渲染新的方块
game.nextBlock = new Block();
// 方块已经到底了,然后要渲染到地图的code中
this.renderMap();
// 判断是否可以消行
game.map.checkRemove();
// 判断游戏是否结束
this.checkOver();
}
};
11.困难升级+实现分数机制
为了实现困难升级(即速度加快),我们给Game设置一个during属性表示步长,并记录setInterval的帧数f(执行次数),仅当f模上during为整数时,方块才下落从而控制速度,当during逐渐减小时,方块的下落速度也会加快
Game.prototype.start = function () {
var self = this;
// 设置帧编号
this.f = 0;
this.timer = setInterval(() => {
self.f++;
// 渲染帧编号
document.getElementById("f").innerHTML = "帧编号:" + self.f;
// 清屏
self.clear();
// 渲染方块
self.block.render();
// 渲染预览方块
self.setNextColor();
// 渲染地图
self.map.render(self);
// 下落,通过帧编号%步长控制下落的速度
self.f % this.during == 0 && self.block.checkDown();
}, 20);
};
在消行处理的函数checkRemove中实现加分操作,并可以设置速度越快加分越多
// 分数增加,根据不同的速度决定加多少分数
if (game.during <= 30 && game.during >= 20) {
game.score += 10;
} else if (game.during < 20 && game.during >= 10) {
game.score += 20;
} else {
game.score += 30;
}
// 渲染分数
document.getElementById("score").innerHTML = "分数:" + game.score;
if (game.score % 100 == 0) {
game.during -= 5;
if (game.during <= 0) {
game.during = 1;
}
}
预览