清明假期期间, 闲的无聊, 就做了一个小游戏玩玩, 目前游戏逻辑上暂未发现 bug, 只不过样子稍微丑了一些 -.-
项目地址: https://github.com/Jiasm/tetris
在线 Demo: http://blog.jiasm.org/tetris/?width=16&height=40 (修改 URL 参数可以调整难度)
整体分成三块进行开发, 使用面向对象式编程进行开发(其实我更喜欢用函数式编程, 但苦于游戏的一些状态用对象来存储会更直观一些):
Game:
负责生成新的方块
负责方块移动的处理
方块触底的判断
移除满足清除条件的行
Render:
负责用 Game 的数据来渲染整个游戏界面
Controller:
负责接受用户输入 (上下左右各种操作) 并处理
向用户反馈当前游戏的状态
这样分层带来了一个好处, 我们游戏的逻辑 Game 模块并不依赖于当前程序运行的环境, 而 Render 可以是 Canvas,DOM, 甚至是控制台输出. 我们要移植到其他平台, 只需要修改 Render 即可.
项目结构
忽略了一些与游戏没有直接关系的结构
.
model
Brick.js
Game.js
index.js
utils
buildEnum.js
deepCopy.js
getShape.js
index.js
lineIndex.js
matrixString.js
rotateArray.js
enum
gameType.js
index.js
pointType.js
data
shapes.js
controller
index.js
view
RenderCanvas.js
index.js
各目录下的 index.js 是为了方便同时引用多个文件, 大致长这个样子:
- export { default as model1 } from './model1'
- export { default as model2 } from './model2'
然后我们就可以在用到的地方写:
import { model1, model2 } from './XXX'
model
这里是游戏的核心逻辑所在位置.
像俄罗斯方块这种的矩阵类游戏, 存储数据最合适的方法就是一个二维数组了.
为了更直观一些, 我们选择了游戏的高度作为第一层数组的长度:
- matrix = new Array(height).fill(new Array(width))
- // width: 2 height: 4
- [
- [ 1, 1],
- [ 1, 1],
- [ 1, 1],
- [ 1, 1]
- ]
而且这样选择在一些逻辑处理上也会更方便一些:
下移操作时, 我们只需改变元素的第一层下标
判断是否触底时, 我们只需将当前下标 + 1 判断是否有元素即可
我们对数组中的元素进行了定义:
0: 空, 表示当前坐标为空白
1: 新的方块, 表示当前活动的方块
2: 老的方块, 已经触底固定的方块
接下来, 我们就遇到了一个问题, 如何处理方块的放置.
我们知道, 游戏会不停的向棋盘中加载新的方块.
如果我们每次处理下移的时候, 都将当前二维数组中对应的方块元素移除, 然后在塞入到新的位置, 未免太过繁琐了.
所以我们在初始化数据时, 初始化两个二维数组.
当我们加载一个新的方块后, 将方块对应的元素塞入其中的一个二维数组.
然后等到我们有进行其他的操作时, 比如左右移动, 向下之类的.
我们直接使用第二个二维数组覆盖到当前的数组中去, 然后再将更改下标后的方块塞入数组.
这样在数据上, 我们就完成了方块的移动.
- class Game {
- init () {
- // 初始化两个矩阵
- this.matrix = [[], []]
- this.oldMatrix = [[], []]
- }
- move () {
- // 重置当前矩阵数据
- this.matrix = deepCopy(this.oldMatrix) // 解除引用
- // 加载方块数据
- this.matrix[y][x1] = 1
- this.matrix[y][x2] = 1
- }
- }
左右移动的处理
左右的移动不能像向下移动一样, 单纯的下标 + 1.
我们需要判断当前的操作是否有效.
比如右侧如果遇到了障碍物或者到达边缘, 我们肯定是不能够再进行移动的.
- // blend 为活动砖块的形状描述 [[1, 1, 1], [0, 1, 0]] 类似这样的结构
- if (
- x>= width - brickWidth ||
- blend.some((row, rowIndex) => {
- let _pos = oldMatrix[y + rowIndex]
- return row && row[brickWidth - 1] && _pos && _pos[x + brickWidth]
- })
- )
- return // 右侧有障碍物, 无法移动
使用类似这样的逻辑进行判断, 保证当前方块向右移动后不会覆盖之前的方块.
快速向下的处理
我看有些游戏实现的, 貌似下降触发只是加速下降而已(这种情况只需要改变定时下降的速度即可)-.- 这里的实现是, 直接触底
所以就会遇到一个问题, 当前砖块最多可以下降到什么位置?
- [1, 1, 1]
- [0, 0, 0]
- [0, 2, 0]
- [2, 2, 2]
就像这样的一个数据, 0|2 这两列都可以向下移动两列, 但是这样就会导致中间一列的重叠.
我们一定要取出下降幅度最小的那个值.
所以我们就要算出最后一行 1 的下标以及第一行 2 的下标, 将这两个下标进行相减, 最小值即为我们当前方块可下降的距离.
旋转方块的处理
旋转方块应该是游戏中比较复杂的一块逻辑了.
绝不是仅仅简单的将方块的二维数组由行改为列, 在有些时候, 我们还需要判断方块是否可以进行旋转.
就像这样的, 中间的绿色长条是不能够进行旋转的.
所以我们要先拿到旋转后的数据, 来与当前游戏中的数据进行比较, 检验是否会出现重叠的情况, 如果出现了, 则表示不能够进行旋转.
触底检测
每完成一个移动的动作后, 我们都需要进行方块的触底检测.
也就是判断当前方块下, 是否已经有元素占位, 如果有的话, 则表示已经触底了, 当前元素就会被固定进矩阵数组中.
同样的, 我们在判断时, 不需要将方块所有的下标都检查一遍, 只需要检查最底部一层的有效元素即可.
- [1, 1],
- [0, 1],
- [0, 1],
像这样的一个方块, 我们仅需要判断第一列的第二行 & 第二列的第四行是否有元素即可完成检查.
移除行
当某一行被填满元素后, 我们就要将它进行移除.
在触底检测触发后, 如果有方块被固定进数组, 此时我们再进行移除行的操作.
因为如果没有新的方块进入, 移除行的这步操作就不是必要的.
同时, 得分的计数也应该在此处进行, 我们将移除的行数进行记录, 获取到的行数便是得分了.
至此, 所有有关矩阵数据的操作就结束了.
Game 对象只去维护这么一个二维数组, 对象本身不包含任何游戏相关的操作, 只会在被调用时进行对应的处理.
然后生成新的二维数组.
utils
这里放置了一些比较通用的方法, 用来提高开发效率使用.
比如获取方块最底部一层的下标之类的工具函数.
enum
存放了一些状态的枚举, 游戏状态以及方块所对应的状态, 类似这样的数据:
- {
- empty: 0,
- newBrick: 1,
- oldBrick: 2
- }
- data
存放了游戏中各种使用到的方块信息.
正方形, 梯形之类的方块在二维数组中所对应的描述.
controller
就是上边我们所说的, 用来与用户交互的模块, 由 Controller 来获取游戏相关的信息, 并调用 Render 进行渲染.
监听键盘事件, 在页面中渲染一些控制按钮.
以及定时触发 Game 的下落方法.
view
游戏界面的渲染部分, 目前选定的是使用 canvas, 所以只写了 RenderCanvas.
在渲染的这部分, 稍微做了一些优化处理, 将活动中的方块与固定的方块进行分开渲染.
这样在用户操作上下左右移动时, 并不会重新渲染整个游戏布局, 而只是渲染活动方块的 canvas.
小记
两天多的时间进行开发, 其中有半天时间在修复 FlowType 的 Warning 提示...
搞完了以后, 觉得实现这个的主要难点就在于方块旋转 & 触底的判断这里了.
能够清晰的管理游戏对应的二维数组, 这个游戏开发起来就会很顺畅.
界面还有待优化.
来源: https://www.cnblogs.com/jiasm/p/8733356.html