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

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

作者: HolidayPeng | 来源:发表于2018-11-01 18:02 被阅读45次

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

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/jmmxxqtx.html