纯前端canvas俄罗斯方块小游戏实现

小时候打的第一款游戏,说起来大概就是俄罗斯方块了,这种游戏老少咸宜,设计的非常巧妙,不高冷,不装逼,之前查询过这个游戏的过往,发现那些年火爆经典的游戏中,俄罗斯方块的火爆经典程度简直超乎我的想象。其实仔细想下,其实我们的确可以自己来实现一个类似的游戏。于是乎,经过多天的构思和参考别人写过的demo,我也自己实现了一遍,下面我就来谈谈自己的俄罗斯方块。

我的俄罗斯方块

功能点罗列

  • 地图数据结构
  • 方块数据结构
  • 地图生成
  • 地图绘制
  • 地图更新
  • 方块创建
  • 方块擦除
  • 方块下落
  • 方块的方向键操作
  • 方块变形
  • 方块满行消除
  • 方块触底检测
  • 方块左右碰撞检测

当然还有canvas元素的处理,这里就不说了。

地图数据结构

使用0, 1这样的数字来代表有无方块,地图上无方块时,就都是0. 示例如下:

[
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
]

方块数据结构

跟地图类似,使用0,1来代表形状,总结归纳出方块的几种情况:

[
    [1, 1, 1, 1]], // 一字形
    [[1, 1], [1, 1]], // 田字形
    [[1, 1, 0], [0, 1, 1]], // z字形
    [[0, 1, 1], [1, 1, 0]], // 反z字形
    [[1, 0, 0], [1, 1, 1]], // L字形
    [[0, 0, 1], [1, 1, 1]], // 反L字形
    [[0, 1, 0], [1, 1, 1]] // T字形
]

地图生成

我们将地图控制的变量明确出来,这样就可通过设置控制地图的生成。 这些变量是,多少行,多少列,一个格子多大,格子间距多大,方块生成初始化时离x轴起点的位置,方块下落速度, 加速下落速度等。

var tetrisGame = new Tetris({
    row: 12,
    col: 12,
    grid: 30,
    margin: 10,
    offsetX: 4,
    interval: 400,
    fasterInterval: 100
})
tetrisGame.start()

地图绘制

根据地图的数据,借助canvas提供的api(就两个api就可以搞定,分别是fillStyle和fillRect),给不同的数据点绘制不同的颜色。

// 地图绘制
render: function () {
    var map = this.map
    var mRowLen = map.length
    var mColLen = map[0].length
    var margin
    var grid = this.etting.grid
    margin = this.setting.margin
    for (var i = 0; i < mRowLen; i++) {
        for (var j = 0; j < mColLen; j++) {
            if (!map[i][j]) {
                this.ctx.fillStyle = 'grey'
            } else if (map[i][j] === 1) {
                this.ctx.fillStyle = 'orange'
            }
            this.ctx.fillRect(j * (grid + margin), i * (grid + margin), grid, grid)
        }
    }
}

地图更新

方块的移动需要地图做更新操作,方块就是地图中的一部分,它的移动其实就是每个格子状态根据一定的算法来改变,达到动画的效果的。因此地图或者说游戏的更新,就是根据方块当前的位置和方块本身的数据结构来更新的。

方块创建

方块的生成是随机的,利用js的随机数以及预定义好的方块数据结构,很容易做到。然后根据我们预设的方块的初始化的位置,就可以在地图中生成。

方块擦除

方块是随着时间运动的,直到触底。因此在运动的时候,我们根据时间的特性,下一个状态的方块来临之前,我们将上一个状态的方块清掉,这里注意的是,只清除那些格子有形的部分,否则会出现当与其他已定方块耦合时,将已定方块的某些部分也清除了。

方块下落

方块下落使用定时器来处理,每一个时间点,做一次清除然后增加方块y轴的值,然后再更新地图。同时在下落的时候,需要判断是否落地。

方块的方向键操作

