场景展开

我们在写贪吃蛇的时候,我们已经创建了 ground 游戏舞台对象,那么除了游戏舞台,我们还有地板、围墙、蛇头、蛇身、食物这些东西。
按照昨天的套路,我们能写出如下的代码:

  1. var Floor = tool.extends(Square); //地板,由多个小方块组成,非单例
  2. var Wall = tool.extends(Square); // 围墙,由多个小方块组成,非单例
  3. var SnakeHead = tool.single(Square); //蛇头,有且仅有一个蛇头,单例模式
  4. var SnakeBody = tool.extends(Square); //蛇身,由多个小方块组成,非单例
  5. var Snake = tool.single(); //蛇,起到整体控制的作用,只需要一个即可,单例模式
  6. var Food = tool.single(Square); //食物,全程只出现一个食物,所以只用实例化一次,单例模式

一下子,又多了很多的构造函数,那么思考,像昨天的游戏舞台倒也还好,整个游戏就一个,所以 new 一次就完事了。
但是其他的元素就没那么省事儿了。
例如 Floor 地板,Floor 由多个小方块组成,所以,创建每一块地方的代码变成了如下:

  1. // 第一块地板
  2. var floor1 = new Floor(positionX + squareWidth, positionY + squareWidth, squareWidth, squareWidth);
  3. // 同一排的第二块地板
  4. var floor2 = new Floor(positionX + squareWidth * 2, positionY + squareWidth, squareWidth, squareWidth);
  5. // 同一排的第三块地板
  6. var floor3 = new Floor(positionX + squareWidth * 3, positionY + squareWidth, squareWidth, squareWidth);

如果是创建 Wall 围墙,那么代码就变成了:

  1. // 第一块围墙
  2. var wall1 = new Wall(positionX + squareWidth, positionY + squareWidth, squareWidth, squareWidth);
  3. // 同一排的第二块围墙
  4. var wall2 = new Wall(positionX + squareWidth * 2, positionY + squareWidth, squareWidth, squareWidth);
  5. // 同一排的第三块围墙
  6. var wall3 = new Wall(positionX + squareWidth * 3, positionY + squareWidth, squareWidth, squareWidth);

那么,这里其实就涉及到了一个问题:批量生产
在此场景中,我们就可以使用设计模式中的工厂模式

什么是工厂模式?

工厂模式是我们最常用的一种实例化对象模式,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式,使用工厂方法代替了 new 的操作。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
工厂模式 - 图1

书写 SquareFactory 工厂

了解了什么是工厂模式后,今天的第一件事情就是来书写这个工厂。
因为只有创建了工厂,才能创建地板、围墙和蛇。
仔细想想,无论是地板,还是围墙以及蛇,其实都是有一个一个小方块组成的。
所以我们的工厂其实就是能够为我们创建一个一个的小方块。
我们期望这个工厂大致是这样的:

  1. // 在坐标为 3,1 的地方创建一个深粉色的蛇头的小方块
  2. SquareFactory.create('SnakeHead', 3, 1, 'deeppink');
  3. // 在坐标为 2,1 的地方创建一个绿色的蛇身小方块
  4. SquareFactory.create('SnakeBody', 2, 1, 'green');
  5. // 在坐标为 x,y 的地方创建一个黑色的墙的小方块
  6. SquareFactory.create('Wall', x, y, 'black');
  7. // 在坐标为 x,y 的地方创建一个灰色的地板的小方块
  8. SquareFactory.create('Floor', x, y, 'gray');

也就是说,这个叫做 SquareFactory 的工厂,有一个 create 的方法,语法如下:

  1. SquareFactory.create('方块类型', x坐标, y坐标, '颜色');

首先,我们创建这个工厂,不用想得太复杂,就是一个空函数,如下:

  1. // 创建方块工厂
  2. function SquareFactory() { } // 厂房

接下来这个工厂要生产不一样的小方块,那么我就要这个工厂有不同的流水线,每条流水线生产对应的方块
工厂模式 - 图2
那么具体落实到代码怎么写呢?
很简单,就是给 SquareFactory 这个工厂函数的原型上面挂方法即可,例如:
生产地板的流水线

  1. // 创建地板的流水线
  2. // 其实就是给 SquareFactory 原型对象上挂上了一个 Floor 方法
  3. // 在 SquareFactory 原型对象上的 Floor 方法里面实例化 Floor 对象
  4. SquareFactory.prototype.Floor = function (x, y, color) {
  5. var floor = new Floor(x, y, squareWidth, squareWidth); // 实例化 Floor 对象
  6. this.init(floor, color, ''); // 设置该 DOM 元素的 CSS 信息
  7. return floor; // 返回该对象
  8. }

生产围墙的流水线

  1. // 创建围墙的流水线
  2. // 其实就是给 SquareFactory 原型对象上挂上了一个 Wall 方法
  3. // 在 SquareFactory 原型对象上的 Wall 方法里面实例化 Wall 对象
  4. SquareFactory.prototype.Wall = function (x, y, color) {
  5. var wall = new Wall(x, y, squareWidth, squareWidth); // 实例化 Wall 对象
  6. this.init(wall, color, ''); // 设置该 DOM 元素的 CSS 信息
  7. return wall; // 返回该对象
  8. }

