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

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

作者: 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

相关文章

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

    上一篇讲解了具体思路,这一篇上具体的代码。首先是七种形状的类: 剩下六个类的结构类似,这里就不再赘述。接下来是这几...

  • 2017-08-14

    面向对象编程用对象的思想去写代码,就是面向对象编程-面向过程-面向对象面向对象编程的特点1.抽象 抽取一样的东西...

  • 面向对象:封装和继承、多态

    面向对象解决的问题 并不是:封装、继承、多态 而是写代码的套路问题(定势思维) 面向对象的继承,用 extend ...

  • Python的面向对象

    Python面向对象详解 引言:   面向对象是一种编程思想,面向对象是一种对现实世界理解和抽象的方法。它的编程思...

  • (三)python基础之:面向对象基础

    一、面向对象 面向对象是一种符合人类思维的编程方法,现实世界就是由对象和对象构成的,所以我们可以用代码将现实世界映...

  • 面向对象的编程(1)

    一.什么是面向对象编程 1.用对象的思想去写代码,就是面向对象编程 我们以前常用的编程方式可以是比较过程式的写法,...

  • 快速初始化数组方法

    不就之前看看别人60行代码写一个俄罗斯方块,研究了代码,主要是使用特殊方法节省了代码,我这里写出快速生成2二维数组...

  • s面向对象的写法

    js面向对象的写法一、在html中引入该js文件,使用时: 二、一般写一个较大的模块的js代码时,采用这种方法写j...

  • 用面向对象的方法写一个俄罗斯方块(一):设计思路

    俄罗斯方块的逻辑稍微有些复杂,实现的方法也有很多,这里分享其中一种。首先我们得有数据驱动的思维,把这个游戏抽象成两...

  • 第二周 面向对象

    第二周 面向对象 2019.8.5 类 包含属性(准确说是field 域)、方法、构造方法、代码块、内部类/接口....

网友评论

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

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