写在前面
刚学完了js不久,就用原生js写了一个贪吃蛇的小游戏,主要的思想就是面向对象。写一篇博客记录下来方便自己今后的复习,也顺便分享给大家。
效果图
引入MVC的设计模式
该游戏用到了一个经典的设计模型:MVC。游戏中的各种状态和数据由MODEL来管理,VIEW用来显示model的变化,用户和游戏之间的交互用control来完成。这样设计的好处是,model完全独立,view是model的状态机,model和view都由control来驱动。
MODEL
1.贪吃蛇的参与对象
-
舞台
舞台是通过css和HTML去渲染的,代码中有一个宽高600px的div表示游戏的主界面,可以划分成30*30个像素为20px小的方块。每个小方块坐标值可以用数组来表示,舞台可以看成30 * 30的二维数组。 -
墙
舞台之外的地方都可与看作墙,蛇撞到墙就会死亡。 -
方块
方块对象是通过dom操作创建并添加到舞台中去的,食物和蛇身上的每一个结点都是一个方块对象,继承了方块的属性和方法,视觉上看到的方块就是20*20像素的方块,css的定位让方块覆盖在舞台上。在用css属性对每一个方块表现出不同的形式,包括食物和蛇。- 食物 食物是一个指向舞台某一个点的坐标值
- 蛇身 蛇身包括蛇头,蛇尾,蛇身体,是一串坐标索引链表。 q:为什么不用数组用链表
2.蛇的活动
游戏中蛇的活动有四个:单纯的移动,吃食,碰撞(撞到自己,撞到墙)。
1.移动
过程分析
蛇移动时视觉 上发生的变化,移动一格,蛇头带动着蛇身体往下一个方向整体移动,链表依次移动了一个位置;蛇移动时候内部发生的变化,蛇是由单链表结点链接起来的,链表中存放的结点在舞台中的坐标值。移动的时候先创建一个新的蛇body,dom操作删除蛇头,在dom操作添加新的蛇body放在原来蛇头的位置,再创建一个新的蛇头放在蛇将要走的下一个位置。getNextPos方法中有一个数组存放了下一个位置的坐标值。最后再删除尾部结点,注意蛇身上每一个结点变化,相应的链表关系也要随之变化。不断跟新链表是为了方便后面碰撞和吃食过程中与自身的结点做对比。
相关代码
move: function(formot) {
var newBody = new Squre(this.head.x/wi, this.head.y/he, 'snackbody');
//更新链表关系
newBody.next=this.head.next;
this.head.next.last = newBody;
newBody.last = null;
this.head.delete();
newBody.create();
//创建一个新蛇头
var newHead = new Squre(this.head.x/wi + this.derection.x, this.head.y/he + this.derection.y,'snackHead');
newHead.create();
//更新链表关系
newHead.next = newBody;
newHead.last = null;
newBody.last = newHead;
//蛇头旋转
//newHead.viewContent.style.transform='rotate('+this.derection.rotate+'deg)';
//更新蛇头
this.head = newHead;
//更新蛇身上每一个方块信息,最前面插入newhead
this.pos.splice( 0, 0, [this.head.x/wi,this.head.y/he ]);
//是否删除蛇尾
if(!formot) {
this.tail.delete();
this.tail = this.tail.last;
this.pos.pop();
}
},
问题:数组作为蛇链表合适吗
数组的pop和unshift可以无缝表示蛇的移动,但是方便不代表性能好,unshift插入数组的时间复杂度是o(n),pop删除的时间复杂度是o(1)。而且蛇移动是一个高频的动作,如果一次动作的算法复杂度是o(n),蛇的长度远大,时间存储空间上都会产生一些问题,而链表插入一个结点的时间复杂度为o(1),能够提高游戏的性能,js中没有现成的链表结构,而是自己定义了last和next分别指向前一个和后一个结点将彼此之间串联了起来。
2.吃食
随机投食
吃食之前你得先有食物,而且这个食物是随机产生的,在创建食物的函数中,先随机生成食物的坐标值,这个坐标值不能和蛇身体重合,所以用forEach来遍历蛇结点与之对比,若重合就循环随机生成坐标直到不重合。随机生成食物坐标之后通过dom操作再将食物展现到游戏的页面中。现在有一个问题,就是吃完食物后要再创建一个食物就会浪费效率,能不能只需要一个食物就可以解决呢?这里用到了单例模式,通过if语句来判断HTML中是否存在.food这个选择器的元素,如果存在那么将修改他的坐标值修改left、top属性值,如果不存在就创建这个食物。
//创建食物开始啊
function createFood(){
//随机生成食物的坐标
var a = null, b= null;
var include = true;
while(include) {
a=Math.round(Math.random()*(i-1));
b=Math.round(Math.random()*(j-1));
snack.pos.forEach(function(value){
if(a !=value[0]&& b != value[1]) {
include = false;
}
});
}
//生成食物
food = new Squre(a, b,'food');
food.pos=[a,b];//存储生成食物的坐标
var foodDom = document.querySelector('.food');
if(foodDom){
foodDom.style.left = a*wi+'px';
foodDom.style.top = b*he+'px';
}else{
food.create();
}
}
过程分析
吃食的条件是当蛇要移动的下一个结点位置刚好等于食物的位置时候吃掉食物,吃食视觉上变化,吃掉食物,头结点移动到食物的位置,蛇身体边长一个单位。内部变化,吃掉食物其实就是蛇移动的过程,所以同蛇移动的内部变化基本一样,只是没有删除尾结点。吃掉食物后相应得分改变。
相关代码
eat: function() {
this.thing.move.call(this, true);
createFood();
game.score++;
},
3.碰撞
过程分析
碰撞有两种,撞到自己和墙,两种的条件不同,但是原理相同都是来判断结点是否相等,若相等就死亡并输出得分。
相关代码
die: function() {
console.log('die');
game.over();
}
View
view的任务主要有两个,绘制游戏界面,渲染model里面的各种数据结构,本文主要讲的是原生js实现贪吃蛇的思路,这里没有渲染引擎都是css来渲染,如何通过css来渲不做讨论。
Control
control主要做三件事
1.游戏与用户的驱动
- 游戏初始化 游戏初始化时将创建一个食物,创建一条蛇,监听键盘的方向,调用游戏开始函数,代码如下
Game.prototype.init= function() {
snack.init();
// snack.getNextPos();
createFood();
document.onkeydown= function(e) {
if(e.which == 37&& snack.derection !=snack.derectionNum.right) {
snack.derection =snack.derectionNum.left;
}else if(e.which == 38&& snack.derection !=snack.derectionNum.down) {
snack.derection =snack.derectionNum.top;
}else if(e.which == 39&& snack.derection !=snack.derectionNum.left) {
snack.derection =snack.derectionNum.right;
}else if(e.which == 40&& snack.derection !=snack.derectionNum.top) {
snack.derection =snack.derectionNum.down;
}
}
this.start();
}
- 游戏开始
游戏开始,蛇要动,要运动就要知道方向,要知道方向就要获取下一个蛇头的方向,每两百ms获取一次,蛇每200ms运动一个位置。
Game.prototype.start = function() {
this.timer = setInterval(function(){
snack.getNextPos();
},200);
}
- 游戏结束
游戏结束,游戏界面回到最初的状态并且输出得分,开始按钮出现。最关键的地方在回到初始状态,要将之前插入进去的HTML元素全部删掉用innerHTML = '';就可以删掉dom操作添加到页面的元素,然后重新创建游戏对象,蛇对象。
Game.prototype.over = function() {
clearInterval(this.timer);
alert('你的得分为:'+this.score);
//回到初始状态
var snackWarp = document.getElementById('snackWarp');
snackWarp.innerHTML = '';
snack = new Snack();
game = new Game;
var startButton = document.querySelector('.startBtn');
startButton.style.display = 'block';
}
- 游戏暂停
鼠标点击游戏界面任何地方游戏将会暂停,暂停只需清除计时器并显示暂停按钮,点击暂停按钮游戏将继续进行,暂停按钮消失。
//暂
var snackWarp = document.getElementById('snackWarp');
var pauseBtn = document.querySelector('.pause button');
snackWarp.onclick= function() {
game.Pause();
pauseBtn.parentNode.style.display = 'block';
}
pauseBtn.onclick = function(){
game.start();
pauseBtn.parentNode.style.display = 'none';
}
2.驱动model
驱动model主要是只做一件事,将model蛇运动的方向跟新为用户指定的运动方向
3.同步view和model
这个操作也比较简单,检查model是否有跟新,如果有更新通知view更新游戏界面。
参考文献
凹凸实验室:aotu.io/notes/2017/…