开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第二天,点击查看活动详情
线上地址:http://codeape.site
源码:https://gitee.com/wooden-joint/my-game/blob/master/src/components/Tetris.vue
开发技术
vue2 + js
需求
方块自动掉落,键盘下左右,可以控制方块移动,上键用于旋转方块。当方块固定到底部,都需要创建一个新的方块(从几个样式中随机选择一种),空格键暂停/开始游戏。
代码讲解
创建根据模型数据创建对应的方块
当页面初始化时,需要先创建方块, 该函数是每次创建方块都需调用的,所以要先判断游戏是否结束;
当游戏进行中时,需要先记录当前的模型的索引,然后初始化方块的位置数据,然后随机从数据源中深拷贝一份方块数据,再将该数据保存到一个数组中。
createModel() {
// 判断游戏是否已经结束了
if (this.isGameOver()) {
this.gameOver();
return;
}
// 1.当前模型索引加一
this.currentIndex++;
// 重新初始化 16宫格的位置
this.moveX = 0;
this.moveY = 0;
// 2.确定当前使用了哪一个模型
// 先深拷贝数据
// 随机数 $lodash.random (从 0 到 this.MODELS.length - 1) 之间
let obj = this.$lodash.cloneDeep(
this.MODELS[this.$lodash.random(0, this.MODELS.length - 1)]
);
// es6 将对象转为数组
this.currentModel.push(Object.values(obj));
// 3.设置当前块元素所在的 x, y 坐标
this.positionModel();
// 4.让模型自动下落
this.autoDown();
},
设置方块的位置
方块的html结构
<div v-for="(item, index) in currentModel">
<div
v-for="(item2, index2) in item"
:class="item[0].isFixed ? 'fixed_model' : 'activity_model'"
></div>
</div>
紧接着需要设置当前块元素所在的 x, y 坐标;
设置该位置前需要先判断是否越界(后面方块的移动都需要调用该函数),根据当前的方块元素找到每个块应在的位置,然后设置它的 top 和 left 值。
positionModel() {
// 设置位置前先判断是否越界
this.checkBound();
this.$nextTick(function () {
let eles = document.querySelectorAll(".activity_model");
for (let i = 0; i < eles.length; i++) {
// 找到每个块元素对应的数据(行、列)
let blockModel = this.currentModel[this.currentIndex][i];
// 根据每个块元素对应的数据来指定块元素的位置
// 每个块元素的位置由两个值确定:
// 1、16宫格所在的位置 this.moveX。2、块元素在16宫格的位置 blockModel.row
let top = (this.moveY + blockModel.row) * this.STEP + "px";
let left = (this.moveX + blockModel.col) * this.STEP + "px";
// console.log(top)
// console.log(left)
this.$set(eles[i].style, "top", top);
this.$set(eles[i].style, "left", left);
// this.$set(eles[i].style, "fontSize", "30px")
// console.log(eles[i].style)
// }
}
});
},
方块自动下落
开启定时器,然后移动方块
autoDown() {
if (this.mInterval) {
clearInterval(this.mInterval);
}
this.mInterval = setInterval(() => {
this.move(0, 1);
}, this.speed);
},
移动的时候需要注意是否和底下已固定的方块发生了触碰,没发生触碰则移动方块,然后调用 positionModel()
函数,使方块移动
move(x, y) {
if (
this.isTouch(
this.moveX + x,
this.moveY + y,
this.currentModel[this.currentIndex]
)
) {
// 底部的触碰发生在移动16宫格的时候
// 此次移动是因为 y 轴的移动而引起的
if (y !== 0) {
// 模型之间底部发生了触碰
this.fixedBottomModel();
}
// 表示要发生触碰
return;
}
// 控制16宫格移动
this.moveX += x;
this.moveY += y;
// 根据16宫格的位置重新定位块元素
this.positionModel();
},
监听用户的键盘事件
使用 document.onkeydown
方法监听键盘事件,左右下,都是调用 move
函数,上则需要调用 rotate
函数
- 注:this.$store.commit(“setCurrentKey”, “left”); 只是我用来设置项目中方向键的样式的。可以不用管。
onKeyDown() {
document.onkeydown = (e) => {
switch (e.code) {
// 左
case "ArrowLeft":
this.$store.commit("setCurrentKey", "left");
this.move(-1, 0);
break;
// 上
case "ArrowUp":
this.$store.commit("setCurrentKey", "up");
this.rotate();
break;
// 右
case "ArrowRight":
this.$store.commit("setCurrentKey", "right");
this.move(1, 0);
break;
// 下
case "ArrowDown":
this.$store.commit("setCurrentKey", "down");
this.checkBound();
this.move(0, 1);
break;
// 空格键
case "Space":
this.pause();
break;
}
};
},
方块移动时需要判断是否发生了触碰
参数中的 x,y 表示16宫格(将要/下一步)移动的位置;model 表示当前模型数据源(将要)完成的变化(旋转等)
isTouch(x, y, model) {
// 判断触碰,就是判断活动中的模型(将要移动到的位置)是否已经存在被固定的(块元素)
// 如果存在返回 true 表示(将要)移动的位置会发生触碰
for (let k in model) {
let blockModel = model[k];
// 该位置是否已经存在块元素?
if (this.fixedBlocks[y + blockModel.row + "_" + (x + blockModel.col)]) {
return true;
}
}
return false;
},
方块发生了触碰
将方块固定到底部,然后改变方块的样式并让其不可继续移动。每个块都需要将位置记录下来。固定的方块存放到 fixedBlocks
变量中。然后需要判断是否需要清空这一行,同时创建新的方块。
- fixedBlocks = { 行_列: 块元素 }
fixedBottomModel() {
// 1.改变模型中块元素的样式
// 2.让模型不可以再进行移动
this.currentModel[this.currentIndex][0].isFixed = true;
this.$nextTick(function () {
// 记录该块元素所在的位置
let fixedClass = document.querySelectorAll(".fixed_model");
for (let i = 0; i < this.currentModel[this.currentIndex].length; i++) {
let blockModel = this.currentModel[this.currentIndex][i];
// 固定的16宫格内的当前块索引
let currentI = fixedClass.length - this.currentModel[this.currentIndex].length + i;
this.fixedBlocks[
this.moveY + blockModel.row + "_" + (this.moveX + blockModel.col)
] = fixedClass[currentI];
}
// 判断是否要请空该行
this.isRemoveLine();
// 3.创建新的模型
this.createModel();
});
},
判断是否要清空这一行
遍历行和列,判断每一行是否已经被铺满了,如果铺满了,则需要清除该行增加得分。
isRemoveLine() {
// 一行中每列都存在块元素
// 遍历所有行
for (let i = 0; i < this.ROW_COUNT; i++) {
// 标记符: 假设当前行已经被铺满了
let flag = true;
// 遍历所有列
for (let j = 0; j < this.COL_COUNT; j++) {
// 如果当前行中有一列没有数据,那么就说明当前行没有被铺满
if (!this.fixedBlocks[i + "_" + j]) {
flag = false;
break;
}
}
if (flag) {
// 该行已经被铺满了
this.removeLine(i);
// 得分++
this.score += 100;
}
}
},
清除铺满的那一行
遍历当前行的所有列,并删除每一个小方块,然后清除该行中所有块元素的数据源。然后让被清理行以上的所有块元素往下移动对应的行数。
removeLine(line) {
this.$nextTick(function () {
// 获取所有 块元素外面的 16 宫格
let allNodes = document.querySelector("#container").childNodes;
// 遍历该行中的所有列
for (let i = 0; i < this.COL_COUNT; i++) {
// 1.删除该行中所有的块元素
allNodes.forEach((item) => {
for (let j = 0; j < item.children.length; j++) {
// 如果删除了一个块元素就退出循环
if (item.children[j] === this.fixedBlocks[line + "_" + i]) {
item.removeChild(this.fixedBlocks[line + "_" + i]);
return;
}
}
});
// 2.删除该行中所有块元素的数据源
this.fixedBlocks[line + "_" + i] = null;
}
this.downLine(line);
});
},
让被清理行以上的方块下落
遍历被清理的行以上的所有行,如果存在数据则代表有方块,则让其数据源中的行数 +1 代表往下移动,然后操作块元素的 top 值让其下落,再清除旧的数据。
downLine(line) {
// 遍历被清理行之上的所有行
for (let i = line - 1; i >= 0; i--) {
// 该行中的所有列
for (let j = 0; j < this.COL_COUNT; j++) {
// 如果没有数据 就跳到下一次循环
if (!this.fixedBlocks[i + "_" + j]) continue;
// 存在数据
// 1.被清理行之上的 所有块元素 的数据源 的行数 + 1
this.fixedBlocks[i + 1 + "_" + j] = this.fixedBlocks[i + "_" + j];
// 2.让块元素在容器中的位置下落
this.fixedBlocks[i + 1 + "_" + j].style.top =
(i + 1) * this.STEP + "px";
// 3.清理掉之前的块元素
this.fixedBlocks[i + "_" + j] = null;
}
}
},
游戏结束
当固定的块元素到达顶部分割线区域时,则代表游戏结束。
isGameOver() {
// 当第 0 行有元素时
for (let i = 0; i < this.COL_COUNT; i++) {
if (this.fixedBlocks["3_" + i]) {
return true;
}
}
return false;
},
以上就是俄罗斯方块中主要用到的函数。
收获
该俄罗斯方块,大概是一年多之前的时候开始写的。当时基础较差,看着b站的一个js版本的视频,然后自己改写的uniapp的vue版本。整个小游戏写下来对js的基本功还是挺有帮助的。像一些需要用到深拷贝的地方和一些属性的添加,就能更清晰的了解 js 的引用类型啊,vue2 的响应式监听等。
暂无评论内容