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