We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
贪婪嗜蛇。好吧其实就是贪吃蛇。
先来看看最后呈现的结果(做的比较简单,界面很low):
这里面涉及到什么东西呢,先来看张UML图: 我们的游戏场景是由一个个小方格组成的,然后这些小方格又可以具体分为蛇头、蛇身、食物、障碍物、地面。蛇头和蛇身组成蛇,障碍物聚合成四周的墙。然后每个方格都有一个公共的接口touch,他是蛇触碰到改方格时候具体的策略。
项目地址:https://lyzsg.github.io/Supercalifragilisticexpialidocious-Snake/
首先我们知道像地板、障碍物、食物等等都继承自方块,所以我们需要实现继承的方法。同时,我们知道食物、蛇头等可以运用到单例模式,所以我们也来一个转单例的方法。我们把这些公共方法单独写在jsUtil.js文件里,便于管理。
首先,继承的话,我们一下子就能想到圣杯模式:
var jsUtil = { // 圣杯模式,无需多言 inherit: function (target, origin) { var temp = function () {}; temp.prototype = origin.prototype; target.prototype = new temp(); target.prototype.constructor = target; } }
但是圣杯模式的继承,我们知道只是会继承父类原型上的属性,而父类身上的属性获取不到。
然而在我们这里,像地板、障碍物等都需要继承自方块这个基类,方块身上有定义了坐标和宽高,如果仅用圣杯模式的话是继承不了方块身上的属性的:
function Square(x, y, width, height) { this.x = x || 0; this.y = y || 0; this.width = width || 100; this.height = height || 100; } function Food() { } var food = new Food(); food.x food.y // undefined undefined
普通的解决方法是,我们可以在Food方法里面通过Square.call(this, x, y, width, height)来让方块的属性也能运用到食物里面。但是这样也未免太过繁琐,我们不仅要给Food写形参,也要修改函数体。
我们希望可以直接通过一个extends方法来实现这种继承: var Floor = jsUtil.extends(Square),简单快捷。(但同时我们也不能自定义Floor的函数体了)
那么怎么实现这个extend方法呢?很简单,只要把我们上面所说的封装起来就好了:
var jsUtil = { // 圣杯模式 inherit: function (target, origin) { var temp = function () {}; temp.prototype = origin.prototype; target.prototype = new temp(); target.prototype.constructor = target; }, // 返回一个构造函数(子类),继承自origin extends: function (origin) { var result = function () { // 当这个构造函数进行new操作时,就会同时继承到origin上面的属性。这里没用call直接用apply origin.apply(this, arguments); } // 圣杯模式继承origin的原型 this.inherit(result, origin); return result; } }
因为在这个项目里面,继承自方块的地板食物这些,也没有一些自己的属性,所以可以这么干。虽然这样最后得到的Floor似乎和Square没什么两样,但是这样能帮我们在一开始理清对象间的关系。
我们再来实现单例,我们需要单例的食物、蛇头的构造函数,所以我们同样要继承方块类自身的属性和原型,最后可以通过 var Food = jsUtil.single(Square) 的到单例的继承的构造函数。
single: function(origin) { var singleResult = (function () { var instance = null; return function () { if(instance) { return instance; } // 继承origin自身属性 origin && origin.apply(this, arguments); instance = this; } }()); // 因为要给最后返回的构造函数继承原型,而且要形成闭包,所以要有一个立即执行函数 origin && this.inherit(singleResult, origin); return singleResult; }
我们再来单体提取出一些配置变量,例如场景的宽高,格子的宽高,游戏难度等等,方便我们灵活修改。也把基类和子类都定义在这里。
// 决定游戏场景的大小和位置 // 横向系数 纵向系数 var XLEN = 30; var YLEN = 30; // 格子宽度 var SQUAREWIDTH = 20; // 游戏场景 广场 坐标 var BASE_X_POINT = 200; var BASE_Y_POINT = 100; // 设置游戏难度 // 蛇的移动速度 var INTERVAL = 300; // 定义 基类 function Square(x, y, width, height, dom) { this.x = x || 0; this.y = y || 0; this.width = width || 100; this.height = height || 100; // dom ,方块默认是div this.viewContent = dom || document.createElement('div'); } // 公共接口 Square.prototype.touch = function () {console.log('touch')} // 其他子类 // 地板 var Floor = jsUtil.extends(Square); // 障碍物 var Stone = jsUtil.extends(Square); // 食物 var Food = jsUtil.single(Square); // 蛇 var Snake = jsUtil.single(); // 蛇身 var SnakeBody = jsUtil.extends(Square); // 蛇头 var SnakeHead = jsUtil.single(Square); // 因为蛇头是单例,所以我们必须给一个方法来改变他的位置 SnakeHead.prototype.update = function (x, y) { this.viewContent.style.left = x * SQUAREWIDTH + 'px'; this.viewContent.style.top = y * SQUAREWIDTH + 'px'; this.x = x; this.y = y; } // 广场 var Ground = jsUtil.extends(Square);
看到这么多方块的种类,不由得我们就想来个工厂模式来管理一下是不是。那就来吧。具体的在工厂模式那篇笔记中已经讲过了,这里就不多说了。
不同的只是我们的子工厂,不再是一个构造函数,而是调用他真正的构造函数(所以更像一个流水线),并且多了一步init,对出厂前的产品进行最后的包装,给他一个实际的样式而已。(不同的方块类型有不同的颜色)
function SquareFactory () {} SquareFactory.create = function (type, x, y, color) { if(typeof SquareFactory.prototype[type] === 'undefined') { throw 'no this type'; } if(SquareFactory.prototype[type].prototype.__proto__ != SquareFactory.prototype) { SquareFactory.prototype[type].prototype = new SquareFactory(); } var newSquare = SquareFactory.prototype[type](x, y, color); return newSquare; } // 出厂前进行最后的包装,给viewContent添加位置和样式 SquareFactory.prototype.init = function (square, color) { square.viewContent.style.position = 'absolute'; square.viewContent.style.left = square.x * SQUAREWIDTH + 'px'; square.viewContent.style.top = square.y * SQUAREWIDTH + 'px'; square.viewContent.style.width = square.width + 'px'; square.viewContent.style.height = square.height + 'px'; square.viewContent.style.backgroundColor = color; } // 子工厂(流水线) SquareFactory.prototype.Floor = function (x, y, color) { var floor = new Floor(x, y, SQUAREWIDTH, SQUAREWIDTH); this.init(floor, color); return floor; } SquareFactory.prototype.Food = function (x, y, color) { var food = new Food(x, y, SQUAREWIDTH, SQUAREWIDTH); this.init(food, color); return food; } SquareFactory.prototype.Stone = function (x, y, color) { var stone = new Stone(x, y, SQUAREWIDTH, SQUAREWIDTH); this.init(stone, color); return stone; } SquareFactory.prototype.SnakeHead = function (x, y, color) { var snakeHead = new SnakeHead(x, y, SQUAREWIDTH, SQUAREWIDTH); snakeHead.update(x, y); // 更新位置 this.init(snakeHead, color); return snakeHead; } SquareFactory.prototype.SnakeBody = function (x, y, color) { var snakeBody = new SnakeBody(x, y, SQUAREWIDTH, SQUAREWIDTH); this.init(snakeBody, color); return snakeBody; }
我们根据配置变量设置的广场坐标和横纵向系数,生成一个广场实例:
var oGround = new Ground(BASE_X_POINT, BASE_Y_POINT, XLEN * SQUAREWIDTH, YLEN * SQUAREWIDTH);
接着,我们给他一个初始化的方法,来给他的viewContent(默认div)添加样式,让广场显示出来:
oGround.init = function () { this.viewContent.style.position = 'absolute'; this.viewContent.style.left = this.x + 'px'; this.viewContent.style.top = this.y + 'px'; this.viewContent.style.width = this.width + 'px'; this.viewContent.style.height = this.height + 'px'; this.viewContent.style.backgroundColor = '#0ff'; document.body.appendChild(this.viewContent); }
我们知道,广场由地板、障碍物、蛇、食物等组合而成,所以我们要在广场里面填充这些东西。把广场拆分为一个一个的小方块,有的方块是地板、有的是墙、有的是蛇、有的是食物。所以,为了方便我们的添加,我们把这个广场看作是一个二维数组,数组下标作为我们的坐标。
例如我们知道广场的四周都是墙壁,即当x = 0 或者 x = XLEN -1 或者 y = 0 或者 y = YLEN - 1 时我们应该给广场填充障碍物。
我们先填充地板和墙:
oGround.init = function () { this.viewContent.style.position = 'absolute'; this.viewContent.style.left = this.x + 'px'; this.viewContent.style.top = this.y + 'px'; this.viewContent.style.width = this.width + 'px'; this.viewContent.style.height = this.height + 'px'; this.viewContent.style.backgroundColor = '#0ff'; document.body.appendChild(this.viewContent); this.squareTable = []; // 一共有 YLEN 行 for(var i = 0; i < YLEN; i ++) { this.squareTable[i] = new Array(XLEN); // 每行有 XLEN 列 for(var j = 0; j < XLEN; j ++) { var newSquare = null; // 第一列 或 最后一列 或 第一行 或 最后一行时,填充墙壁 if(j == 0 || j == XLEN - 1 || i == 0 || i == YLEN - 1) { newSquare = SquareFactory.create('Stone', j, i, 'black'); // 其他填装地板 } else { newSquare = SquareFactory.create('Floor', j, i, 'orange'); } this.squareTable[i][j] = newSquare; // 添加到广场 this.viewContent.appendChild(newSquare.viewContent); } } }
同时,因为我们还要添加食物和蛇,所以需要一个拆地板的功能和添加新内容的功能:
// 根据坐标移除一块的内容 oGround.remove = function (x, y) { this.viewContent.removeChild( this.squareTable[y][x].viewContent ); this.squareTable[y][x] = null; } // 在给定坐标处添加新的内容 oGround.append = function (square) { this.squareTable[square.y][square.x] = square; this.viewContent.appendChild(square.viewContent); }
我们先让蛇出现在场景中。假设一开始蛇有一个蛇头,两段蛇身,我们通过init方法将其初始化:
var snake = new Snake(); // 记录蛇头和蛇尾,方便后面实现蛇的移动 snake.head = null; snake.tail = null; snake.init = function (ground) { var snakeHead = SquareFactory.create('SnakeHead', 3, 1, 'red'); var snakeBody1 = SquareFactory.create('SnakeBody', 2, 1, 'pink'); var snakeBody2 = SquareFactory.create('SnakeBody', 1, 1, 'pink'); // 使用双向链表的形式便于管理 this.head = snakeHead; snakeHead.next = snakeBody1; snakeHead.prev = null; snakeBody1.next = snakeBody2; snakeBody1.prev = snakeHead; snakeBody2.next = null; snakeBody2.prev = snakeBody1; this.tail = snakeBody2; // 添加蛇 ground.remove(snakeHead.x, snakeHead.y); ground.append(snakeHead); ground.remove(snakeBody1.x, snakeBody1.y); ground.append(snakeBody1); ground.remove(snakeBody2.x, snakeBody2.y); ground.append(snakeBody2); }
接着,蛇要动吧,但是它接触到不同的方块所表现出的效果是不同的,例如他碰到地板,就继续移动,而碰到墙,他就会挂掉,碰到食物,他就增长且继续移动。
所以这里我们来给方块都加上一个touch接口(方法),当蛇将要触碰方块时,该方块就会触发该touch方法时,会给蛇传递一个信息,蛇根据这个信息来做不同的事。
先在配置变量中,加上一个TOUCH STRATEGY触碰策略对象
// TOUCH STRATEGY var TOUCHNUM = { MOVE: 'MOVE', DIE: 'DIE', EAT: 'EAT' }
然后在工厂中,我们给方块出厂前增加一个touch方法,触发时它返回相应的策略
SquareFactory.prototype.init = function (square, color, strategy) { /* ... */ square.touch = function() { return strategy; } } // 子工厂中给init传入他对应的策略,例如Floor就是MOVE,FOOD就是EAT... SquareFactory.prototype.Floor = function (x, y, color) { var floor = new Floor(x, y, SQUAREWIDTH, SQUAREWIDTH); this.init(floor, color, TOUCHNUM.MOVE); return floor; } /* ... */
然后蛇呢,就根据对应的策略执行不同的回调:
snake.strategies = { MOVE: function () { }, EAT: function () { }, DIE: function () { } }
那么,如何让蛇能够预判下一个将要触碰的方块,从而得到其策略呢?毕竟我们不能等蛇头嵌到墙上再让他死,得让他将要动的时候就死。
我们知道蛇是有运动方向的,于是就可以根据当前运动方向来对蛇头当前的坐标的下一坐标进行预判。例如,如果当前方向是向右,那么蛇头的下一个坐标就是x + 1, y + 0。我们把这个加多少减多少单独抽出来:
var DIRECTIONENUM = { UP: { x: 0, y: -1, }, DOWN: { x: 0, y: 1 }, LEFT: { x: -1, y: 0 }, RIGHT: { x: 1, y: 0 } }
然后给蛇一个move方法,来判断他下一个将要触碰的方块的策略,并执行。
snake.move = function (ground) { var square = ground.squareTable[this.head.y + this.direction.y][this.head.x + this.direction.x]; if(typeof square.touch == 'function') { this.strategies[square.touch()](this, square, ground); } }
接下来我们只要完成MOVE、EAT、和DIE即可。
先实现MOVE,蛇要怎么移动呢?实质上,只要在将要移动到的地方添加一个蛇头,然后把蛇尾去掉,这样就相当于一次移动了,并不需要全部蛇身都跟着动。这里需要注意的只是,把新蛇头添上后,就的蛇头你要用一个新的蛇身给替换掉,因为总不能同时有两个蛇头吧。而因为我们使用了双向链表,所以能够比较好的来操作这些东西。
知道了原理,接着让我们落实代码:
snake.strategies = { MOVE: function (snake, square, ground) { // 生成新的身体 并 显示在场景中 var newBody = SquareFactory.create('SnakeBody', snake.head.x, snake.head.y, 'pink'); newBody.next = snake.head.next; newBody.prev = null; newBody.next.prev = newBody; // 替换掉原来的蛇头 ground.remove(snake.head.x, snake.head.y); ground.append(newBody); // 根据预判的square的坐标来生成新的蛇头 var newHead = SquareFactory.create('SnakeHead', square.x, square.y, 'red'); // 单例 newHead.next = newBody; newHead.prev = null; newBody.prev = newHead; ground.remove(newHead.x, newHead.y); ground.append(newHead); snake.head = newHead; // 更新蛇头 // 删除尾巴 var newFloor = SquareFactory.create('Floor', snake.tail.x, snake.tail.y, 'orange'); ground.remove(snake.tail.x, snake.tail.y); ground.append(newFloor); snake.tail = snake.tail.prev; snake.tail.next = null; }, EAT: function () { }, DIE: function () { } }
只要注意一下链表的指向,还是比较简单的。接着只要我们开启一个定时器,不断去调用snake.move,那么蛇就会开始移动了。
接着我们来实现一下键盘的监听来改变方向:
// 包装成节流函数 var tChangeDirection = jsUtil.throttle(changeDirection, INTERVAL); document.onkeydown = function (e) { tChangeDirection(e) } function changeDirection(e) { if (e.key == 'ArrowUp' && snake.direction != DIRECTIONENUM.DOWN) { snake.direction = DIRECTIONENUM.UP; } else if (e.key == 'ArrowDown' && snake.direction != DIRECTIONENUM.UP) { snake.direction = DIRECTIONENUM.DOWN; } else if (e.key == 'ArrowLeft' && snake.direction != DIRECTIONENUM.RIGHT) { snake.direction = DIRECTIONENUM.LEFT; } else if (e.key == 'ArrowRight' && snake.direction != DIRECTIONENUM.LEFT) { snake.direction = DIRECTIONENUM.RIGHT; } }
需要注意的是,我这里用到了节流的思想。因为我们是定时器驱动蛇的移动,所以当我们键盘按得足够快时,可能蛇头的方向变了两次,而实际移动只是最后那一次,导致了蛇头撞到连接头的身体。例如,当蛇在向右时,如果你节流,当快速按下再按左时,蛇还没来得及往下就往左移动了,也就撞到了自己,导致游戏结束。所以,我们用到了节流。节流方法写在公共方法中:
var jsUtil = { /* ... */ //节流 throttle: function (origin, wait) { var startTime = 0; return function () { var lastTime = + new Date(); if(lastTime - startTime >= wait) { origin.apply(this, arguments); startTime = lastTime; } } } }
那么到现在,蛇已经可以移动了。接着我们再来实现食物的相关控制:
先创建一个FoodControl类来控制食物的出现:
// jsUtil.js var FoodControl = jsUtil.single(); // food.js var foodControl = new FoodControl(); foodControl.init = function (ground) { foodControl.create(ground); }
我们通过create函数来实现对食物的创建。首先我们知道食物出现的位置是随机的,同时它不能出现在蛇身上,也不能出现在墙壁里。
一般的做法是,随机生成一个坐标,然后判断其是否在蛇身上,如果在则重新生成一个坐标,再判断....
var x = null; var y = null; var flag = true; while(flag) { x = 1 + parseInt(Math.random() * (YLEN - 3)); y = 1 + parseInt(Math.random() * (XLEN - 3)); var ok = true; for(var node = snake.head; node; node = node.next) { if(x == node.x && y == node.y) { ok = false; break; } } if(ok) { flag = false; } }
但是这样并不好。在一开始可能体现不出来,但是随着蛇身的增长,while循环执行的次数可能就会越来越多,到最后食物可能就出现得很慢了。
那怎么办呢?我们可以来一个数组,在创建食物之前,先遍历一下地图,把所有地板的坐标都记录下来,然后再生成一个该数组长度范围内的随机数,取得一个坐标作为食物的坐标。这样就可以大大减少计算的时间。
foodControl.create = function (ground) { // 每次创建食物前都更新一次存放地板坐标的数组 oGame.floorArr.length = 0; // 遍历得到地板数组 for(var i = 0 ; i < YLEN; i ++) { for(var j = 0; j < XLEN; j ++) { if(ground.squareTable[i][j].touch() == 'MOVE') { oGame.floorArr.push({y: i, x: j}); } } } // 随机生成数组长度范围内的数,并取得坐标值 var ranNum = parseInt(Math.random() * oGame.floorArr.length), x = oGame.floorArr[ranNum].x, y = oGame.floorArr[ranNum].y; // 把食物添加到广场中 var food = SquareFactory.create('Food', x, y, 'green'); ground.remove(food.x, food.y); ground.append(food); }
接着我们再来完善一下蛇的EAT行为: 其实吃食物,只是和移动有一点不同,就是蛇尾可以不清除。因为吃了一个食物后,蛇身变长,就相当于你清了蛇尾之后又给它添上去了,还不如不清呢。所以我们还要修改一下MOVE,让他多接收一个参数,判断执行时是否来自EAT,如果是才删除尾巴。
snake.strategies = { MOVE: function (snake, square, ground, fromEat) { /* ... */ if (!fromEat) { var newFloor = SquareFactory.create('Floor', snake.tail.x, snake.tail.y, 'orange'); ground.remove(snake.tail.x, snake.tail.y); ground.append(newFloor); snake.tail = snake.tail.prev; snake.tail.next = null; } }, EAT: function (snake, square, ground) { // 蛇继续移动,但不消除尾巴 this.MOVE(snake, square, ground, true); // 加分 oGame.score ++; // 重新生成食物 foodControl.create(ground); }, DIE: function () { oGame.over(); } }
最后我们写一下游戏的开始结束相关,我们的项目就完成了。
// game.js var oGame = new Game(); oGame.timer = null; oGame.score = 0; oGame.floorArr = []; oGame.init = function () { oGround.init(); snake.init(oGround); foodControl.init(oGround); var tChangeDirection = jsUtil.throttle(changeDirection, INTERVAL); document.onkeydown = function (e) { tChangeDirection(e) } function changeDirection(e) { if (e.key == 'ArrowUp' && snake.direction != DIRECTIONENUM.DOWN) { snake.direction = DIRECTIONENUM.UP; } else if (e.key == 'ArrowDown' && snake.direction != DIRECTIONENUM.UP) { snake.direction = DIRECTIONENUM.DOWN; } else if (e.key == 'ArrowLeft' && snake.direction != DIRECTIONENUM.RIGHT) { snake.direction = DIRECTIONENUM.LEFT; } else if (e.key == 'ArrowRight' && snake.direction != DIRECTIONENUM.LEFT) { snake.direction = DIRECTIONENUM.RIGHT; } } } oGame.start = function () { this.timer = setInterval(function () { snake.move(oGround); }, INTERVAL) } oGame.over = function () { clearInterval(this.timer); alert('game over, 你的分数是' + this.score); } oGame.init(); oGame.start();
通过利用设计模式来写贪吃蛇, 我们可以很方便地对项目进行维护和一些更新迭代。例如当我们想添加另一种食物,吃了可以让蛇移动变快,我们只需要在子工厂添加这一食物的生产,并且给它一个蛇触碰到时的策略,然后在蛇里具体去实现它,这样一个新功能就能很方便地添上了。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
项目介绍
贪婪嗜蛇。好吧其实就是贪吃蛇。
先来看看最后呈现的结果(做的比较简单,界面很low):
这里面涉及到什么东西呢,先来看张UML图:
我们的游戏场景是由一个个小方格组成的,然后这些小方格又可以具体分为蛇头、蛇身、食物、障碍物、地面。蛇头和蛇身组成蛇,障碍物聚合成四周的墙。然后每个方格都有一个公共的接口touch,他是蛇触碰到改方格时候具体的策略。
项目地址:https://lyzsg.github.io/Supercalifragilisticexpialidocious-Snake/
具体实现
提取公共方法
首先我们知道像地板、障碍物、食物等等都继承自方块,所以我们需要实现继承的方法。同时,我们知道食物、蛇头等可以运用到单例模式,所以我们也来一个转单例的方法。我们把这些公共方法单独写在jsUtil.js文件里,便于管理。
首先,继承的话,我们一下子就能想到圣杯模式:
但是圣杯模式的继承,我们知道只是会继承父类原型上的属性,而父类身上的属性获取不到。
然而在我们这里,像地板、障碍物等都需要继承自方块这个基类,方块身上有定义了坐标和宽高,如果仅用圣杯模式的话是继承不了方块身上的属性的:
普通的解决方法是,我们可以在Food方法里面通过Square.call(this, x, y, width, height)来让方块的属性也能运用到食物里面。但是这样也未免太过繁琐,我们不仅要给Food写形参,也要修改函数体。
我们希望可以直接通过一个extends方法来实现这种继承: var Floor = jsUtil.extends(Square),简单快捷。(但同时我们也不能自定义Floor的函数体了)
那么怎么实现这个extend方法呢?很简单,只要把我们上面所说的封装起来就好了:
因为在这个项目里面,继承自方块的地板食物这些,也没有一些自己的属性,所以可以这么干。虽然这样最后得到的Floor似乎和Square没什么两样,但是这样能帮我们在一开始理清对象间的关系。
我们再来实现单例,我们需要单例的食物、蛇头的构造函数,所以我们同样要继承方块类自身的属性和原型,最后可以通过 var Food = jsUtil.single(Square) 的到单例的继承的构造函数。
配置变量
我们再来单体提取出一些配置变量,例如场景的宽高,格子的宽高,游戏难度等等,方便我们灵活修改。也把基类和子类都定义在这里。
工厂
看到这么多方块的种类,不由得我们就想来个工厂模式来管理一下是不是。那就来吧。具体的在工厂模式那篇笔记中已经讲过了,这里就不多说了。
不同的只是我们的子工厂,不再是一个构造函数,而是调用他真正的构造函数(所以更像一个流水线),并且多了一步init,对出厂前的产品进行最后的包装,给他一个实际的样式而已。(不同的方块类型有不同的颜色)
游戏广场
我们根据配置变量设置的广场坐标和横纵向系数,生成一个广场实例:
接着,我们给他一个初始化的方法,来给他的viewContent(默认div)添加样式,让广场显示出来:
我们知道,广场由地板、障碍物、蛇、食物等组合而成,所以我们要在广场里面填充这些东西。把广场拆分为一个一个的小方块,有的方块是地板、有的是墙、有的是蛇、有的是食物。所以,为了方便我们的添加,我们把这个广场看作是一个二维数组,数组下标作为我们的坐标。
例如我们知道广场的四周都是墙壁,即当x = 0 或者 x = XLEN -1 或者 y = 0 或者 y = YLEN - 1 时我们应该给广场填充障碍物。
我们先填充地板和墙:
同时,因为我们还要添加食物和蛇,所以需要一个拆地板的功能和添加新内容的功能:
蛇
我们先让蛇出现在场景中。假设一开始蛇有一个蛇头,两段蛇身,我们通过init方法将其初始化:
接着,蛇要动吧,但是它接触到不同的方块所表现出的效果是不同的,例如他碰到地板,就继续移动,而碰到墙,他就会挂掉,碰到食物,他就增长且继续移动。
所以这里我们来给方块都加上一个touch接口(方法),当蛇将要触碰方块时,该方块就会触发该touch方法时,会给蛇传递一个信息,蛇根据这个信息来做不同的事。
先在配置变量中,加上一个TOUCH STRATEGY触碰策略对象
然后在工厂中,我们给方块出厂前增加一个touch方法,触发时它返回相应的策略
然后蛇呢,就根据对应的策略执行不同的回调:
那么,如何让蛇能够预判下一个将要触碰的方块,从而得到其策略呢?毕竟我们不能等蛇头嵌到墙上再让他死,得让他将要动的时候就死。
我们知道蛇是有运动方向的,于是就可以根据当前运动方向来对蛇头当前的坐标的下一坐标进行预判。例如,如果当前方向是向右,那么蛇头的下一个坐标就是x + 1, y + 0。我们把这个加多少减多少单独抽出来:
然后给蛇一个move方法,来判断他下一个将要触碰的方块的策略,并执行。
接下来我们只要完成MOVE、EAT、和DIE即可。
先实现MOVE,蛇要怎么移动呢?实质上,只要在将要移动到的地方添加一个蛇头,然后把蛇尾去掉,这样就相当于一次移动了,并不需要全部蛇身都跟着动。这里需要注意的只是,把新蛇头添上后,就的蛇头你要用一个新的蛇身给替换掉,因为总不能同时有两个蛇头吧。而因为我们使用了双向链表,所以能够比较好的来操作这些东西。
知道了原理,接着让我们落实代码:
只要注意一下链表的指向,还是比较简单的。接着只要我们开启一个定时器,不断去调用snake.move,那么蛇就会开始移动了。
接着我们来实现一下键盘的监听来改变方向:
需要注意的是,我这里用到了节流的思想。因为我们是定时器驱动蛇的移动,所以当我们键盘按得足够快时,可能蛇头的方向变了两次,而实际移动只是最后那一次,导致了蛇头撞到连接头的身体。例如,当蛇在向右时,如果你节流,当快速按下再按左时,蛇还没来得及往下就往左移动了,也就撞到了自己,导致游戏结束。所以,我们用到了节流。节流方法写在公共方法中:
食物
那么到现在,蛇已经可以移动了。接着我们再来实现食物的相关控制:
先创建一个FoodControl类来控制食物的出现:
我们通过create函数来实现对食物的创建。首先我们知道食物出现的位置是随机的,同时它不能出现在蛇身上,也不能出现在墙壁里。
一般的做法是,随机生成一个坐标,然后判断其是否在蛇身上,如果在则重新生成一个坐标,再判断....
但是这样并不好。在一开始可能体现不出来,但是随着蛇身的增长,while循环执行的次数可能就会越来越多,到最后食物可能就出现得很慢了。
那怎么办呢?我们可以来一个数组,在创建食物之前,先遍历一下地图,把所有地板的坐标都记录下来,然后再生成一个该数组长度范围内的随机数,取得一个坐标作为食物的坐标。这样就可以大大减少计算的时间。
接着我们再来完善一下蛇的EAT行为:
其实吃食物,只是和移动有一点不同,就是蛇尾可以不清除。因为吃了一个食物后,蛇身变长,就相当于你清了蛇尾之后又给它添上去了,还不如不清呢。所以我们还要修改一下MOVE,让他多接收一个参数,判断执行时是否来自EAT,如果是才删除尾巴。
游戏控制
最后我们写一下游戏的开始结束相关,我们的项目就完成了。
总结
通过利用设计模式来写贪吃蛇, 我们可以很方便地对项目进行维护和一些更新迭代。例如当我们想添加另一种食物,吃了可以让蛇移动变快,我们只需要在子工厂添加这一食物的生产,并且给它一个蛇触碰到时的策略,然后在蛇里具体去实现它,这样一个新功能就能很方便地添上了。
The text was updated successfully, but these errors were encountered: