js贪吃蛇详解
终于写了心心念念的贪吃蛇 ,很早就听说过它的大名,一直没有去实现它,这次借着学习jq和面向对象的机会去实现了它 。 js写了注释 后面也有分析哦,不是很难一起看看吧~…
https://github.com/xingxinglieo/-demo
html和css代码
html css 先设置两个div是蛇头(蛇头一定要设z-index 否则可能被食物覆盖就不好了)和一个长度?身体 ;一个长度?身体被定在height:20px width:20px;游戏界面默认200px,300px 可调用时传参设置
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>贪吃蛇小demo</title>
<style type="text/css">
* {
padding: 0px;
margin: 0px;
}
#contain {
position: absolute;
background-color: black;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
.snakebody {
height: 20px;
width: 20px;
position: absolute;
background-color: white;
}
#food{
height: 20px;
width:20px;
position: absolute;
top: 40px;
left: 220px;
background-color: white;
}
</style>
</head>
<body>
<div id="contain">
<div id="food">
</div>
<div class="snakebody" style="z-index: 3;">
</div>
<div class="snakebody">
</div>
</div>
</body>
<script src="js/jquery-3.4.0.min.js" type="text/javascript" charset="utf-8"></script>
<script src="js/main.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
main(20,20,3);
</script>
</html>
js代码
function main(wids, heit, speed) { //三个参数 游戏界面的宽度 高度 还有?的速度
window.direction = 1;//保存用户按下方向键 以控制?方向
document.onkeydown = function(e) {
switch (e.keyCode) {//选择判断防止调头
case 37:
direction = direction == 1 ? 1 : -1;
break;
case 38: //向上键
direction = direction == wid ? wid : -wid;
break;
case 39: //右键
direction = direction == -1 ? -1 : 1;
break;
case 40: //向下键
direction = direction == -wid ? -wid : wid;
break;
}
}
window.wid = wids;
window.hei = heit;
//用户不传参 默认 一行30个 一列20个 宽高最小 15 速度提供1-5的选择 小数或超出 会判断
window.speed = Math.floor(window.speed = speed ? speed : 3) > 5 ? 5 : speed < 1 ? 1 : speed;
total = (wid = (wid = wid || 30) < 15 ? 15 : wid) * (hei = (hei = hei || 20) < 15 ? 15 : hei); //保证最低为15*15
window.positions = 2 * wid + 10;
window.markPosition = [2 * wid + 5, 2 * wid + 4]; //初始两个方块 在第三行 第5个第6个
$('#contain').height(20 * hei).width(20 * wid).children('.snakebody').eq(0).css({//设置基础位置
top: '40px',
left: '100px',
backgroundColor: 'skyblue'
}).next().css({
top: '40px',
left: '80px'
});
snake.move();
};
game = {
crashWall: function() {
switch (direction) { //即将撞墙且没有改变撞墙的方向 返回ture
case 1:
if (markPosition[0] % wid == wid - 1) return true;
break;
case -1:
if (markPosition[0] % wid == 0) return true;
break;
case wid:
if (Math.floor(markPosition[0] / wid) == hei - 1) return true;
break;
case -wid:
if (Math.floor(markPosition[0] / wid) == 0) return true;
break;
}
},
eatItselt: function() {//判断身体是否与头部位置相等
if (markPosition.indexOf(markPosition[0], 1) != -1) return true;
},
eatFood: function() {
//判断头部是否与食物位置相等 true 则数组增加一个数 增加一个蛇节
//并重设食物位置和颜色
if (markPosition[0] == positions) { //创建增加数组末尾一个数据等待在move
markPosition[markPosition.length] = null;
$('#contain').append('<div class="snakebody"></div>')
while (markPosition.indexOf(positions = Math.floor(Math.random() * total)) != -1); //防止食物与?重叠
$('#food').css({
top: snake.toTop(positions),
left: snake.toLeft(positions),
backgroundColor: ('rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math
.floor(
Math.random() * 255) + ')')
})
//食物的位置就是除和取余 背景颜色就是随机rgb ;
}
}
}
snake = {
toTop: function(pos) {//返回设置top/left字符串值 用于处理markposition和positions中的数据
return Math.floor(pos / wid) * 20 + 'px'
},
toLeft: function(pos) {
return pos % wid * 20 + 'px'
},
move: function() {
timeId = setInterval(function() {
if (game.crashWall() || game.eatItselt()) {//判断是否撞墙或吃到自己
clearInterval(timeId);
alert('游戏结束了');
return;
}
game.eatFood();//判断是否吃到食物
var record = JSON.parse(JSON.stringify(markPosition)); //深拷贝数组
markPosition[0] += direction; //第一个蛇节增加方向的值(上下为一行方块个数 左右为1),方向会改变
$('.snakebody').eq(0).css({
top: snake.toTop(markPosition[0]),
left: snake.toLeft(markPosition[0])
})
for (let i = 1; i < markPosition.length; i++) {//让后面的蛇节的位置值等于前一个蛇节的位置值 实现移动
markPosition[i] = record[i - 1];
$('.snakebody').eq(i).css({
top: snake.toTop(markPosition[i]),
left: snake.toLeft(markPosition[i])
})
}
}, (6 - speed) * 50)
},
}
主体思路
用一个markposition 存储所有蛇节的位置,位置的数据本例是采用蛇节在界面的总位置(也可以采取存储x,y轴数据可能判断时候更方便一些),用direction 设置移动方向,判断?是否吃到自己 判断是否撞墙 判断是否吃到食物 ,吃到食物后生成新食物 蛇节增长
初始设置:main函数
function main(wids, heit, speed) { //三个参数 游戏界面的宽度 高度 还有?的速度
window.direction = 1;//保存用户按下方向键 以控制?方向
document.onkeydown = function(e) {
switch (e.keyCode) {//选择判断防止调头
case 37:
direction = direction == 1 ? 1 : -1;
break;
case 38: //向上键
direction = direction == wid ? wid : -wid;
break;
case 39: //右键
direction = direction == -1 ? -1 : 1;
break;
case 40: //向下键
direction = direction == -wid ? -wid : wid;
break;
}
}
window.wid = wids;
window.hei = heit;
//用户不传参 默认 一行30个 一列20个 宽高最小 15 速度提供1-5的选择 小数或超出 会判断
window.speed = Math.floor(window.speed = speed ? speed : 3) > 5 ? 5 : speed < 1 ? 1 : speed;
total = (wid = (wid = wid || 30) < 15 ? 15 : wid) * (hei = (hei = hei || 20) < 15 ? 15 : hei); //保证最低为15*15
window.positions = 2 * wid + 10;
window.markPosition = [2 * wid + 5, 2 * wid + 4]; //初始两个方块 在第三行 第5个第6个
$('#contain').height(20 * hei).width(20 * wid).children('.snakebody').eq(0).css({//设置基础位置
top: '40px',
left: '100px',
backgroundColor: 'skyblue'
}).next().css({
top: '40px',
left: '80px'
});
snake.move();
};
将传进来的参数设置在window里暴露给其他函数
其中
wid = (wid = wid || 30) < 15 ? 15 : wid
首先进行 括号内的|| 若用户传入参数 wids 上面window.wid 就被赋一个具体的值 否则是undefined若用户传参 则为参数值 若为传则默认30(这里忘了取整了) 为了防止用户传参过小 导致界面过小 影响体验 这里再进行一次选择判断 如果参数小于15 则为15 否则是原参数
direction 为方向标志 默认为1(向右) direction 赋值时判断 因为不能回头移动
如:
direction = direction == 1 ? 1 : -1;
设置位置标记数组默认有两个初始方块的数据
设置两个初始方块的位置(真是没事找事在js里面设置
最后调用?移动的方法
游戏逻辑
game = {
crashWall: function() {
switch (direction) { //即将撞墙且没有改变撞墙的方向 返回ture
case 1:
if (markPosition[0] % wid == wid - 1) return true;
break;
case -1:
if (markPosition[0] % wid == 0) return true;
break;
case wid:
if (Math.floor(markPosition[0] / wid) == hei - 1) return true;
break;
case -wid:
if (Math.floor(markPosition[0] / wid) == 0) return true;
break;
}
},
eatItselt: function() {//判断身体是否与头部位置相等
if (markPosition.indexOf(markPosition[0], 1) != -1) return true;
},
eatFood: function() {
//判断头部是否与食物位置相等 true 则数组增加一个数 增加一个蛇节
//并重设食物位置和颜色
if (markPosition[0] == positions) { //创建增加数组末尾一个数据等待在move
markPosition[markPosition.length] = null;
$('#contain').append('<div class="snakebody"></div>')
while (markPosition.indexOf(positions = Math.floor(Math.random() * total)) != -1); //防止食物与?重叠
$('#food').css({
top: snake.toTop(positions),
left: snake.toLeft(positions),
backgroundColor: ('rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math
.floor(
Math.random() * 255) + ')')
})
//食物的位置就是除和取余 背景颜色就是随机rgb ;
}
}
}
是否over了判断
crashWall: 是否撞墙判断 如果用户在判断时处于墙边缘未改变要撞墙的方向 游戏结束
eatItselt:使用indexof方法从第二个数据检查数组有没有数据与第一个数据(头)相同的
相同则是头与身体重合 即吃到自己
吃到食物判断
蛇头位置等于食物位置(每生成一次食物需记录其位置)
生成食物
本质是吃到食物后改变原食物的坐标和颜色
需注意 食物不能与身体重叠 (本例有小概率与新增加的最后一节蛇尾重合bug 但是因为下一次移动?就移动开了 所以基本无法发现)
while
(
markPosition.indexOf(positions = Math.floor(Math.random() * total))
!= -1); //防止食物与?重叠
while循环直至indexOf 不匹配数组中任何一个数据
?方法
snake = {
toTop: function(pos) {//返回设置top/left字符串值 用于处理markposition和positions中的数据
return Math.floor(pos / wid) * 20 + 'px'
},
toLeft: function(pos) {
return pos % wid * 20 + 'px'
},
move: function() {
timeId = setInterval(function() {
if (game.crashWall() || game.eatItselt()) {//判断是否撞墙或吃到自己
clearInterval(timeId);
alert('游戏结束了');
return;
}
game.eatFood();//判断是否吃到食物
var record = JSON.parse(JSON.stringify(markPosition)); //深拷贝数组
markPosition[0] += direction; //第一个蛇节增加方向的值(上下为一行方块个数 左右为1),方向会改变
$('.snakebody').eq(0).css({
top: snake.toTop(markPosition[0]),
left: snake.toLeft(markPosition[0])
})
for (let i = 1; i < markPosition.length; i++) {//让后面的蛇节的位置值等于前一个蛇节的位置值 实现移动
markPosition[i] = record[i - 1];
$('.snakebody').eq(i).css({
top: snake.toTop(markPosition[i]),
left: snake.toLeft(markPosition[i])
})
}
}, (6 - speed) * 50)
},
}
totop/left 处理markposition 返回设置top/left所需字符串
重点
markPosition[0] += direction; //方向会改变
$('.snakebody').eq(0).css({
top: snake.toTop(markPosition[0]),
left: snake.toLeft(markPosition[0])
})
for (let i = 1; i < markPosition.length; i++) {
markPosition[i] = record[i - 1];
$('.snakebody').eq(i).css({
top: snake.toTop(markPosition[i]),
left: snake.toLeft(markPosition[i])
})
}
头增加direction值
?身体开始 后面的数值等于原数组前一个的数值(模拟移动)
需要一个新数组存储原数组数值
注意
1.var record = markPosition; 是不可以的 这个是浅拷贝数组 只是拷贝了数组的指向 如果markPosition[i] 中数据则也改变 record[i] 数据数据(他们是同一个数据
这样子才能新创一个数组完全拷贝其数据;
2.判断over 或者eat必须在前面 否则会出现慢半拍的bug
缩减没有帮助阅读的换行和坐标更换为x,y之后可以40行js(不传参可以30行)js代码内完成这个小游戏,只要肯多缩减,20多行的大佬级别也并不是遥不可及(毕竟思路是一样的,这个游戏挺适合新手上手的,我也是小菜鸟一枚,第一次就写写自己理解哦~