根据keydown事件和keyCode的值,我们给不同的方向键提供不同的处理方法。向上时,我们做方块变形,向下时,我们加速下落,左右键做左右移动操作。

// 方块移动及变形操作
enableKeyControl: function () {
    var _this = this
    document.onkeydown = function (e) {
        switch (e.keyCode) {
            case 37: // 向左
                if (!_this.borderTest(_this.curBlock, -1)) {
                    _this.clearBlock()
                    _this.x--
                    _this.updateMap()
                }
                break
            case 39: // 向右
                if (!_this.borderTest(_this.curBlock, 1)) {
                    _this.clearBlock()
                    _this.x++
                    _this.updateMap()
                }
                break
            case 38: // 向上即变形
                _this.clearBlock()
                _this.transform()
                _this.updateMap()
                break
            case 40: // 向下即加速
                if (!_this.onkeydownFlag) {
                    _this.onkeydownFlag = true
                    clearInterval(_this.timer)
                    _this.fall(_this.setting.fasterInterval)
                }
                break
        }
    }
    document.onkeyup = function (e) {
        if (e.keyCode === 40) {
            _this.onkeydownFlag = false
            clearInterval(_this.timer)
            _this.fall(_this.setting.interval)
        }
    }
}

方块变形

方块变形就是方块的旋转,我们通常喜欢顺时针来旋转,比较舒服,当然你喜欢逆时针,也可以做的。我是用的顺时针。所以根据旋转的规律,就可以根据旋转前方块的数据结构得出旋转后的数据结构。这里也需要注意一点,是否可以旋转,需要判断,因为有时候一旋转出了边界,一旋转和底部实体融合了,等等。判断的方法是,方块的数据暂时不变更,我们将旋转后的数据,去做测试,判断是否触底或超出边界,即去做方块触底检测和左右方向碰撞检测,如果没有碰撞,就更新方块的数据,如果有,就不允许更新方块数据。

// 方块变形
transform: function () {
    var result = []
    var curBlock = this.curBlock
    var blockRowLen = curBlock.length
    var blockColLen = curBlock[0].length
    for (var i = 0; i < blockColLen; i++) {
        result.push([])
        for (var j = 0; j < blockRowLen; j++) {
            result[i][blockRowLen - j - 1] = curBlock[j][i]
        }
    }
    if (
        !this.groundTest(result) &&
        !this.borderTest(result, -1, true) &&
        !this.borderTest(result, 1, true)
    ) this.curBlock = result
}

方块满行消除

满行的条件触发是以方块落地开始的,所以落地判断成功,就应该需要去判断是否满行,然后做相应的处理。满行的判断,是通过遍历整个地图,看是否有那么些行的格子里都填满了,如果是就需要消除,消除就是在地图的数据结构中,干掉这一样,当然,这个时候的同时,我们可以在地图的最前面行unshift一个充满空格子的空行。

方块触底检测

触底分为两种情况,第一种,是最底部,这个很好判断。第二种,是与其他已经落下的方块之间的判断,看是否还需要往下走。第二种判断方法是,先判断方块的最底部那一行,如果有实体的地方对应的正下方的地图的格子也是实体,那么就不应该再下落,也就是说触底了,如果没有,就去对底部没有实体的那些列去做while循环,向上找到方块的实体格子,这个是总能找到的,因为方块的设计缘故,找到后,就也做类似的判断,看这个方块处的格子对应的正下方地图的格子是否是实体,进而判断是否触底。

方块左右碰撞检测

左右碰撞也分为两种,一种是边界,一种是与其他方块。这里碰撞后,下落还是继续的,不是停止。这个地方的判断,归根结底,其实和下落时与其他方块的判断很类似,只是判断的时候换了一个方向而已。

啦啦啦,终于写完啦。这个简易的俄罗斯方块的代码我上传到了github,看源码请参考https://github.com/Andrewuetyang/tetris

发表评论

电子邮件地址不会被公开。 必填项已用*标注