用面向对象的方法写一个俄罗斯方块(二):代码详解

2018-11-01  本文已影响45人  HolidayPeng

上一篇讲解了具体思路,这一篇上具体的代码。首先是七种形状的类:

/**
 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

上一篇 下一篇

猜你喜欢

热点阅读