上一篇讲解了具体思路,这一篇上具体的代码。首先是七种形状的类:
/**
iBlock.js
*/
class LinePiece {
constructor() {
// 当前数据为旋转数组中的第一个
this.data = this.rotate[this.dir];
// 保存旋转90度、180度、279度、360度后的数据
this.rotate = [
[
[2, 2, 2, 2]
],
[
[2],
[2],
[2],
[2]
],
[
[2, 2, 2, 2]
],
[
[2],
[2],
[2],
[2]
],
];
//记录当前旋转数组索引
this.dir = 0;
// 记录移动的距离
this.position = {
x: 0,
y: 0
};
// 用来给每一个实例保存定时下落的计时器
this.timer = null;
}
}
export default LinePiece;
剩下六个类的结构类似,这里就不再赘述。接下来是这几个类的公共方法的类:shapes.js。
首先我们把七种形状的类放入公共类的一个数组中,方便取用:
/**
shapes.js
*/
import IBlock from './iBlock.js';
import Square from './square.js';
import RLBlock from './rLBlock.js';
import TBolck from './tBolck.js';
import Swagerly from './twagerly.js';
import RSwagerly from './rSwagerly.js';
import LinePiece from './linePiece.js';
class Shapes() {
constructor: {
this.shapesData = [
lBlock,
Square,
RLblock,
TBolck,
Swagerly,
RSwagerly,
LinePiece
]
}
}
取到七种形状的类以后,我们需要让这些形状随机生成一个实例,于是要有一个generateShape方法:
/**
shapes.js
*/
……
Shapes.prototype.generateShape = function() {
this.curShape = new (this.shapesArr[Math.floor(Math.random() * 8)])();
};
当方块下落到底部或碰到其他方块的时候,执行该方法;当方块已经堆积到游戏区域顶端时返回,不再往下执行(不再生成新的方块);此外生成方块以后,在不做任何键盘操作的情况下,方块每隔500毫秒下落一个单位(执行一次goDown方法):
/**
shapes.js
*/
class Shapes() {
constructor: {
this.shapesData = [
lBlock,
Square,
RLblock,
TBolck,
Swagerly,
RSwagerly,
LinePiece
];
this.gameOver = false; // 判断游戏是否结束
}
}
Shapes.prototype.generateShape = function(gameData, gameDivs) {
// 游戏结束时返回,不再往下执行
if (this.gameOver) return;
// 清空当前方块的计时器
if (this.curShape) clearInterval(this.curShape.timer);
// 生成新的方块
this.curShape = new (this.shapesArr[Math.floor(Math.random() * 8)])();
// 判断游戏区域是否已堆满方块,否则每隔500ms执行下落方法;是则gameover,清除定时器,更新最后一组数据
if (this.downable(gameData)) {
this.curShape.timer = setInterval(() => {
this.goDown(gameData, gameDivs);
}, 500);
} else {
this.gameOver = true;
clearInterval(this.curShape.timer);
this.updateData(this.curShape.data, gameData, gameDivs);
}
};
生成以后我们要把当前形状的数据更新到游戏区域中,于是要有一个updateData方法,通过循环当前形状的数组,将每一个数据复制到游戏区域的数组中,并刷新DOM:
/**
shapes.js
*/
……
Shapes.prototype.updateData = function(curData, gameData, gameDivs) {
for (let i = 0; i < curData.length; i++) {
for (let j = 0; j < curData[0].length; j++) {
gameData[i + this.curShape.position.x][j + this.curShape.position.y] = curData[i][j];
}
}
this.refreshGame(gameData, gameDivs);
};
Shapes.prototype.refreshGame = function(gameData, gameDivs) {
for (let i = 0; i < gameData.length; i++) {
for (let j = 0; j < gameData[0].length; j++) {
switch (gameData[i][j]) {
case 0:
gameDivs[i][j].className = 'none';
break;
case 1:
gameDivs[i][j].className = 'done';
break;
case 2:
gameDivs[i][j].className = 'current';
break;
default:
}
}
}
};
我们也要有一个游戏的js文件,生成gameData和对应的DOM,并对Shapes类的实例进行操作:
/**
game.js
*/
import Shapes from './shapes.js';
// 游戏区域数组
let gameData = [
[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, 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],
];
// 游戏区域DOM
let gameDivs = [];
// 根据游戏区域数组创建并插入DOM
for (let i = 0; i < gameData.length; i++) {
const gameDiv = [];
for (let j = 0; j < gameData[0].length; j++) {
const newNode = document.createElement('div');
newNode.className = 'none';
newNode.style.top = (i * 20) + 'px';
newNode.style.left = (j * 20) + 'px';
document.querySelector('#game').appendChild(newNode);
gameDiv.push(newNode);
}
gameDivs.push(gameDiv);
}
// 创建方块实例并运行
const shapes = new Shapes();
shapes.generateShape(gameData, gameDivs);
// 绑定键盘事件
document.addEventListener('keydown', e => {
if (e.keyCode === 37) {
shapes.goLeft(gameData, gameDivs);
}
if (e.keyCode === 39) {
shapes.goRight(gameData, gameDivs);
}
if (e.keyCode === 40) {
shapes.goDown(gameData, gameDivs);
}
if (e.keyCode === 32) {
shapes.rotateShape(gameData, gameDivs);
}
}, false);
下面来看方块移动的具体逻辑。先来看下落:
/**
shapes.js
*/
……
Shapes.prototype.goDown = function(gameData, gameDivs) {
if (this.downable(gameData)) {
this.clearBefore(gameData);
this.curShape.position.x++;
this.updateData(this.curShape.data, gameData, gameDivs);
} else {
this.generateShape(gameData, gameDivs);
}
};
在每次移动之前,我们要先去判断一下当前方块是否还能继续向下移动(移动到最底部或者碰到其他已固定住的方块时,不能再移动):
/**
shapes.js
*/
……
Shapes.prototype.downable = function(gameData) {
const curData = this.curShape.data;
for (let i = 0; i < curData.length; i++) {
if (i + this.curShape.position.x === gameData.length - 1) {
this.settleData(curData, gameData);
return false;
}
for (let j = 0; j < curData[0].length; j++) {
if (curData[i][j] === 2 && gameData[i + this.curShape.position.x + 1][j + this.curShape.position.y] === 1) {
this.settleData(curData, gameData);
return false;
}
}
}
return true;
};
每向下移动一次,记录方块位置的X方向的值+1,然后以此更新游戏区域数据。不过在此之前,需要先清空之前的数据:
/**
shapes.js
*/
……
Shapes.prototype.clearBefore = function(gameData) {
const curData = this.curShape.data;
for (let i = 0; i < curData.length; i++) {
for (let j = 0; j < curData[0].length; j++) {
gameData[i + this.curShape.origin.x][j + this.curShape.origin.y] = 0;
}
}
};
当它移动到最底部或触碰到其他方块的时候,需要固定住不可再被移动,此时需要有一个settleData方法,将游戏区域该形状的值由2变更为1:
/**
shapes.js
*/
……
Shapes.prototype.settleData = function(curData, gameData) {
for (let i = 0; i < curData.length; i++) {
for (let j = 0; j < curData[0].length; j++) {
if (gameData[i + this.curShape.origin.x][j + this.curShape.origin.y] === 2) {
gameData[i + this.curShape.origin.x][j + this.curShape.origin.y] = 1;
}
}
}
};
同理我们可以得出向左移动的方法、判断是否可以左移的方法;向右移动的方法、判断是否可以向右移动的方法:
/**
shapes.js
*/
……
Shapes.prototype.goLeft = function(gameData, gameDivs) {
if (this.leftable(gameData)) {
this.clearBefore(gameData);
this.curShape.origin.y--;
this.updateData(this.curShape.data, gameData, gameDivs);
}
};
Shapes.prototype.leftable = function() {
const curData = this.curShape.data;
for (let i = 0; i < curData.length; i++) {
for (let j = 0; j < curData[0].length; j++) {
if (j + this.curShape.origin.y < 1) return false;
}
}
return true;
};
Shapes.prototype.goRight = function(gameData, gameDivs) {
if (this.rightable(gameData)) {
this.clearBefore(gameData);
this.curShape.origin.y++;
this.updateData(this.curShape.data, gameData, gameDivs);
}
};
Shapes.prototype.rightable = function(gameData) {
const curData = this.curShape.data;
for (let i = 0; i < curData.length; i++) {
for (let j = 0; j < curData[0].length; j++) {
if (j + this.curShape.origin.y >= gameData[0].length - 1) return false;
}
}
return true;
};
此外方块还可以旋转,我们可以通过改变方块实例中的dir属性,用它从rotate属性中取出对应的形状,赋值给当前形状:
/**
shapes.js
*/
……
Shapes.prototype.rotateShape = function(gameData) {
this.curShape.dir = (this.curShape.dir + 1) % 4;
if (this.rotatable(this.curShape.rotate[this.curShape.dir], gameData)) {
this.clearBefore(gameData);
this.curShape.data = this.curShape.rotate[this.curShape.dir];
for (let i = 0; i < this.curShape.data.length; i++) {
for (let j = 0; j < this.curShape.data[0].length; j++) {
gameData[i + this.curShape.origin.x][j + this.curShape.origin.y] = this.curShape.data[i][j];
}
}
this.refreshGame(gameData, gameDivs);
}
};
同样我们需要一个判断当前形状是否可以旋转的方法:
/**
shapes.js
*/
……
Shapes.prototype.rotatable = function(nextDirData, gameData) {
for (let i = 0; i < nextDirData.length; i++) {
if (i + this.curShape.origin.x >= gameData.length - 1) return false;
for (let j = 0; j < nextDirData[0].length; j++) {
if (j + this.curShape.origin.y >= gameData[0].length - 1) return false;
if (j + this.curShape.origin.y < 1) return false;
}
}
return true;
};
基本操作完成了,我们来看消除和计分。
消除分两步:去掉填满的部分;剩下的部分向下移动被填满的层数,我们分别设为removeSolid方法和fall方法:
Shapes.prototype.removeSolid = function(gameData) {
//去掉之前先把当前的gameData保存起来
this.originalData = gameData;
// 循环gameData,如果一整排都被占满,用一个set保存这排的索引,然后把该排的值变成0
for (let i = 0; i < gameData.length; i++) {
if (gameData[i].every(item => item === 1)) {
this.fulfiledLines.add(i);
for (let j = 0; j < gameData[0].length; j++) {
gameData[i][j] = 0;
}
}
}
//最后通过判断set的长度来确定是否有被填满的排,有的话就执行下面的fall方法,并计分
if (this.fulfiledLines.size > 0) {
this.fall(gameData);
this.score++;
}
};
Shapes.prototype.fall = function(gameData) {
for (let i = 0; i < this.originalData.length; i++) {
for (let j = 0; j < this.originalData[0].length; j++) {
if (i + this.fulfiledLines.size < this.originalData.length && gameData[i][j] === 1) {
gameData[i + this.fulfiledLines.size][j] = this.originalData[i][j];
}
}
}
this.fulfiledLines.clear();
};
整个俄罗斯方块的逻辑到这里就讲完了,完整的代码点击这里:https://github.com/PengHoliday/Teris
网友评论