html + js + css
贪吃蛇详解
想写好代码,先爱上编程。
本文通过有趣的贪吃蛇小游戏,一步步分析实现思路,体会编程的乐趣。
本文完整的源码文件,请在公众号
陈随易
后台回复贪吃蛇
获取。
创建舞台
创建一个 html
文件,显示一个宽高为 400px
的绿色草地。
我们的贪吃蛇,将在这里开始它的表演!
源码
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>html + js + css 贪吃蛇详解</title>
<style>
/* 这是贪吃蛇的 css 代码 */
/* 为了计算方便,把边距相关的属性和盒模型重置 */
* {
padding: 0;
border: 0;
margin: 0;
outline: 0;
box-sizing: border-box;
}
/* 舞台 */
.stage {
width: 400px;
height: 400px;
margin: 50px;
background-color: #52b356;
}
</style>
</head>
<body>
<!-- 这是贪吃蛇的 html 代码 -->
<div class="stage"></div>
<script>
// 这是贪吃蛇的 js 代码
</script>
</body>
</html>
演示效果:
问:*
号表示什么意思?
答:表示所有的
html
元素标签,如上css
代码,表示重置所有的标签样式。
问:为什么要重置?
答:如下图
任何一个元素,都有
padding (内间距)
、border (边框)
,margin (外边距)
,outline (轮廓)
以及元素里面的content (内容)
不同的元素,有可能这几个值都不相同
比如,
body
元素的默认margin
是8px
,三个a
标签并排,两两之间也是有间距的。为了解决不同标签之间的差异,干脆全部统一为
0
内间距,0
边框,0
外边距,0
轮廓。
问:box-sizing: border-box
有什么作用?
答:举个例子,比如,使用浮动
float
将2
个div
放在同一行,它们的宽度都是50%
,此时2
个div
元素刚好占据一行。此时,如果我们给第一个
div
元素,设置border:10px solid #000000
了10px的黑色边框
。那么默认情况下,第二个
div
会被挤到第二行显示,第一个div
的宽度会变成50%
+10px
+10px
宽度 (因为左右各有10
像素边框)。如果我们设置了
box-sizing: border-box
的话,那么2
个div
仍然会在同一行显示,占满一行。这是什么原因呢?
box-sizing
叫做盒子模型,它有2
个值,默认值是content-box
,如果我们设置了border
属性,这个盒子会往外面扩张,会把其他元素挤走,影响我们的布局。而另外一个值
border-box
,则会向内扩张,如果设置了border
为10px
,则不会对身边的朋友
产生影响,而是挤压自己的content
区域,自己默默承受压力。在
css
中,布局是非常基础,非常重要的东西。所谓牵一发而动全身
,一个小小的地方布局错乱,整个页面就很可能会不稳定。所以,使用border-box
重置元素的模型,更利于写出稳定的页面布局。
画格子
有了舞台,接下来就需要画格子了。
我们的贪吃蛇呢,是一个个格子到处跑的。
舞台的宽高是 400px
,那么我们只需要设置每个格子为 50px
就行了。
源码
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>html + js + css贪吃蛇详解</title>
<style>
/* 这是贪吃蛇的 css 代码 */
/* 为了计算方便,把边距相关的属性和盒模型重置 */
* {
padding: 0;
border: 0;
margin: 0;
outline: 0;
box-sizing: border-box;
}
/* 舞台 */
.stage {
width: 400px;
height: 400px;
margin: 50px;
background-color: #52b356;
border-top: 1px solid #333333;
border-left: 1px solid #333333;
}
/* 行 */
.row {
height: 50px;
display: flex;
}
/* 列 */
.col {
height: 50px;
width: 50px;
border-right: 1px solid #333333;
border-bottom: 1px solid #333333;
}
</style>
</head>
<body>
<!-- 这是贪吃蛇的 html 代码 -->
<div class="stage">
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
</div>
<script>
// 这是贪吃蛇的 js 代码
</script>
</body>
</html>
演示效果:
问:为什么 row
使用了 display:flex
属性?它的作用是什么?
答:时代在前进,
css
也在不断变化。时至今日2023
年,使用浮动float
布局的方式已经越来越少了。正所谓
浮动浮动
,我们的元素就像一个个泡沫箱
,我们的浏览器就是一个流动的水面。浮动float
,就相当于我们把泡沫箱
放在水面上,最终一个个随着水流动的方向,挤到一起。这种不可靠的、不好控的、过于灵活的布局方式,我们应该尽量抛弃它,使用更好的布局来实现,
flex
就是替代float
的一种方式。更加具体的
felx
说明,不是本文的主要内容,请读者朋友搜索相关资料了解。
画一个蛋
一步步来,不要纠结,先把蛋的样子画出来再说
源码
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>html + js + css贪吃蛇详解</title>
<style>
/* 这是贪吃蛇的 css 代码 */
/* 为了计算方便,把边距相关的属性和盒模型重置 */
* {
padding: 0;
border: 0;
margin: 0;
outline: 0;
box-sizing: border-box;
}
/* 舞台 */
.stage {
width: 400px;
height: 400px;
margin: 50px;
background-color: #52b356;
border-top: 1px solid #333333;
border-left: 1px solid #333333;
}
/* 行 */
.row {
height: 50px;
display: flex;
}
/* 列 */
.col {
height: 50px;
width: 50px;
border-right: 1px solid #333333;
border-bottom: 1px solid #333333;
display: flex;
justify-content: center;
align-items: center;
}
/* 蛋 */
.egg {
width: 24px;
height: 30px;
/* 一个发光的蛋 */
background-color: #ffe81b;
/* 蛋的上方 2 个角设置为 12px 的圆角 */
border-top-left-radius: 12px;
border-top-right-radius: 12px;
/* 蛋的上方 2 个角设置为 10px 的圆角 */
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
</style>
</head>
<body>
<!-- 这是贪吃蛇的 html 代码 -->
<div class="stage">
<div class="row">
<div class="col">
<div class="egg"></div>
</div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
</div>
<script>
// 这是贪吃蛇的 js 代码
</script>
</body>
</html>
演示效果:
随机显示
经过上一步,我们放好了一个蛋。
但是,我们总不能一直让蛋都只能出现在第一个格子里面吧?
- 蛋刚出现的时候,是随机出现在任意一个格子里的。
- 蛇吃掉蛋之后,新出现的蛋也是随机显示在任意一个格子里的
所以,到了这一步,就要开始写我们又爱又恨的 javascript
代码了。
首先声明,本文所有 javascript
皆为原生 js
代码,不熟悉的说明该多练练基础了。
那么,如何随机放置蛇蛋呢?我们先列举一下知识点和思路。
- 通过随机函数
Math.random()
,根据格子的行,列数量,生成横坐标和纵坐标值,两者结合,可以唯一定位一个格子位置。 - 根据随机函数生成的行,列值,取得对应的格子元素。
- 把蛋放到对应的格子中。
这里就涉及到了,Math.random()
函数怎么用呢?这个问题,不知道的请通过搜索引擎搜索了解。
这里呢,咱们只需要知道,Math.random()
可以生成一个 [0,1)
之间的值。
比如:0.9006360462499876
,0.6073391049644372
等。
还记得高中的教过的 集合
吗?[]
中括号,表示包含,()
小括号表示不包含。
所以,Math.random()
将会生成一个 大于等于0
且 小于1
之间的值。
那么,这样的一个随机值,怎么变成我们的格子坐标呢?
源码
// 蛋的随机横坐标,也就是第几行
let eggX = Math.floor(Math.random() * 8);
// 蛋的随机纵坐标,也就是第几列
let eggY = Math.floor(Math.random() * 8);
来,计算一下。
假设 Math.random()
获得的值是最大值 0.9999999999999999
。
那么,0.9999999999999999 * 8 = 7.999999999999999
。
通过 Math.floor()
函数进行取整后,得到的值是 7
。
格子的 行
和 列
,我们可以用一个 二维数组
来表示
源码
let stage = [
[0, 1, 2, 3, 4, 5, 6, 7], // 第 0 行
[0, 1, 2, 3, 4, 5, 6, 7], // 第 1 行
[0, 1, 2, 3, 4, 5, 6, 7], // 第 2 行
[0, 1, 2, 3, 4, 5, 6, 7], // 第 3 行
[0, 1, 2, 3, 4, 5, 6, 7], // 第 4 行
[0, 1, 2, 3, 4, 5, 6, 7], // 第 5 行
[0, 1, 2, 3, 4, 5, 6, 7], // 第 6 行
[0, 1, 2, 3, 4, 5, 6, 7] // 第 7 行
];
众所周知,数组的索引是从 0
开始的,通过上面的计算,我们的随机数可能的最大值,乘以 8
并取整后,得到的是数字 7
。
刚好跟咱们的二维数组吻合,不会导致随机出现的 蛋
,跑到舞台外面去。
分析完毕,我们的代码如下:
源码
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>html + js + css贪吃蛇详解</title>
<style>
/* 这是贪吃蛇的 css 代码 */
/* 为了计算方便,把边距相关的属性和盒模型重置 */
* {
padding: 0;
border: 0;
margin: 0;
outline: 0;
box-sizing: border-box;
}
/* 舞台 */
.stage {
width: 400px;
height: 400px;
margin: 50px;
background-color: #52b356;
border-top: 1px solid #333333;
border-left: 1px solid #333333;
}
/* 行 */
.row {
height: 50px;
display: flex;
}
/* 列 */
.col {
height: 50px;
width: 50px;
border-right: 1px solid #333333;
border-bottom: 1px solid #333333;
display: flex;
justify-content: center;
align-items: center;
}
/* 蛋 */
.egg {
width: 24px;
height: 30px;
/* 一个发光的蛋 */
background-color: #ffe81b;
/* 蛋的上方 2 个角设置为 12px 的圆角 */
border-top-left-radius: 12px;
border-top-right-radius: 12px;
/* 蛋的上方 2 个角设置为 10px 的圆角 */
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
</style>
</head>
<body>
<!-- 这是贪吃蛇的 html 代码 -->
<div class="stage" id="stage">
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
</div>
<script>
// 这是贪吃蛇的 js 代码
// 舞台元素
let stageElement = document.querySelector('#stage');
// 蛋的随机横坐标,也就是第几行
let eggX = Math.floor(Math.random() * 8);
// 蛋的随机纵坐标,也就是第几列
let eggY = Math.floor(Math.random() * 8);
// 随机格子元素,也就是蛋需要被放置的位置
let eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
</script>
</body>
</html>
演示效果:
为了方便获取元素,我们给舞台 stage
元素,增加了一个同名的 id
属性。
document.querySelector
就是用来获取元素的方法,我们看看 stageElement
元素都有哪些内容。
可以看到,通过 console.log
方法,打印的舞台 stage
内容是这个元素的结构,跟我们的 html
代码一致。
而通过 console.dir
方法,打印的舞台 stage
内容是这个元素具备的属性。
往下拉,就能看到一个 children
属性,这里面有 8
个 div
元素,它们的类名都是 row
,也就是我们的 行
。
同理,我们点击 row
的 children
属性,便能看到 8
个列 col
,其中第 5
列,便是蛋所在的位置。
这就是为什么,我们可以通过代码
源码
let eggElement = stageElement.children[eggX].children[eggY];
定位到蛋的位置,并将蛋放到这个地方的原理了。
画蛇头
一号主角,终于要上场了。
在画一号主角之前,我们来总结一下,之前的思路。
化繁为简:复杂逻辑拆分为简单逻辑,各个击破。
速战速决:
颜色、样式、位置、大小,都是可以随时更改的,它们不是贪吃蛇的核心要素。
不要花太多时间在这上面,要学会,节省时间。
很多人不知道什么是编程思维,也时常有人问我。
我无法通过口头和文字的方式告诉你。
但是,我想,如果你有耐心看到这里,或许你应该有所领悟。
蛇怎么画呢?
这个需要好好思考了,它跟前面有点不一样,这是真正需要逻辑思考的开始。
我们知道,程序,是由 数据结构
+ 算法
组成的。
他们的关系,用贪吃蛇的例子来说,那就是:
- 蛇的组成,是
数据结构
。 - 蛇能够移动和吃到蛋,是
算法
。
javascript
里面,有两个最为常见的结构:数组
和 对象
。
那么,用哪个来表示蛇的结构呢?
答案是,用两者组合来表示。
首先,来看看蛇的特点。
- 头在前,尾在后。
- 顺序是确定性的,固定的。
- 吃到蛋后,尾部会增长。
能够表示这样一个蛇的结构,很明显,数组是非常合适的。
因为数组里面的元素顺序是固定的,可以往后添加元素,添加元素后,顺序也是固定的。
源码
// 蛇元素数组
let snakeArray = [];
蛇的结构确定了,接下来要干嘛呢?
确定蛇的位置。
理解了前面蛋的生成,相必这里也不用多说。
但是呢?我们知道,蛋,永远只有一个。
不会同时出现 2
个蛋,也不会有多个蛋排在一起 (当然,你可以这么做,不过这不在本教程范围内)
而蛇呢?它的各个身体部分,是有关联的。
你不能头在第一行,身体第二节在第四行,第三节在第八行,这不合理。
所以,我们需要干嘛呢?
还记得前面说过的吗?我们要用数组和对象的结合,去确定蛇的位置。
那么,蛇的头部以及身体部分的每个元素,都有它自己的横轴和纵轴坐标。
那么,怎么表示呢?如下。
源码
// 蛇元素数组
let snakeArray = [{ x: 0, y: 0 }];
为了方便起见,我们一开始,只生成蛇的头部。
所以,数组的第一个元素,就是蛇头的位置。
可以看到,它是一个对象。
数组里面放对象,对象里面放坐标,是不是很优雅?这就是数据结构的威力。
如果体会不到这威力的话,我们举个反例,用双层数组去表示。
源码
// 蛇元素数组
let snakeArray = [[0, 0]];
数组的第一个元素还是个数组,数组的第 0 个元素,是横轴,第 1 个元素是纵轴。
比起上面的的结构,那个更好点?
很明显是上面那个,代码是写给人看的,是给人写的。
越直观,越通俗的代码,越好。
好了,我们来确定一下,蛇头的位置。
跟蛋一样,用两个随机数来生成。
这里其实涉及到,蛇头和蛋刚好在同一个位置怎么办呢?
凉拌。
如果看到这里,你还不懂得,速战速决,各个击破,兵来将挡,水来土掩的道理,那你就没有学到本文的精华。
本文明面上是教你如何写 贪吃蛇
,实际是教你如何养成 编程思维
。
如果你要考虑一大堆细节才能动手,那么你很可能不是一个合格的程序员。
一个稍微复杂的任务指派给你,分分钟把你逼疯。
正所谓 取舍取舍
。
它从某方面来说,并不代表放弃,而是先做最重要的部分,再处理边缘部分。
源码
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>html + js + css贪吃蛇详解</title>
<style>
/* 这是贪吃蛇的 css 代码 */
/* 为了计算方便,把边距相关的属性和盒模型重置 */
* {
padding: 0;
border: 0;
margin: 0;
outline: 0;
box-sizing: border-box;
}
/* 舞台 */
.stage {
width: 400px;
height: 400px;
margin: 50px;
background-color: #52b356;
border-top: 1px solid #333333;
border-left: 1px solid #333333;
}
/* 行 */
.row {
height: 50px;
display: flex;
}
/* 列 */
.col {
height: 50px;
width: 50px;
border-right: 1px solid #333333;
border-bottom: 1px solid #333333;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* 蛋 */
.egg {
width: 24px;
height: 30px;
/* 一个发光的蛋 */
background-color: #ffe81b;
/* 蛋的上方 2 个角设置为 12px 的圆角 */
border-top-left-radius: 12px;
border-top-right-radius: 12px;
/* 蛋的上方 2 个角设置为 10px 的圆角 */
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
/* 蛇 */
.snake {
width: 100%;
height: 100%;
}
.snake.head {
background-color: #35f06c;
}
.snake.body {
background-color: #35cb61;
}
</style>
</head>
<body>
<!-- 这是贪吃蛇的 html 代码 -->
<div class="stage" id="stage">
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
</div>
<script>
// 这是贪吃蛇的 js 代码
// 舞台元素
let stageElement = document.querySelector('#stage');
// 蛋的随机横坐标,也就是第几行
let eggX = Math.floor(Math.random() * 8);
// 蛋的随机纵坐标,也就是第几列
let eggY = Math.floor(Math.random() * 8);
// 随机格子元素,也就是蛋需要被放置的位置
let eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇头的横坐标
let snakeX = Math.floor(Math.random() * 8);
// 蛇头的纵坐标
let snakeY = Math.floor(Math.random() * 8);
// 贪吃蛇蛇元素数组
let snakeArray = [{ x: snakeX, y: snakeY }];
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
</script>
</body>
</html>
演示效果:
往上移动
蛋和蛇头都画好了,每次刷新本页面,都会看到不同位置的蛇蛋和蛇头
那么接下来呢?当然是要控制蛇的移动,让蛇能够吃到蛋。
这里呢,我们使用键盘的方向键对蛇的移动进行控制。
在进行我们的代码编写之前呢,我们一定要谨记,不要 意念编程
。
一定要 眼睛
看清楚,脑袋
想清楚,再动手。
至少有两个问题,需要首先理清楚。
- 如何监听键盘按下的操作?
- 按下之后,如何判断按的是哪个键?
解决办法很简单:做实验
源码
// 监听键盘按下,打印按下的事件
document.addEventListener('keydown', function (event) {
console.dir(event);
});
当我们按下向上的方向键。
可以看到,有多种属性,都指明了按下的键的值。
那么,选择哪个来判断呢?为了更直观地描述,这里我们选择 key:ArrowUp
属性。
记住,不要一口吃个胖子,我们先控制蛇向上移动,然后,再控制它四面八方移动。
在写代码之前,我们需要思考一个事情。
如何往上移动?
我们知道,蛇的位置,依靠 x
轴和 y
轴定位。
那么,往上移动,要修改的是 x
的值?还是 y
的值呢?
按照我们的惯性思维来说,肯定是修改 y
的值。
但是,大家还记得前面我们怎么画格子的吗?
x
表示第几行,y
表示这一行的第几列。
我往上移动,是不是相当于,我的行数变小了 (从上往下,依次为第 0
行到到第 7
行。)
所以,该怎么设置呢?是不是 x = x - 1
,对吧。
行数减小了 1
行嘛,缩写就是 x -= 1
那么,实现代码如下。
源码
// 这是贪吃蛇的 js 代码
// 舞台元素
let stageElement = document.querySelector('#stage');
// 蛋的随机横坐标,也就是第几行
let eggX = Math.floor(Math.random() * 8);
// 蛋的随机纵坐标,也就是第几列
let eggY = Math.floor(Math.random() * 8);
// 随机格子元素,也就是蛋需要被放置的位置
let eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇头的横坐标
let snakeX = Math.floor(Math.random() * 8);
// 蛇头的纵坐标
let snakeY = Math.floor(Math.random() * 8);
// 贪吃蛇蛇元素数组
let snakeArray = [{ x: snakeX, y: snakeY }];
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
document.addEventListener('keydown', function (event) {
// 如果按的是 上
if (event.key === 'ArrowUp') {
// 贪吃蛇的蛇头,也就是数组的第一个元素,往上移动一行
snakeArray[0].x -= 1;
}
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
});
演示效果:
可以发现,按上方向键的时候,蛇头是移动了,但是之前的位置并没有清除掉原来的蛇头,所以造成了多个残影。
那么,下一步,在生下一个位置的贪吃蛇之前,把上一个贪吃蛇删除掉。
怎么删除呢?原理很简单。
不管是蛇还是蛇的身体,都有一个共同的 snake 类名,找到所有当前的 snake 类名,然后删除就行了。
源码
// 监听键盘按下
document.addEventListener('keydown', function (event) {
// 蛇头,贪吃蛇数组的第0个元素
let snakeHead = snakeArray[0];
// 如果按的是 上
if (event.key === 'ArrowUp') {
// 往上移动一行
snakeHead.x -= 1;
}
// 删除上一个贪吃蛇
stageElement.querySelectorAll('.snake').forEach(function (item) {
item.parentElement.removeChild(item);
});
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
});
演示效果:
这里要注意一个地方,我们删除的时候,使用了 parentElement
属性。
这是什么意思呢?表示当前蛇元素是父级元素,也就是 cell
类名元素。
为什么要用它呢?因为,原生 js
里面,添加一个子元素,或者删除一个子元素。
都要以该元素的父级为参照,找到所有的 .snake
元素后,要删除该元素,就要找到其父级 cell
。
然后,通过 cell
删除子元素 removeChild
的方式,将 .snake
元素删掉。
四面移动
还记得前面说的吗?不要一口吃个胖子,一步步来,又稳又准。
搞定了往上移动,其他三个方向,那就很简单了。
这里,只展示监听部分代码,前面的就不放了,以免代码过长。
源码
// 上方向键控制蛇往上移动
document.addEventListener('keydown', function (event) {
// 蛇头,贪吃蛇数组的第0个元素
let snakeHead = snakeArray[0];
// 如果按的是 上
if (event.key === 'ArrowUp') {
snakeHead.x -= 1;
}
// 如果按的是 下
if (event.key === 'ArrowDown') {
snakeHead.x += 1;
}
// 如果按的是 左
if (event.key === 'ArrowLeft') {
snakeHead.y -= 1;
}
// 如果按的是 右
if (event.key === 'ArrowRight') {
snakeHead.y += 1;
}
// 删除上一个贪吃蛇
stageElement.querySelectorAll('.snake').forEach(function (item) {
item.parentElement.removeChild(item);
});
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
});
演示效果:
如上图,大家会发现一个很有意思的问题。
蛋是怎么消失的?
我们的代码里面,只有删除蛇,并没有删除蛇蛋呀?
这是因为,蛇
经过的格子,都会被 蛇
占据。
经过蛋格子的时候,蛋
被替换成 蛇
。
然后,当我们按下方向键的时候,蛇
的上一个位置被删除了。
所以,蛋
就不见了。
注意一下,这里并不是被吃掉,而是被 蛇
覆盖了。
那么,接下来,就是写,蛇
如何吃 蛋
了。
蛇吃蛋
又到了思考时刻。
蛇怎么吃蛋呢?
还记得我们的蛇结构吗?
源码
// 蛇头的横坐标
let snakeX = Math.floor(Math.random() * 8);
// 蛇头的纵坐标
let snakeY = Math.floor(Math.random() * 8);
// 蛇元素数组
let snakeArray = [{ x: snakeX, y: snakeY }];
看到没?蛇本身是一个数组,蛇的头和身体,是这个数组的每一个元素。
这个元素是个对象,记录了头和身体的横坐标和纵坐标的位置。
那么我们要增加蛇的身体,是不是直接往数组里面,添加身体元素就行了?
所以,蛇吃饱了之后的样子,如下。
源码
let snakeArray = [
{ x: x1, y: y1 },
{ x: x2, y: y2 },
{ x: x3, y: y3 },
// ...
{ x: xn, y: yn }
];
我们本节的标题是,蛇吃蛋
。
但是呢,我们一直在说,蛇吃了蛋之后怎么样。
那么,蛇怎么吃蛋呢?
这个再简单不过了,蛇头和蛋的坐标一致时,蛇就吃到蛋了。
虽然说起来简单,但是,有个问题,却比较隐密。
什么问题呢?
蛇吃到蛋之后,怎么放一个身体元素到尾部?
有人可能会说,数组不是有 push
方法吗,直接 push({x:n,y:m})
啊。
是么?too yang to simple
我就问一个问题,怎么得出追加的身体部分的横坐标和纵坐标值?
所以,事情没有那么简单。
先看代码。
源码
// 草地元素
let stageElement = document.querySelector('#stage');
// 蛋的随机x轴坐标,也就是第几行
let eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
let eggY = Math.floor(Math.random() * 8);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
let eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇头的横坐标
let snakeX = Math.floor(Math.random() * 8);
// 蛇头的纵坐标
let snakeY = Math.floor(Math.random() * 8);
// 蛇元素数组 - 目前只有一个蛇头
let snakeArray = [{ x: snakeX, y: snakeY }];
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
// 上方向键控制蛇往上移动
document.addEventListener('keydown', function (event) {
// 蛇头,贪吃蛇数组的第0个元素
let snakeHead = snakeArray[0];
// 如果按的是 上
if (event.key === 'ArrowUp') {
snakeHead.x -= 1;
// 蛇是否吃到蛋
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x - 1,
y: snakeTail.y
});
}
}
// 如果按的是 下
if (event.key === 'ArrowDown') {
snakeHead.x += 1;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x + 1,
y: snakeTail.y
});
}
}
// 如果按的是 左
if (event.key === 'ArrowLeft') {
snakeHead.y -= 1;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x,
y: snakeTail.y + 1
});
}
}
// 如果按的是 右
if (event.key === 'ArrowRight') {
snakeHead.y += 1;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x,
y: snakeTail.y - 1
});
}
}
// 删除上一个贪吃蛇
stageElement.querySelectorAll('.snake').forEach(function (item) {
item.parentElement.removeChild(item);
});
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
});
哦,天哪,代码量瞬间多了起来。
不过,仔细看的话,其实有部分代码逻辑是一样的。
可能有疑惑,先不管,先看效果。
疑问应该挺多的。
一个个解答。
解答之前呢,我们可以看到,我们已经实现了,蛇能够吃蛋,能够长身体这两个功能。
但是呢,BUG
也非常之明显,身体不跟着头走。
其次呢,我们之前说过了,蛇长身体,没这么简单。
怎么不简单呢?看看代码量增加了这么多就知道了。
具体增加了什么呢?
根据不同的方向键,身体增加的方式不一样。
也就是说,我按左方向吃了蛋,身体会追加到头的右侧。
按右方向键,身体会增加到头的左侧。
按上方向键,身体会增加到头的下侧。
按下方向键,身体会增加到头的上侧。
总而言之,身体永远不会出现在头移动方向上的的前方,左方和右方,只会增加到后方。
小小的身体增加,也需要大大的智慧啊!
继续,我们还增加了什么功能呢?
蛇吃完蛋了,总不能就没得吃了吧,对不对?
所以,每次吃了蛋,就再生一个蛋。
逻辑部分很明显了,但是重复代码比较多,这个后面会讲优化。
那么,还剩下一个重头戏,身体不跟着头走!
身随头走
到了这个地方,就不仅仅是写代码的问题了,而是编程思维的问题,思考策略的问题,代码调试的艺术的问题。
具体怎么说呢?
首先,我们要定位问题。
问题不是已经定位了吗,身体不跟着头走啊!
是的,概况的讲,是这样。
但是!
你能够确定,为了让身体跟着头走,哪些代码需要修改,哪些代码不需要修改,哪些代码需要重新整理逻辑吗?
所以,解决问题,可不能纸上谈兵,要有具体的分析,具体的判断。
很多人就是缺乏这一点,所以,代码学得很难受,写得更难受。
好了,我们来分析一下。
蛇吃蛋,蛋随机再生成,跟身体不随头走这个 BUG
,当然是无关的。
其次,每次吃了蛋之后,根据移动的方向,追加身体元素,这里也是毫无破绽。
那么,还剩下一个什么呢?移动的方式有问题啊!
怎么个有问题法?
仔细看代码,每次移动的按方向键的时候,只有蛇头加 1
,减 1
,减 1
,加 1
,对不对?
身体部分呢?风雨不动安如山。
所以,我们需要让身体部分,也动起来。
而且,是跟着蛇头动起来。
怎么跟着蛇头动呢?
举个例子。
你要坐火车,需要排队买票。
人很多,排成一排,这个是不是就跟我我们这个贪吃蛇一模一样?
假设第一个人是蛇头,第一个人买完票,走到一边。
那么之前第一个人的位置,是不是被第二个人占据?
然后呢?后面的人,以此类推,后一个人,占据前一个人的位置。
所以,我们要怎么做?
蛇头移动的时候,要让身体部分,一个个占据前一个的位置!
源码
// 草地元素
let stageElement = document.querySelector('#stage');
// 蛋的随机x轴坐标,也就是第几行
let eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
let eggY = Math.floor(Math.random() * 8);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
let eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇头的横坐标
let snakeX = Math.floor(Math.random() * 8);
// 蛇头的纵坐标
let snakeY = Math.floor(Math.random() * 8);
// 蛇元素数组 - 目前只有一个蛇头
let snakeArray = [{ x: snakeX, y: snakeY }];
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
// 上方向键控制蛇往上移动
document.addEventListener('keydown', function (event) {
// 蛇头,贪吃蛇数组的第0个元素
let snakeHead = snakeArray[0];
// 如果按的是 上
if (event.key === 'ArrowUp') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x - 1,
y: snakeArray[index].y
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
// 蛇是否吃到蛋
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x - 1,
y: snakeTail.y
});
}
}
// 如果按的是 下
if (event.key === 'ArrowDown') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x + 1,
y: snakeArray[index].y
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x + 1,
y: snakeTail.y
});
}
}
// 如果按的是 左
if (event.key === 'ArrowLeft') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x,
y: snakeArray[index].y - 1
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x,
y: snakeTail.y + 1
});
}
}
// 如果按的是 右
if (event.key === 'ArrowRight') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x,
y: snakeArray[index].y + 1
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 8);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 8);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x,
y: snakeTail.y - 1
});
}
}
// 删除上一个贪吃蛇
stageElement.querySelectorAll('.snake').forEach(function (item) {
item.parentElement.removeChild(item);
});
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
});
改造成功。
来看看,我们改造了什么?
主要是下面这段代码。
源码
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x - 1,
y: snakeArray[index].y
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
来分析一下,这段代码做了啥?
首先看到,定义了一个数组,蛇之二号 snakeArray2
为啥要定义它呢?用来保存蛇移动之后的数据,然后替换掉蛇移动之前的数据。
为啥要这么做呢?这是一个关于,值类型和引用类型的故事,这个故事我们这里不讲,不然这篇文章扯来扯去,可以写一本书了。
总而言之,我们的思路是没错的:用蛇的新位置,替换旧位置,蛇就移动了。
那么新位置怎么来呢?当然是来源于蛇本身。
所以,我们对蛇当前的数组,进行遍历,来生成新的位置数据。
首先第一点,蛇头是个特殊的元素。
怎么个特殊法?
它前面没有元素了,不能采取占位法去占据前一个位置,所以,它需要根据移动方向,增减横坐标或者纵坐标的值。
仔细看看,可以发现,它跟我们前面身体不随头走的时候,那里的蛇头增减逻辑是一样的。
那么,剩下的就是身体了。
身体很简单,把前一个的位置,赋值给自己,结束。
贪吃蛇自动移动
你见过,玩过完全手动控制,不会自动往前跑的贪吃蛇吗?
看到这里,你当然见过了。
但是,这是一条 非主流
的贪吃蛇,得让它自己动起来。
自动自动
,我们很容易想到一个东西:定时器
。
没错,就是用它来实现,贪吃蛇自动移动。
源码
// 草地元素
let stageElement = document.querySelector('#stage');
// 蛋的随机x轴坐标,也就是第几行
let eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
let eggY = Math.floor(Math.random() * 10);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
let eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇头的横坐标
let snakeX = Math.floor(Math.random() * 10);
// 蛇头的纵坐标
let snakeY = Math.floor(Math.random() * 10);
// 蛇元素数组 - 目前只有一个蛇头
let snakeArray = [{ x: snakeX, y: snakeY }];
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
// 默认自动往右移动
let arrow = 'ArrowRight';
// 移动循环函数
function MoveLoop() {
// 蛇头,贪吃蛇数组的第0个元素
let snakeHead = snakeArray[0];
// 如果按的是 上
if (arrow === 'ArrowUp') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x - 1,
y: snakeArray[index].y
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
// 蛇是否吃到蛋
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 10);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x - 1,
y: snakeTail.y
});
}
}
// 如果按的是 下
if (arrow === 'ArrowDown') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x + 1,
y: snakeArray[index].y
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 10);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x + 1,
y: snakeTail.y
});
}
}
// 如果按的是 左
if (arrow === 'ArrowLeft') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x,
y: snakeArray[index].y - 1
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 10);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x,
y: snakeTail.y + 1
});
}
}
// 如果按的是 右
if (arrow === 'ArrowRight') {
// 递增移动,占据前一个位置
let snakeArray2 = [];
snakeArray.forEach(function (item, index) {
if (index === 0) {
snakeArray2.push({
x: snakeArray[index].x,
y: snakeArray[index].y + 1
});
} else {
snakeArray2.push({
x: snakeArray[index - 1].x,
y: snakeArray[index - 1].y
});
}
});
snakeArray = snakeArray2;
if (snakeHead.x === eggX && snakeHead.y === eggY) {
// 蛋被吃掉了,再生成一个
eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
eggY = Math.floor(Math.random() * 10);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
eggElement = stageElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '<div class="egg"></div>';
// 蛇尾 - 蛇数组最有一个元素
let snakeTail = snakeArray[snakeArray.length - 1];
snakeArray.push({
x: snakeTail.x,
y: snakeTail.y - 1
});
}
}
// 删除上一个贪吃蛇
stageElement.querySelectorAll('.snake').forEach(function (item) {
item.parentElement.removeChild(item);
});
// 生成贪吃蛇
snakeArray.forEach(function (item, index) {
// 随机格子元素,表示蛇的位置,同上
let snakeElement = stageElement.children[item.x].children[item.y];
// 生成蛇 - 数组的第一个元素是蛇头
if (index === 0) {
snakeElement.innerHTML = '<div class="snake head"></div>';
} else {
snakeElement.innerHTML = '<div class="snake body"></div>';
}
});
}
// 上方向键控制蛇往上移动
document.addEventListener('keydown', function (event) {
// 赋值全局方向变量
arrow = event.key;
// 执行依次移动函数
MoveLoop();
});
setInterval(function () {
MoveLoop();
}, 500);
演示效果:
每半秒,也就是 500
毫秒,移动一次。
怎么做到的呢?
我们把方向提取到一个全局变量 arrow
,用它来控制移动的反向。
然后,把监听内的手动代码,全部提取到一个外部函数,外部函数根据 arrow
的值,来决定蛇的移动方向。
然后,定时器开动,蛇就自动移动了。
代码里改动非常少,但是,我相信,有很多人想不到。
这就是编程思维,四两拨千斤,看透问题的本质。
我们需要锻炼这样的思维。
陈随易的编程思维课
,就是通过一个个案例,一篇篇深入分析的文章,来培养同学们的 编程思维
。
为啥要用这种方式呢?
我想,很多同学应该都听说过,编程思维
这个词,却很难理解,也很难讲得通,更没人能讲给你听。
这是为啥呢?因为这是一种思维方式,是很难言说的。
那么怎么能够分享这种思维呢?
很简单。
张三说某某饭点的某某菜很好吃,无论张三怎么描述,李四始终半信半疑。
那怎么办呢?带李四去吃一顿不就好了。
这个非常浅显易懂的道理,很多人不懂得,也不知道。
编程怎么学习?要从本质,从根本上理解。
我是陈随易,爱好开源,乐于助人,喜欢交朋友。
希望我的文章,能够让天下没有难学的编程。
本文完整的源码文件,请在公众号
陈随易
后台回复贪吃蛇
获取。