在 JavaScript 版俄罗斯方块 中曾提到,由于临时起意,因此项目结构和不少命名都比较混乱。另外,计分等功能也未实现。此次抽空实现计分和速度设置,并在此以前进行了简单的重构。javascript
项目结构上主要是将原来的 app
改名为 src
,表示脚本和 less 源码都在这里。固然原来存放脚本源码的 app/src
也相改名为 src/scripts
。css
[root> |-- index.html : 入口 |-- js/ : 构建生成的脚本 |-- css/ : 构建生成的样式表 |-- lib/ : bower 引入的库 `-- src/ : 前端源文件 |-- less : 样式表源文件 `-- scripts : 脚本(es6)源文件
除此这外,基 scripts
中细分了模块,在重构的过程当中建立了 model
和 tetris
两个子目录。html
重构以前先进行了简单的结构分析,主要是将几个模块划分出来,放在 model
目录下。重构和写新功能的过程当中建立了 tetris
目录,这里放的是功能类和辅助类。然而最主要的功能仍是在 scrits/tetris.js
中。前端
下面是一开始分析模型时画的图:java
写程序,重构老是很是须要但也很是容易出错的部分。俄罗斯方块的整个重构的过程从 源码中 working 分支 的提交日志中能够看到。git
关于重构,最重要的一点是:改变代码结构,但不改变逻辑。也就是说,每一步重构都要在保证原有业务逻辑的基础上对代码进行修改——虽然并非 100% 能达到,但要尽最大努力遵循这个原则,才不会在重构的过程当中产生莫名其妙的 BUG。关于这一点,应该是在《重构 改善既有代码的设计》一书中提到的。es6
虽然不肯定改代码不改逻辑的原则是在 《重构 改善既有代码的设计》 这本书中提到的,可是这本书仍是推荐你们去看一看。重构对于开发有着很重要的做用,不太重构过程当中涉及到不少设计模式,因此设计模式也是须要读一读的。typescript
在重构的过程当中,我为全部类都加入了私有成员定义。这样作的目的是避免在使用它们的时候,不当心访问了不应访问的成员(通常指不当心改写,但有时候不当心取值也可能形成错误)。segmentfault
关于私有成员这个话题,我曾在 ES5 中模拟 ES6 的 Symbol 实现私有成员 中讨论过。在这里我没有用那篇博客中提到的方法,而是直接使用了 Symbol。Babel 对 Symbol()
作了兼容处理,若是是在支持 Symbol
的浏览器上,会直接使用 ES6 的 Symbol;不支持的,则用 Babel 实现的一个模拟的 Symbol 代替。设计模式
加入了私有化成员的代码看起来有些奇怪,好比下面这个简单的 Point
类的代码。如下的实现主要是为了(尽量)保证 Point
对象一但生成,其坐标就不能随意改动——也就是 Immutable。
const __ = { x: Symbol("x"), y: Symbol("y") }; export default class Point { constructor(x, y) { this[__.x] = x; this[__.y] = y; } get x() { return this[__.x]; } get y() { return this[__.y]; } move(offsetX = 0, offsetY = 0) { return new Point(this.x + offsetX, this.y + offsetY); } }
这段代码还好,在写了不少
const __ = { ... }
以后,我忽然以为很是思念 TypeScript。在 TypeScript 中只须要简单的private _x;
就能够申明私有成员。TypeScript 中申明的私有成员仅限于静态检查,最终生成的 JavaScript 脚本中,这些成员均可以在外部访问。不过不要紧,由于静态检查能够更好的帮咱们规避错误。
只有 scripts/model
下面实现的几个类是比较纯粹的模型,除了用于存储数据的字段(Field)和存取数据的属性(Property)以外,方法也都是用于存取数据的。
model/point.js
和 model/blockpoint.js
里分别实现了用于描述点(小方块)的两个类,区别仅仅在于 BlockPoint
多一个颜色属性。实际上 BlockPoint
是 Point
的子类。在 ES6 里实现继承太容易了,下面是这两个类的结构示意
class Point { constructor(x, y) { // .... } } class BlockPoint extends Point { constructor(x = 0, y = 0, c = "c0") { super(x, y); // .... } }
继氶的实现关键就两点须要注意:
extends
关键字实现继承constructor
,记得第一句话必定要调用父类的构造函数 super(...)
。Javaer 应该很熟悉这个要求的。Form
在这里不是“表单”的意思,而是“形状、外形”的意思,表示一个方块图形(Shape)经过旋转造成的最多4 种形态,每一个 Form
对象是其中一种。因此 Form
实际上是一组 Point
组成的。
上一个版本中没有定义 Form
这个数据结构,是在生成 Shape 的时候生成的匿名对象。那段代码看起来特别绕,虽然也能够提取个函数出来,不过如今经过 Form
类的构造函数来生成,不只达到了一样的目的,也把 width
的 height
封装起来了。
Shape
和 SHAPES
跟原来区别不大。SHAPES
的生成代码经过定义 Form
类,简化了很多。而 Shape
类在构建后,也因为成员私有化的缘由,color
和 forms
不能被改变了,只能获取。
除了几个比较纯粹的模型类放在 model
中,主要入口 index.js
和 tetris.js
放在脚本源码根目录下,其它的游戏相关类都是放在 tetris
目录下的。这只是用包(Java概念)或命名空间(C++/C#概念)的概念对源码进行了一个基本的划分。
Block
表示一个大方块,是由四个小方块组成的大方块,它的原型(此原型非 JS 的 Prototype)就是 Shape
。因此一个 Block
会有一个 Shape
原型的引用,同时保存着当前它的位置 position
和形态 formIndex
,这两个属性在游戏过程当中是能够改变的,直接影响着 Block
最终绘制出来的位置和样子。
整有游戏中其实只有两个 Block
,一个在预览区中,另外一个在游戏区定时下落并被玩家操做。
Block
对象下落到底以后就再也不是 Block
了,它会被固化在游戏区。为何要这样设计呢?由于 Block
表示的是一个完整的大方块,而游戏区下方的方块一旦填满一行就会被消除,大方块将不再完整。这种状况有两个方案能够描述:
BlockPoint
,经过矩阵管理。很明显,第二种方法经过二维数组实现,会更直观,程序写起来也会更简单。因此我选用了第二种方法。
Block
除了描述大方块的位置和形态以外,也会配合游戏控制进行一些数据运算和变化,好比位置的变化:moveLeft()
、moveRight()
、moveDown()
等,以及形态的变化 rotate()
;还有几个 fastenXxxx
方法,生成 BlockPoint[]
用于绘制或判断下一个位置是否能够放置。关于这一点,在 JavaScript 版俄罗斯方块 中已经谈过。
BlockFactory
功能未变,仍然是产生一个随机方块。
以前对 Puzzle 和 Matrix 的定义有点混淆,这里把它们区分开了。
Puzzle 用于绘制浏览区和预览区,它除了描述一个指定长宽的绘制区域以外,还有存储着两个重要的对象,block: Block
和 fastened: BlockPoint[]
,也就是上面提到的运动中的方块,和固定下来的若干小方块。
Puzzle 本向不维护 block
和 fastened
,但它要绘制这两个重要数据对象中的全部 BlockPoint
。
Matrix 再也不是一个类,它是两个数据。一个是 Puzzle
中的 matrix
属性,维护着由 <div>
(行) 和 <span>
(单元) 组成的绘制区;另外一个是 Tetris
中的 matrix
属性,维护着一个 BlockPoint
的矩阵,也就是 Puzzle::fastened
的矩阵形态,它更容易经过固化或删除等操做来改变。
因为 Tetris::matrix
在大部分时间是不变的,则 Puzzle
绘制的时候须要的只是其中其中非空部分的列表,因此这里有一个比较好的业务逻辑是:在 Tetris::matrix
变化的时候,从它从新生成 Puzzle::fastened
,由 Puzzle
绘制时使用。
有点遗憾,写此博文的时候发现重构以后忘了实现这一优化处理,仍然是在每次
Tetris::render
的时候都会去从新生成Puzzle::fastened
。不过不要紧,下个版本必定记得处理这个事情。
在重构和写新功能的过程当中,发现了事件的重要性,好些处理都会用到事件。
好比在点击暂停/恢复 和 从新开始 的时候,须要去判断当前游戏的状态,并根据状态的状况来触发究竟是不是真的暂停或从新开始。
又好比,在计分和速度选择功能中,若是计分达到必定程度,就须要触发提速。
上面提到的这些均可以使用观察者模式来设计,则事件就是观察者模式的一个典型实现。要实现本身的事件处理机制其实不难,可是这里能够偷偷懒,直接借用 jQuery 的事件处理,因此定义了 Eventable
类用于封装 jQuery 的事件处理,全部支持事件的业务类均可以从它继承。
封装很简单,这里采用的是封装事件代理对象的方式,具体能够看源代码,一共只有 20 多行,很容易懂。也能够在构造函数中把 this
封装一个 jQuery 对象出来代理事件处理,这种方式能够将事件处理函数中的 this
指向本身(本身指 Eventable 对象)。不过还好,这个项目中不须要关心事件处理函数中的 this
。
在实现 Tetris 中的主要游戏逻辑的时候,发现状态管理并不简单,尤为是加了 暂停/恢复 按钮以后,暂停状态就分为代码暂停和人工暂停两种状况,对于两种状况的恢复操做也是有区别的。除此以外还有游戏结束的状态……因此干脆就定义个 StateManager
来管理状态了。
StateManager
维护着游戏的状态,提供改变状态的方法,也提供判断状态的属性。若是 JavaScript 有接口语法的话,这个接口大概是这样的
interface IStateManager { get isPaused(): boolean; get isPausedByManual(): boolean; get isRestartable(): boolean; get isOver(): boolean; pause(byWhat); resume(byWhat); start(); over(); }
我又开始想念 TypeScript 了
InfoPanel
主要用于积分和速度的管理,包括与用户的交互(UI)。CommandPanel
则是负责两个按钮事件的处理。
说实在的,我仍然认为 Tetris
的代码有点复杂,还须要重构简化。不过尝试了一下以后发现这并非一件很容易的事情,因此就留待后面的版原本处理了。
此次对俄罗斯方块游戏的重构只是一个初步的重构,最初的目的只是想把模型定义清楚,不过也对业务处理进行了一些拆分。模型定义的目的是达到了,可是业务拆分仍然不尽满意。
工做上以前的两个项目都是用的 TypeScript 1.8,虽然是 TypeScript 1.8 有一些坑在那里,可是 TypeScript 的静态语言特性,尤为是静态检查对大型 JavaScript 项目仍是有很大帮助的。以前一直认为 TypeScript 增长了代码量,也下降了 JavaScript 的灵活度,但此次用 ES6 重构俄罗斯方块游戏让我深深的感觉到,这根本不是 TypeScript 的缺点,它至少能够解决 JavaScript 中的这几个问题:
因此,下个版本我准备尝试用 TypeScript 2.0 来改写。