JS学习笔记之贪吃蛇小游戏demo实例详解
作者:倪晓磊
本文实例讲述了JS学习笔记之贪吃蛇小游戏demo实例。分享给大家供大家参考,具体如下:
最近跟着视频教程打了一个贪吃蛇,
来记录一下实现思路,
先上代码
静态页
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>贪吃蛇</title> </head> <style> *{ margin: 0; padding: 0; } .map{ width:800px; height: 600px; background-color: #ccc; position:relative; } </style> <body> <!-- 画出地图,设置样式 --> <div class="map"> </div> </body> <script src="food.js"></script> <script src="snake.js"></script> <script src="game.js"></script> </html>
food.js
//食物就是一个对象,宽高横纵坐标,先定义构造函数,然后创建对象 (function (){ var elements=[];//用来保存每个小方块食物 function Food(x,y,width,height,color){ //横纵坐标 this.x=x||0; this.y=y||0; this.width=width||20; this.height=height||20; //背景颜色 this.color=color||"green"; } //为原型添加初始化的方法(作用:在页面上取显示这个食物) //因为食物要在地图上显示,所以,需要地图这个参数 Food.prototype.init=function(map){ //先删除这个食物 //外部无法访问的函数 remove() var div=document.createElement("div"); //把这个div加到map中 map.appendChild(div); //设置div的样式 div.style.width=this.width+"px"; div.style.height=this.height+"px"; div.style.backgroundColor=this.color; // div.style.left=this.x+"px"; //先脱离文档流 div.style.position="absolute"; //随机横纵坐标 this.x=parseInt(Math.random()*(map.offsetWidth/this.width))*this.width; this.y=parseInt(Math.random()*(map.offsetHeight/this.height))*this.height; div.style.left=this.x+"px"; div.style.top=this.y+"px"; // Food.prototype.init=function(map){ // } //把div加入到数组elements中 elements.push(div); } function remove(){ //elements数组中有这个食物 for(var i=0;i<elements.length;i++){ var ele=elements[i] //找到这个子元素的父级元素,然后删除这个子元素 ele.parentNode.removeChild(ele); //再次把elements中的这个子元素也要删除 elements.splice(i,1) } } //把Food暴露给Window,外部可以使用 window.Food=Food; }());
snake.js
//蛇 (function(){ var elements=[];//存放小蛇的每个身体部分 //蛇的构造函数 function Snake(width,height,direction){ //小蛇的每个部分的宽 this.width=width||20; this.height=height||20; //身体 this.body=[ {x:3,y:2,color:"red"}, {x:2,y:2,color:"orange"}, {x:1,y:2,color:"orange"} ]; this.direction=direction||"right"; } //蛇的初始化 Snake.prototype.init=function(map){ remove() //循环遍历创建div for(var i=0;i<this.body.length;i++){ var obj=this.body[i]; //创建div var div=document.createElement("div"); //把div加入到map地图中 map.appendChild(div); //设置div的样式; div.style.position="absolute"; div.style.width=this.width+"px"; div.style.height=this.height+"px"; div.style.left=obj.x*this.width+"px"; div.style.top=obj.y*this.height+"px"; div.style.backgroundColor=obj.color; //把div加入到elements数组中--目的是删除 elements.push(div) } } //蛇的移动 Snake.prototype.move=function(food,map){ //改变蛇身体位置 var i=this.body.length-1; //2 for(;i>0;i--){ this.body[i].x=this.body[i-1].x; this.body[i].y=this.body[i-1].y; } //判断方向---改变小蛇的头的坐标位置 switch (this.direction){ case "right": this.body[0].x+=1; break; case "left": this.body[0].x-=1; break; case "top": this.body[0].y-=1; break; case "bottom": this.body[0].y+=1; break; } //判断有没有吃到食物 //小蛇的头的坐标和食物位置 var headX=this.body[0].x*this.width; var headY=this.body[0].y*this.height; //食物的横纵坐标 var foodX=food.x; var foodY=food.y; if(headX==foodX&&headY==foodY){ //获取蛇的最后尾巴 var last=this.body[this.body.length-1]; //把最后的蛇尾复制一份 this.body.push({ x:last.x, y:last.y, color:last.color }) //重新初始化食物 food.init(map); } } //删除小蛇的私有函数 function remove(){ //获取数组 var i=elements.length-1; for(;i>=0;i--){ //先从当前的子元素中找到该子元素的父级元素,然后再弄死这个子元素 var ele=elements[i]; //从map地图上删除这个子元素div ele.parentNode.removeChild(ele); elements.splice(i,1); } } window.Snake=Snake; }()); //蛇 (function(){ var elements=[];//存放小蛇的每个身体部分 //蛇的构造函数 function Snake(width,height,direction){ //小蛇的每个部分的宽 this.width=width||20; this.height=height||20; //身体 this.body=[ {x:3,y:2,color:"red"}, {x:2,y:2,color:"orange"}, {x:1,y:2,color:"orange"} ]; this.direction=direction||"right"; } //蛇的初始化 Snake.prototype.init=function(map){ remove() //循环遍历创建div for(var i=0;i<this.body.length;i++){ var obj=this.body[i]; //创建div var div=document.createElement("div"); //把div加入到map地图中 map.appendChild(div); //设置div的样式; div.style.position="absolute"; div.style.width=this.width+"px"; div.style.height=this.height+"px"; div.style.left=obj.x*this.width+"px"; div.style.top=obj.y*this.height+"px"; div.style.backgroundColor=obj.color; //把div加入到elements数组中--目的是删除 elements.push(div) } } //蛇的移动 Snake.prototype.move=function(food,map){ //改变蛇身体位置 var i=this.body.length-1; //2 for(;i>0;i--){ this.body[i].x=this.body[i-1].x; this.body[i].y=this.body[i-1].y; } //判断方向---改变小蛇的头的坐标位置 switch (this.direction){ case "right": this.body[0].x+=1; break; case "left": this.body[0].x-=1; break; case "top": this.body[0].y-=1; break; case "bottom": this.body[0].y+=1; break; } //判断有没有吃到食物 //小蛇的头的坐标和食物位置 var headX=this.body[0].x*this.width; var headY=this.body[0].y*this.height; //食物的横纵坐标 var foodX=food.x; var foodY=food.y; if(headX==foodX&&headY==foodY){ //获取蛇的最后尾巴 var last=this.body[this.body.length-1]; //把最后的蛇尾复制一份 this.body.push({ x:last.x, y:last.y, color:last.color }) //重新初始化食物 food.init(map); } } //删除小蛇的私有函数 function remove(){ //获取数组 var i=elements.length-1; for(;i>=0;i--){ //先从当前的子元素中找到该子元素的父级元素,然后再弄死这个子元素 var ele=elements[i]; //从map地图上删除这个子元素div ele.parentNode.removeChild(ele); elements.splice(i,1); } } window.Snake=Snake; }());
game.js
//游戏 (function(){ var that=null; //游戏的构造函数 function Game(map){ this.food=new Food(); this.snake=new Snake(); this.map=map;//地图 that=this; } Game.prototype.init=function(){ //初始化游戏 //食物初始化 this.food.init(this.map); this.snake.init(this.map); this.runSnake(this.food,this.map) this.bindKey(); } Game.prototype.runSnake=function(food,map){ //自动的去移动 var timeId=setInterval(function(){ //此时的this是window //蛇的移动 this.snake.move(food,map); //初始化蛇 this.snake.init(map); //横坐标最大值 var maxX=map.offsetWidth/this.snake.width; //获取纵坐标的最大值 var maxY=map.offsetHeight/this.snake.height; //蛇头的坐标 var headX=this.snake.body[0].x; var headY=this.snake.body[0].y; //判断横坐标 if(headX<0||headX>=maxX){ clearInterval(timeId) alert("游戏结束") } //判断纵坐标 if(headY<0||headY>maxY){ clearInterval(timeId) alert("游戏结束") } console.log(headX) }.bind(that),150) } Game.prototype.bindKey=function(){ //获取用户的按键,改变小蛇的方向 document.addEventListener("keydown",function(e){ //获取案件的值 switch(e.keyCode){ case 37: this.snake.direction="left"; break; case 38: this.snake.direction="top"; break; case 39: this.snake.direction="right"; break; case 40: this.snake.direction="bottom"; break; } }.bind(that),false) } window.Game=Game; }()); //初始化游戏对象 var gm=new Game(document.querySelector(".map")); gm.init()
这里加一个小插曲,关于匿名函数自调用的三种写法
第一种
第二种
第三种
注意!注意! 注意! 匿名函数的最后不要忘记加封号; 因为如果忘了加,系统很容易与后面的代码混淆 造成各种很奇葩的报错;
这里我推荐第三种写法,比较清晰明了
好,代码贴完了,我们来分析一下实现思路
首先 第一步
建立一个画布
设置画布的宽度为800px 高度为600px 因为小蛇 需要在画布内任意移动,需要脱离标准文档流,所以需要设置绝对定位, 因此我给画布添加了position:relative; ,再给背景添加一个颜色 ,灰色#ccc
好,画布创建好了,我们可以开始编写逻辑代码了
Food.js 代码分析
首先我们需要创建一个贪吃蛇 吃的“食物”,因此我们需要创建一个食物的对象,这里我在food.js中创建了一个自定义构造函数
定义了 “食物Food”的 x值、y值、宽度、高度、颜色
这里我利用 || 运算设置了默认值,如果 || 左边为false 则会自动取右边的值,所以当实例化对象时若未传参时 自动取 “||” 运算符右边的值
然后在"Food"的原型上定义了一个 init 初始化方法
首先创建一个div ,并将此对象保存在 div变量中
然后 在地图中 添加上这个 div 再逐步给这个 div元素 加上他的宽度、高度、背景颜色、并且设置绝对定位
那我们怎么定位呢?
这里我们可以把整个地图看成是一个坐标系,把地图的宽度除以 “食物”的宽度 来切分这个地图 ,x=1则相当于1个“食物宽度”的单位长,x=3 则相当于3个“食物宽度的单位长”
高度同理
这里我取了随机数 乘以 地图被切分的总份数 这样就会的到 随机的 X和Y 然后乘以 宽度和高度 就的到了不会超出地图的随机坐标 ,举例 : 因为Math.random(0,5) 是不包括5的
因为“食物”是会被贪吃蛇 “吃”掉的
所以我们必须创建一个方法来“消灭”这个“食物”,因此我定义了一个 remove 函数
并且 上方创建了一个 数组elements用于存放 创建出来的 "食物" 这个div元素 的对象,方便用来删除,每次初始化“食物” 时,将对象追加入elements 数组
我们遍历 elements 数组, 通过数组中每个div对象 先找到其父级,然后通过removeChild 方法将其自身删除
因为有保存在elements 数组中,那我们想要删除“食物就很方便了”,每次初始化之前我们秩序要调用一次 remove函数就是实现了“消灭”食物,然后再生成新的食物
因为此处的所有函数都写在了一个 自调用的匿名函数中,所以内部的Food 对象,在外部是访问不到的,
那怎么办呢?
这里我调用了window 对象,将配置好的Food 对象暴露给window ,这样,我们再其他的地方有需要时也可以实例化 Food了
Snake.js 代码分析
其实 “贪吃蛇”身体的实现和 “食物”的实现原理大体相同 ,首先我也同样建立了一个elements数组,用于存放之后小蛇移动时所产生的旧的“身体”,用于删除,因为都是局部变量,所以虽然两个数组名字相同,但不会冲突
这里我也给小蛇设置了宽、高,宽高我设定为默认和食物相同,并且还设置了方向direction 这用来控制小蛇的移动方向,这里我默认给了“right”向右移动,
并且,因为当游戏开始时,小蛇必当有一个初始的长度,我给了它一个脑袋 二节身体,脑袋设置成了红色,方便识别
所以我之后如果需要让小蛇增加长度,体现越吃越长的感觉 ,只需要在 body这个数组中追加对象就可以了
好,小蛇的基本属性配置完了,我们下一步就是要初始化小蛇
同样的,上方也提到了,我创建过一个elements数组,用于存放“小蛇”的旧身体,所以在初始化之前,我们需要调用remove函数,遍历elements数组,和删除“食物”一样的方法,将旧的“蛇身”都给删除了
执行完删除之后呢,我们就可以专心初始化了,
蛇身这么长,那我们该怎么知道蛇身的每一节到底该在哪里呢
这时就用到了我们上方定义的 body 这个数组了,它存放了小蛇的身体的所有部分
我们只需要遍历它,根据其中每个对象的属性都进行创建新的div对象,同意设置其宽、高、left、top,并且,将创建好的对象又存入elements数组中,方便下一次删除
定义完初始化的方法后,我们就得考虑小蛇的移动该怎么实现了,
既然是贪吃蛇游戏,我们肯定需要与玩家互动,让玩家来操控小蛇的走向,
对了,我们自定义构造函数的时候不是设置过一个direction 属性吗,我们就得利用起来,依据此来判断小蛇的走向
至于更改方向,我们放在之后的代码中实现
这里我们定义了一个move 函数 ,并传入两个参数 food "食物对象" 和 map“地图对象”
那为什么要传呢,
虽然我们这个demo里面只有一条小蛇,但这样的写法保留了同时开启多个游戏的可能性
首先我们获取 body 数组的长度 存入i 中,然后倒序倒序倒序遍历 i ,根据 i 作为索引, 从蛇尾巴开始向蛇脑袋遍历,大家想象一下,贪吃蛇的蛇身是不是都是按部就班的沿着脑袋走过的路径走的? 你给它绕个直角、或者正方形它总是老老实实的走完,所以我们每次移动,只需要控制蛇脑袋移动,让蛇身体让它他们挨个获取他们前面那一节身体的坐标就可以了
所以,这里我们倒序遍历,将第 i 节身体赋值前一节身体的 x 属性和 y 属性
蛇身的重新赋值做完了,我们判断一下蛇头的移动方向,因为是固定的4个方向,所以这里使用switch较为方便,
根据 上、下、左、右不一样的情况对 头部的x和y增加或减少
既然是贪吃蛇,我们食物也创建好了,需要实现贪吃蛇吃食物这个过程
首先我们分别计算出 蛇脑袋和食物的X和Y
然后,我们判断一下,
当蛇脑袋的x,y 和食物的x,y都相等的时候,
我们延长一节蛇身,这里我复制了一份最后一节蛇身体 然后追加入body数组,注意!
因为对象是引用类型,所以必须这样拆开赋值
最后,再调用一次食物的初始化,产生新的食物
同样的,这里我也将,Snake 对象暴露给window,供下方的Game.js中的代码调用
Game.js 代码分析
在Game.js中,开头就定义了that
用来保存this 的指向,供后面使用
我们分别实例化一个 “食物”对象和一个“贪吃蛇”对象
传入地图对象,并赋值
、
属性设置好了
那既然是游戏那我们是不是应该设定点游戏规则,
当我们的小蛇到达地图边界时,小蛇就会一头撞死了,游戏结束,
并且我们也没有实现小蛇的移动,让我们来接着实现吧
这里我定义了一个runSnake函数,传入 food 和map 对象
首先,定义一个计时器,存入timeId这个变量中
调用一个蛇的 move(移动) 和 init (初始化函数)
在小蛇成功移动之后,我们再判断一下,小蛇是否已经走到边界了,
计算出,地图宽度最多能被蛇头的宽度分为几份,高度同理
取出蛇头自身的x和y
判断 如果蛇头x<0 说明越过左边界,超过maxX则说明超过右边界,
y同理
如果越过边界,则清除定时器,执行一个弹框
注意,我在这个定时器中的方法后加个一个bind 并传入了开始定义的 that ,也就是提前保存的this 指向,如果不加,这里的代码多处用到了this ,因为setInterVal 的指向为window 所以会导致代码出现错误,无法找到这些方法和属性
接下来我们再来实现一下如何用键盘控制小蛇的移动
根据keycode 来更改 snake对象的 direction ,
同样的,此处的this 指向也不正确,指向的是 触发该事件的对象,这是无法调用snake对象的,所以我们必须改变它,在bind中传入(that)
然后将Game 对象暴露给 window
接着定义初始化游戏的函数
分别调用food对象的初始化函数、小蛇的初始化函数,调用runSnake函数开启定时器让小蛇跑起来
最后绑定上keydown 事件
最后的最后
实例化一个Game对象
调用gm 的init 贪吃蛇小demo就实现了
效果展示
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript数组操作技巧总结》、《JavaScript排序算法总结》、《JavaScript遍历算法与技巧总结》、《JavaScript查找算法技巧总结》及《JavaScript错误与调试技巧总结》
希望本文所述对大家JavaScript程序设计有所帮助。