不多废话,直接上链接
链接:https://pan.baidu.com/s/1ZKtVNhzR4fIzNZSGWgFKQw?pwd=zglt
提取码:zglt
有需要的好兄弟们可以直接取用,想要了解一下编程思路的朋友们可以继续往下看,如有任何问题可以在评论区留言。
首先贪吃蛇小游戏主要需要实现一下几个功能:
(1)小蛇不断向前移动
(2)小蛇根据键盘按键改变移动方向
(3)小蛇撞墙或撞到自己后游戏结束
(4)地图内随机生成苹果
(5)小蛇吃到苹果后增加一格
接下来我们逐条实现
首先在页面内生成一个div,划出800*800的区域,并使其居中,将其class名设置为back
back的内容如下:
.back {
border: 2px solid black;
width: 800px;
height: 800px;
margin: 0 auto;
}
生成效果:
然后在.back中添加两个属性 display: flex;和 flex-wrap: wrap;
.back {
border: 2px solid black;
width: 800px;
height: 800px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
}
这两个属性是弹性盒子的内容,第一个属性设置为flex值将back声明为弹性盒子,第二个属性设置该盒子内容自动换行,设置这两个属性的原因我们接下来讲。
然后我们声明一个css样式名为.box,设置.box的宽高为40,并设置为怪异盒子
.box {
width: 40px;
height: 40px;
box-sizing: border-box;
}
box-sizing: border-box;用于将元素设置为怪异盒子,怪异盒子的特殊之处在于其总宽高固定,设置边框不会增加其实际宽高。
左为怪异盒子,右为普通盒子,给予20像素边框右者实际大小为140*140
这时页面中并没有叫.box的元素,接下来我们使用JavaScript代码给大背景添加小格子。
首先获取clas名为back的大背景。
var back = document.querySelector(".back");
接下来写一个执行400次的for循环,每次循环都用.creatElement()函数生成一个新的div标签,并将该标签的class名设置为box,然后使用.appendChild方法添加进大背景中:
for (let i = 0; i < 400; i++) { //添加地图格子
var box = document.createElement("div");
box.className = "box";
back.appendChild(box);
}
由于大背景宽高为800,小格子宽高为40,这样大背景中一共可以塞下20*20=400个小格子,由于我们给父元素设置为了弹性盒子且自动换行,这时当第一行被小格子塞满时多余的小格子就自动填充到下一行中(类似于浮动),这样逐行填充后就生成了一个400格的棋盘。
为了方便演示,我们给小格子添加一个边框,这是最终效果:
接下来我们开始画蛇,首先用querySelectAll()方法获取大背景中的400个小格子,并命名为box
var box = document.querySelectorAll(".box"); //获取所有格子
然后给格子添加颜色,需要注意的是,由于游戏开始时蛇头的位置是朝右的,所以蛇头一格并不是box[0],而是box[2],这里我设置蛇头为青色,身体为灰色。
//声明初始蛇样式
box[0].style.background = "grey";
box[1].style.background = "grey";
box[2].style.background = "cyan";
最终效果:
现在我们有了一条三格长的小蛇,接下来就是如何让小蛇动起来,这里当然需要用到计时器
var timer = setInterval(move, 200);
setInterval()为间隔计时器,内部传入两个参数,第一个为调用的函数名,第二个为间隔时间(单位为毫秒),当前该计时器会每隔200毫秒调用一次函数,即让小蛇一秒走五次,如果觉得太慢也可以把这个值调小,这样小蛇会移动得更快。
然后我们需要写让小蛇动起来的函数move(),不过在此之前,先将小蛇的头和尾位置以及整体位置存储一下。
//声明蛇,并以行和列的形式存储蛇每一格的位置
var snake = [{hang:0,lie: 0}, {hang:0,lie: 1}, {hang:0,lie: 2}];
var head = {}; //声明蛇头
head.hang = snake[2].hang;//获取当前蛇头位置
head.lie = snake[2].lie;
var tail = {}; //声明蛇尾
tail.hang = snake[0].hang;//获取当前蛇尾位置
tail.lie = snake[0].lie;
接下来我们来写函数move(),首先我们要理解一点,想让小蛇前进一个其实并不需要让小蛇每一段身体都前进,只需要让头部的位置前移一格,让原本头部的位置变为身体,再删除最后一节尾部即可。
这样我们就有了基本的实现思路,接下来就是如何用代码实现。
首先在移动前,我们需要判断蛇头是否撞墙,若撞墙则游戏直接结束无需再进行移动,判断是否撞墙,即判断新的头部位置行和列数值是否大于-1且小于20,不过向右移动时只需要判断右侧即可。
function move() {
if (head.lie + 1 < 20) { //判断是否向右撞墙
} else {
alert("游戏结束"); //撞墙,游戏结束;
clearInterval(timer);//清除定时器,让小蛇停止移动
};
}
由于头部遇到的情况比较复杂(撞墙,吃苹果,撞自己等),所以这里我们将头部移动单独封装成一个函数moveHead(),不过在调用该函数前,先设置好新头部的位置。
function move() {
if (head.lie + 1 < 20) { //判断是否向右撞墙
head.列 += 1;//向右移动即头部列位置+1
moveHead();//调用头部移动函数
} else {
alert("游戏结束"); //撞墙,游戏结束;
clearInterval(timer); //清除定时器,让小蛇停止移动
};
}
function moveHead() {}//声明头部移动函数
接下来正式开始移动小蛇,首先获取到新的头部位置在数组中的下标,由于地图每行为20格,那么位置下标=行数值*20+列数值,获取后将新头部格子刷成青色。
function moveHead() { //声明头部移动函数
//获取头部位置在数组中的下标,并储存为positon
var position = head.hang * 20 + head.lie;
//利用下标在box数组中定位新的头位置,并将其刷成青色
box[position].style.background = "cyan";
}
当前效果:
然后我们需要将原蛇头位置的青色变为灰色,原蛇头位置即当前snake数组中的最后一位,即数组长度-1的位置(数组下标从0开始,所以减一)。
function moveHead() { //声明头部移动函数
//获取新头部位置在数组中的下标,并储存为positon
var position = head.hang * 20 + head.lie;
//利用下标在box数组中定位新的头位置,并将其刷成青色
box[position].style.background = "cyan";
//获取原有头部位置的下标,并储存为positon
position = snake[snake.length - 1].hang * 20 + snake[snake.length - 1].lie;
//将原蛇头位置刷成灰色
box[position].style.background = "grey";
}
当前效果:
接下来就是删除蛇尾,蛇尾位置即snak[0]存储的位置。
function moveHead() { //声明头部移动函数
//获取新头部位置在数组中的下标,并储存为positon
var position = head.hang * 20 + head.lie;
//利用下标在box数组中定位新的头位置,并将其刷成青色
box[position].style.background = "cyan";
//获取原有头部位置的下标,并储存为positon
position = snake[snake.length - 1].hang * 20 + snake[snake.length - 1].lie;
//将原蛇头位置刷成青色
box[position].style.background = "grey";
//获取蛇尾位置
position = snake[0].hang * 20 + snake[0].lie;
//删除蛇尾颜色
box[position].style.background = "";
}
最终效果:
这时我们还需要做最后的处理工作,虽然页面中的snake位置已经变化了,但是snake数组中存储的位置并没有改变,所以现在我们要修改snake数组中的内容。
var newhead = { //处理对象浅复制问题
"hang": head.hang,
"lie": head.lie
};
snake.push(newhead) //将新的头位置添加进数组末尾
snake.shift() //删除数组中尾部位置
由于这里涉及到对象浅复制问题,所以每次添加都需要声明一个新对象,感兴趣的朋友们可以自行了解一下。
最终效果:
这样小蛇的移动就完成了,接下来我们来控制小蛇的移动方向。
首先我们声明一个变量,设置其数值为ArrowRight,为什么是这个值我们接下来讲。
var ahead = "ArrowRight"; //声明蛇的前进方向
然后我们给页面添加一个监听事件,监听鼠标按下,并传入一个参数e。
var ahead = 39; //声明蛇的前进方向
document.addEventListener("keydown", (e) => {})//添加键盘按下的监听事件并传入e
这里传入的e可以简单理解为事件本身,其内部存储了触发该事件时的一系列数据,这里我们需要用到其中的一个数据.key,该属性存储了触发事件的按键名称。
打印e.key时分别点击小键盘上下左右四个键时返回的名称:
这就是我们设置ahead的值为ArrowRight的原因,在JavaScript中不同的按键有不同的名称,我们只需要根据名称就可以利用分支语句决定小蛇前进的方向,同时我们还可以添加一个判断语句,防止小蛇出现180度调头的情况(这里使用了JavaScript的三目运算,感兴趣的朋友们可以自行了解)。
document.addEventListener("keydown", (e) => { //添加键盘按下的监听事件并传入e
switch (e.key) { //添加分支语句,设置移动方向
case "ArrowUp": //判断e.key是否等于该关键字
ahead != "ArrowDown" ? ahead = e.key : ""; //判断并给ahead赋新值
break; //结束Switch执行,防止下方的代码影响结果
case "ArrowRight":
ahead != "ArrowLeft" ? ahead = e.key : ""; //判断并给ahead赋新值
break;
case "ArrowDown":
ahead != "ArrowUp" ? ahead = e.key : ""; //判断并给ahead赋新值
break;
case "ArrowLeft":
ahead != "ArrowRight" ? ahead = e.key : ""; //判断并给ahead赋新值
break;
}
console.log(e.key);
})
然后再给前面的move()函数也添加一个Switch分支语句,通过ahead的值判断头部向哪个方向移动,向上移动时头部行位置减一,向下移动时头部行位置加一,向左和向右则为头部列位置减一和加一。
function move() {
switch (ahead) { //通过ahead判断移动方向
case "ArrowUp": //是否向上
if (head.hang - 1 > -1) {
head.hang -= 1; //向上移动即头部行位置-1
moveHead();
} else {
alert("游戏结束");
clearInterval(timer);
};
break;
case "ArrowDown": //是否向下
if (head.hang + 1 < 20) {
head.hang += 1; //向下移动即头部行位置-1
moveHead();
} else {
alert("游戏结束");
clearInterval(timer);
};
break;
case "ArrowLeft": //是否向左
if (head.lie - 1 > -1) {
head.lie -= 1; //向左移动即头部列位置-1
moveHead();
} else {
alert("游戏结束");
clearInterval(timer);
};
break;
case "ArrowRight": //是否向右
if (head.lie + 1 < 20) {
head.lie += 1; //向右移动即头部列位置+1
moveHead();
} else {
alert("游戏结束");
clearInterval(timer);
};
break;
}
}
由于目前我们可以通过按键来改变ahead的值,而move()函数的移动方向由ahead决定,所以我们就间接实现了通过按键控制小蛇的移动方向,效果如下:
下一步就是声明苹果了,首先我们创建函数addApple(),并声明对象apple存储苹果的位置
var apple = {};
function addapple() {}
接下来我们需要用到一个数学方法Math.random(),该方法的作用是声明一个0到1之间的随机浮点数,由于我们的地图大小为20*20格,所以我们将生成的数字乘20并取整,这样就得到了一个0到20之间的随机整数(向下取整所以不含20)。
function addapple() { //随机生成苹果函数
apple.hang = parseInt(Math.random() * 20); //随机生成行
apple.lie = parseInt(Math.random() * 20); //随机生成列
var position = apple.lie + apple.hang * 20 //通过行和列随机生成苹果
box[position].style.background = "red"; //在随机确定的位置刷红色
}
这时我们调用函数,苹果就会出现在地图中
不过由于目前苹果的位置完全随机,所以有可能会直接生成在小蛇身上,这是我们需要避免的问题,即当随机生成的苹果位置有颜色时重新生成,这里我们可以通过使用一个简单的递归来解决该问题。
var apple = {}; //声明对象存储苹果位置
function addapple() { //随机生成苹果函数
apple.hang = parseInt(Math.random() * 20); //随机生成行
apple.lie = parseInt(Math.random() * 20); //随机生成列
var position = apple.lie + apple.hang * 20 //通过行和列随机生成苹果
if (box[position].style.background != "") { //判断随机生成的苹果位置是否有颜色
addapple(); //若有颜色重新调用该函数生成
} else {
box[position].style.background = "red"; //若无颜色则刷红
}
}
这时当生成的随机位置有颜色时该函数就会重新被调用,生成一个新的苹果位置,若没有颜色则结束递归,并在该位置刷红色。
然后就是最后一步,当小蛇吃到苹果时变长一格,同时生成新的苹果位置,其实这一功能很好实现,我们在移动小蛇时会删除小蛇的尾部,现在只需要让小蛇头部碰到苹果时尾部不删除即可增加一格长度,所以我们需要对movehead()函数进行修改:
function moveHead() {
//获取新头部位置在数组中的下标,并储存为positon
var position = head.hang * 20 + head.lie;
if (box[position].style.background == "red") { //判断新的头部位置是否为红色
snake.unshift({
"hang": snake[0].hang,
"lie": snake[0].lie
}); //若为红色则复制一份蛇尾,删除时便会保留蛇尾
addapple(); //调用函数,生成新苹果
}
box[position].style.background = "cyan";
position = snake[snake.length - 1].hang * 20 + snake[snake.length - 1].lie;
box[position].style.background = "grey";
position = snake[0].hang * 20 + snake[0].lie;
box[position].style.background = "";
var newhead = {
"hang": head.hang,
"lie": head.lie
};
snake.push(newhead)
snake.shift() //删除数组中尾部位置
}
同时这里我们还可以添加一个判断,若新蛇头位置为灰色,则说明小蛇撞到了自己,这时可以直接结束游戏:
function moveHead() {
//获取新头部位置在数组中的下标,并储存为positon
var position = head.hang * 20 + head.lie;
if (box[position].style.background == "red") { //判断新的头部位置是否为红色
snake.unshift({
"hang": snake[0].hang,
"lie": snake[0].lie
}); //若为红色则复制一份蛇尾,删除时便会保留蛇尾
addapple(); //调用函数,生成新苹果
} else if (box[position].style.background == "grey") {//判断新的头部位置是否为灰色
alert("游戏结束");//若为灰色则直接结束游戏
clearInterval(timer);
}
box[position].style.background = "cyan";
position = snake[snake.length - 1].hang * 20 + snake[snake.length - 1].lie;
box[position].style.background = "grey";
position = snake[0].hang * 20 + snake[0].lie;
box[position].style.background = "";
var newhead = {
"hang": head.hang,
"lie": head.lie
};
snake.push(newhead)
snake.shift() //删除数组中尾部位置
}
到这里一款可以正常游玩的贪吃蛇小游戏就算基本完工了,删除注释的话总代码量可能都不到150行,虽然有很多细节有待优化;
那么本期的内容就到这里,如有任何问题欢迎在评论区留言,up都会尽量解答。
----------------------------------------------分割线-----------------------------------------------
后续我又对代码进行了一点优化,首先是添加了分数和规则
规则不难写,分数的话只需要在全局声明一个变量number 来存储分数,每次撞到苹果后该数值加一即可。
var p = document.querySelector("p");//获取第一行,分数行
var number = 0; //存储当前分数
function moveHead() { //声明头部移动函数
.....
if (box[position].style.background == "red") { //判断新的头部位置是否为红色
snake.unshift({
"hang": snake[0].hang,
"lie": snake[0].lie
}); //若为红色则复制一份蛇尾,删除时便会保留蛇尾
addapple(); //调用函数,生成新苹果
number++; //number加一分
p.innerHTML = number + "分"; //改变p标签内容
} else if (box[position].style.background == "grey") { //判断新的头部位置是否为灰色
alert("游戏结束"); //若为灰色则直接结束游戏
clearInterval(timer);
}
.....
}
然后是暂停功能,只需要在addEventListener监听事件的Switch分支中添加一个空格分支,单击空格时弹出一个窗口即可暂停,当浏览器显示窗口时计时器是停止运行的。
document.addEventListener("keydown", (e) => { //添加键盘按下的监听事件并传入e
switch (e.key) { //添加分支语句,设置移动方向
.....
case " ":
alert("暂停中。。。");
}
console.log(e.key);
});
最后我把蛇和苹果的颜色放到了代码最上方,这样可以直接修改所有代码中的颜色。