可以看到,上面的流水线,其实就是在 SquareFactory 这个工厂函数的原型上面挂了对应的 FloorWall 方法,每个方法接收 3 个参数,x 坐标、y 坐标以及颜色。
接收到这些信息后,创建对应类型的对象,例如是 Floor 流水线就 new FloorWall 流水线就 new Wall,创建了对应类型的对象后调用 init 方法来初始化这些小方块的 DOM 信息。
所以 SquareFactory 工厂还应有一个 init 方法,如下:

  1. SquareFactory.prototype.init = function (square, color, action) {
  2. square.viewContent.style.position = "absolute";
  3. square.viewContent.style.width = square.width + "px";
  4. square.viewContent.style.height = square.height + "px";
  5. square.viewContent.style.background = color;
  6. /*
  7. 让 x 代表列,y 代表行
  8. left = 列(x)*宽度;
  9. top = 行(y)*高度;
  10. */
  11. square.viewContent.style.left = square.x * squareWidth + "px";
  12. square.viewContent.style.top = square.y * squareWidth + "px";
  13. }

工厂创建完毕后,最后一步就是让外界来使用这个工厂,批量生产小方块。
前面我们也说了,我们期望使用下面的形式:

  1. SquareFactory.create('方块类型', x坐标, y坐标, '颜色');

所以在 SquareFactory 上面有这么一个 create 方法。但是这个 create 方法不是挂在 SquareFactory.prototype 原型上面的,而是直接挂在 SquareFactory 上面,这样就类似一个静态方法,直接通过类名.方法名的形式来调用,不用实例化对象。
那么, create 方法里面具体做什么呢?
其实就是根据 create 方法的第一个参数,调用工厂的不同流水线,生成了对象后返回给外界。如下:

  1. SquareFactory.create = function (type, x, y, color) {
  2. // 预警处理,如果传递过来的 type 类型不存在,抛出一个错误
  3. if (!SquareFactory.prototype[type]) {
  4. throw new Error('no this type');
  5. }
  6. // 创建一个工厂实例,然后调用工厂对应的流水线方法
  7. return new SquareFactory()[type](x, y, color);
  8. }

在舞台中绘制地板和围墙

有了工厂函数后,我们就可以实际的来使用这个工厂做事情了。
首先来画地板和围墙,对应的代码片段:

  1. // 在有了工厂之后,我们就可以通过工厂来创建地板和围墙
  2. this.squareTable = []; // 该数组用于存放围墙和地板,是一个二维数组,数组里面的每一个元素(数组)保存了这一行的小方块信息
  3. // 外层循环走的是行数(对应的是 y 轴坐标)
  4. for (var y = 0; y < tr; y++) {
  5. this.squareTable[y] = new Array(td);
  6. // 里层循环走的是列数(对应的是 x 轴坐标)
  7. for (var x = 0; x < td; x++) {
  8. if (x == 0 || x == td - 1 || y == 0 || y == tr - 1) {
  9. // 这个条件成立说明走到了围墙身上
  10. var newSquare = SquareFactory.create('Wall', x, y, 'black'); //围墙
  11. } else {
  12. var newSquare = SquareFactory.create('Floor', x, y, 'gray'); //地板
  13. }
  14. this.squareTable[y][x] = newSquare; // 将这个小方块放入到 squareTable,方便后面操作
  15. this.viewContent.appendChild(newSquare.viewContent); // 将这个小方块添加到游戏场景上面
  16. }
  17. }

地板和围墙就画好了,接下来就准备画蛇。
但是在画蛇之前,有一件事情要提前明确一下,就是一个游戏舞台块上面,只能有一个方块。也就是说,某一块游戏舞台块上面放了地板的方块,那么蛇移动到这里时,就要先把地板的方块删除了,然后放上蛇的方块。如下图:
工厂模式 - 图3
那么,此时就要求我们的 ground 舞台对象,具有添加和删除方块的能力。如下:
添加方块

  1. // 添加一个小方块
  2. ground.append = function (square) {
  3. this.viewContent.appendChild(square.viewContent); // 添加到 DOM 里
  4. this.squareTable[square.y][square.x] = square; // 添加到数据里
  5. }

删除方块

  1. // 删除一个小方块
  2. ground.remove = function (x, y) {
  3. var curSquare = this.squareTable[y][x];
  4. this.viewContent.removeChild(curSquare.viewContent); // 删除 DOM
  5. this.squareTable[y][x] = null; // 删除数据
  6. }

绘制蛇头和蛇身

今天的最后一个任务,就是绘制蛇头和蛇身了。
和前面绘制地板和围墙的方式基本一样,通过 SquareFactory 工厂创建小方块,添加到对应场景即可。
但是在添加之前要注意,先将 ground 上面的地板删除掉。代码片段如下:

  1. // 蛇头
  2. var snakeHead = SquareFactory.create('SnakeHead', 3, 1, 'deeppink');
  3. var snakeBody1 = SquareFactory.create('SnakeBody', 2, 1, 'green');
  4. // 蛇尾
  5. var snakeBody2 = SquareFactory.create('SnakeBody', 1, 1, 'green');
  6. snake.head = snakeHead; // 存储蛇头方块
  7. snake.tail = snakeBody2; // 存储蛇尾方块
  8. // 删除游戏场景对应的地板,放上蛇头和蛇尾
  9. ground.remove(snakeHead.x, snakeHead.y);
  10. ground.append(snakeHead);
  11. ground.remove(snakeBody1.x, snakeBody1.y);
  12. ground.append(snakeBody1);
  13. ground.remove(snakeBody2.x, snakeBody2.y);
  14. ground.append(snakeBody2);