贪吃蛇思路指迷

很多人学习编程,竟然不曾自己动手写过小游戏。
这或多或少,在编程生涯中,应该是缺少了一些乐趣的。
而没有动手写过的人,无外乎这么几种。
1,不屑。老子很牛逼,这种小儿科,浪费时间。
2,不会。思前想后,挝耳挠腮,无处下手。
3,不愿。我只对钱感兴趣,对写小游戏,没兴趣。
不知道该如何评判,也不想评判,每个人都有自己的世界观,人生观,价值观。
那么,对于那些有兴趣,却又无处下手的同学,本文将一步一步,还原小游戏《贪吃蛇》的思考过程和开发原理。

画一个框

一个复杂的东西,分解成N个简单的步骤,个个击破,这就是编程之道。
所以,第一步,我们要干嘛呢?画一个框。
玩过贪吃蛇的都知道,贪吃蛇其实是在一个正方形或者长方形范围内移动的。
所以,第一步,先把这个活动范围定好。
那么这里呢,我们给这个活动范围起一个名字,叫做,snake-grassland(蛇之草地,蛇在草地上偷蛋吃)。

/* 先重置一下 */
* {
    padding: 0;
    border: 0;
    margin: 0;
    outline: 0;
    box-sizing: border-box;
}
/* 蛇之草地 - 样式*/
.snake-grassland {
    width: 300px; /* 宽度 */
    height: 300px; /* 高度 */
    background-color: #52af4a; /* 绿色的草地 */
}


我相信,很多人写贪吃蛇的时候,脑袋里冒出了无数的想法。
蛇长什么样?
蛇蛋长什么样?
蛇活动的区域怎么设计好看?
我该用div浮动,还是position定位?
怎么去计算是否碰到了墙壁?
蛇吃到蛋之后,怎么增加蛇的身体长度?
怎么控制蛇的自动移动?
怎么控制蛇的方向?
等等...等等...不一而足。
这里,我告诉你,这些东西,不要去想!不要去想!不要去想!
因为我写这个教程,肯定是写给不会或者没有思路的同学看的。
所以,这类同学有什么特点呢?
他们对于编程的思维观念,编程技巧,眼界阅历,都处于积累的初期。
一下子N个问题冒出来需要处理,是很难理清楚逻辑和规律的,然后得出解决方法的。
所以,与其同时思考N个问题,不如先集中注意力,解决第一个小问题。
后面还有问题怎么办呢?到了那步再说呗。正所谓,今朝有酒今朝醉,明日愁来明日愁。
管它三七二十一,解决不了的问题放后面,先把能解决的解决掉。
这让我想起了读初中的时候,数学解答题不会写怎么办?先写个“解:”呗,得2分再说!

画格子

一间教室,60张椅子,那就只能坐60个人。一人一张,互不干涉,公平公正。
一家四口,那吃饭就是四双碗筷,多了一双,那一定是隔壁老王。
所以,第二步,应该要画好格子。
画好格子干嘛?放蛇蛋嘛!
做个荷包蛋,总不能直接打开了煤气灶就打蛋吧,肯定得先放锅对不对。

 /* 先重置一下 */
* {
    padding: 0;
    border: 0;
    margin: 0;
    outline: 0;
    box-sizing: border-box;
}
/* 蛇之草地 - 样式*/
.snake-grassland {
    width: 300px; /* 宽度 */
    height: 300px; /* 高度 */
    background-color: #52af4a; /* 绿色的草地 */
}
/* 行样式 */
.snake-grassland .line{
    height:10%;
}
/* 草地格子 - 样式 */
.snake-grassland .cell{
    width: 10%; /* 宽度占10分之1 */
    height: 100%; /* 高度占100分之100的行元素高度 */
    float: left; /* 每个格子左浮动 */
    border-right: 1px solid #42a03a; /* 格子边框 */
    border-bottom: 1px solid #42a03a; /* 格子边框 */
}

...

这里,有几个值得思考的地方。
1.为什么要用line规定一行,直接所有cell格子全部浮动不也能画同样的格子布局吗?
2.为什么行高和格子的宽高用百分比,不用像素呢?
3.为什么格子用float定位?不是说这个容易引起BUG,不推荐使用吗?而且你使用了,也没清除浮动呢。
4.为什么不用绝对定位布局呢?这样放蛇蛋的时候,不是更容易用z-index设置层次吗?
5.为什么使用div作为格子的元素,而不用span呢?
下面,来回答一下以上,不过,希望同学们看到上述问题的时候,可以先自行思考一下。
事情的发生,总会有它的科学道理。不然,就是迷信,就是玄学。(虽然CSS本身就是玄学。。。)
回答1:写代码,也要合乎常理,这是一个二维平面游戏,就跟教室里面的桌子排布一个道理。总不能100张桌子排成一行,然后浮动,一个挨着一个吧。这样虽然也能达到同样的效果,但是它们的本质还是二维和一维的区别,后面蛇的移动,增加身体长度,是不好计算的。所以,用行和列,第几行,第几列来定位后面的蛇蛋和蛇,是比较合理的。
回答2:为什么用百分比不用像素?使用像素,需要先知道草地宽度,再除以10,然后得到一个小数,再写上这个数值。百分比不需要知道草地宽度,不需要计算,节省时间。
回答3:为什么用float(浮动)布局。因为,简单。为什么不用清除浮动?草地已经固定了是300px * 300px,里面的格子浮动,不会对草地造成影响,不需要清除浮动。
回答4:为什么格子不用绝对定位?绝对定位需要计算每个格子距离上方的距离,左方的距离。同学,100个格子,你去计算试试。用浮动不需要计算,节省时间。
回答5:为什么格子用div来布局,不用span?个人爱好,你想用其他的也行,反正都是盒子。睡床上和地上有啥区别吗?其实区别不大,都能睡。
所以,综上所述。我们在写代码,开发项目的时候,要学会偷懒,使用能节省时间的技术,去写代码,去开发项目。因为,时间是最宝贵的。

画一个蛋

没错,终于要画蛋了。
到了这一步,我相信,肯定会有部分同学,开始纠结。
蛋怎么画?
放哪?
怎么放?
被蛇吃了怎么放新蛋?
新蛋怎么放?
新蛋放哪?
好了,不用纠结了。
大道至简,请记住前面说的,用最节省时间的方法画一个最简单的蛋!
剩下的问题,不要去思考,那是个无底洞。

 /* 先重置一下 */
* {
    padding: 0;
    border: 0;
    margin: 0;
    outline: 0;
    box-sizing: border-box;
}
/* 蛇之草地 - 样式*/
.snake-grassland {
    width: 300px; /* 宽度 */
    height: 300px; /* 高度 */
    background-color: #52af4a; /* 绿色的草地 */
}
 /* 行样式 */
.snake-grassland .line{
    height:10%;
}
 /* 草地格子 - 样式 */
.snake-grassland .cell{
    width: 10%; /* 宽度占10分之1 */
    height: 100%; /* 高度占10分之1 */
    float: left; /* 每个格子左浮动 */
    border-right: 1px solid #42a03a; /* 格子边框 */
    border-bottom: 1px solid #42a03a; /* 格子边框 */
}
 /* 那一个蛋 - 样式 */
.snake-grassland .egg{
    width: 80%; /* 蛋的宽度 */
    height: 80%; /* 蛋的高度 */
    margin: 10%; /* 蛋的外间距,让蛋居中 */
    border-radius: 50%; /* 让蛋变圆,毕竟,没见过正方形的蛋 */
    background-color: #ffc107; /* 让蛋变黄,当然,你要个红蛋,绿蛋,也行 */
}
 
...

很简单,直接在第一个格子放了一个黄黄的蛋。
虽然这一步很简单,但是它还是有深刻的含义在里面的。
1.它结束的放蛋纠结症,对吧。没什么好纠结的,就放第一个格子。
2.它让我们很直观地看到,蛋放到格子中,是什么样子的。所以,我们接下来写代码,不需要再关心蛋的样式问题了。

随机放蛋

经过上一步,我们放好了一个蛋。但是,我们总不能一直让蛋都只能出现在第一个格子里面吧?肯定是要随机放置在任意一个格子中的。
所以,到了这一步,就要开始写我们又爱又恨的javascript代码了。
首先声明,本文所有javascript皆为原生js代码,不熟悉的说明该多练练基础了。
那么,如何随机放置蛇蛋呢?
我们先列举一下知识点和思路。
1.随机函数Math.random()根据格子的行,列数量,生成范围内的横坐标和纵坐标值,两者结合,可以唯一定位一个格子位置。
2.根据随机函数生成的行,列值,取得对应的格子。
3.把蛋,放到对应的格子中。
直接写代码了吗?不,要多想。
比如,如何生成随机数比较合适呢?
了解过我的朋友都知道,我经常提到一个四字短语:意念编程。
什么意思呢?
就比如到了现在这一步,很多愣头青同学,直接动手生成随机数,然后去取元素去了!
扯淡!你知道元素结构了吗?你知道元素的嵌套方式,怎么通过随机数去获取那个元素了吗?
所以,一行代码,报三个错,不是没有道理的。
来,看一下结构。



现在知道怎么取?该取哪个元素了吗?
x轴,也就是line行的左边,是不是就是草地元素下面的children数组里面的line元素数组下标!
y轴,是不是对应的某个line元素下方的children数组里面的cell元素数组的下班!
用肉眼去观察,比用意念去瞎猫碰死耗子,去胡思乱猜不是准确得多吗?
可惜,还是有那么多人,严重使用着意念编程。
来,想学编程吗?我教你啊。
到了这一步,很多人可能找到主角,就溜之大吉了。
这不好,很不好。
还能干啥呢?除了children,其他的属性含义,作用,用法,都不想了解了解嘛?
想当年我可是每个属性的含义,作用,都一个个百度去搜了的,对于理解和使用原生javascript,是具有非常大的好处的。
好了,继续主题。请看一下,我们的格子结构,是行,列布局,所有的格子元素
那么,我们肯定需要两个随机数。第一个产生x轴,也就是横坐标值,也就是第几行。
第二个产生y轴,也就是纵坐标值,也就是第几列。
那么,我们直接生成两次0到9的随机数就好了,刚好对应10行和10行里面的每个格子。
那,为啥是0到9,不是1到10呢?
因为,数组的起始索引值是0。
----------聪明人的分割线----------
接下来,不要心急,先把随机数写好。

// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10)

来,分析一下,它凭什么可以取到0到9之间的值(包括0,包括9)。
怎么分析呢?很简单,打开菜鸟教程(runoob.com)。



找到这个函数。
找不到?回家放牛吧。
看红圈部分,不用说,这肯定是重点。
它的返回值是[0,1),之间的值。
还记得初中,高中的,开闭区间吗?
中括号表示包含,小括号表示不包含。
也就是说,Math.random()生成的值,是0到0.999...99之间的一个数。
如果用生成的随机数来乘以1,得到的最小值是0 * 1 = 0,得到的最大值是0.999...999 * 1 = 0.999...999
没毛病吧?如果有,回家放牛去吧。
把1放大10倍。最小是依然是0,最大值是多少?9.999...999
把1放大100倍。最小是依然是0,最大值是多少?99.99...999
好了,先打住,该分析Math.floor了。
老套路,打开菜鸟教程(runoob.com)



看到没?返回小于等于x的最大整数。
啥意思?
Math.floor(1.6)返回的是1

同理可得,Math.floor(9.999...999)返回的是9!!!
这不就得到,0到9之间的值了吗。
问题来了。
为啥要这样处理呢?
你获取过数组的第3.16736237个元素吗?没有吧,数组的索引必须是整数!
随机数完美收官,恭喜到了这一步的同学,又前进了一小步。
你的一小步,那真的只是一小步,真正的编程还没开始呢。
接下来,可以根据随机数,获取那个,随机格子了。

 // 草地元素
var grasslandElement = document.querySelector("#snake-grassland");
// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
var eggY = Math.floor(Math.random() * 10);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
var eggElement = grasslandElement.children[eggX].children[eggY];

可以看到,十分之完美!
再接下来可以干嘛了?
给这个随机格子,放一个蛋。

// 草地元素
var grasslandElement = document.querySelector("#snake-grassland");
// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
var eggY = Math.floor(Math.random() * 10);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
var eggElement = grasslandElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '
';

很不简单(写代码简单,把怎么写说清楚,不简单),蛇蛋终于完成了。
而此时,是凌晨1点整。
用心做教育真的不容易,痛苦并快乐着。
看到这里的同学,如果觉得本文对你有用,可以加我微信c91374286
探讨编程,和编程圈子的朋友们,一起交流,互助,提高。

随机画蛇头

一号主角,终于要上场了。
在画一号主角之前,我们来总结一下,之前的思路。
化繁为简:复杂逻辑拆分为简单逻辑,各个击破。
速战速决:颜色,样式,位置,大小,都是可以随时更改的,它们不是开发初期的核心要素。不要花太多时间在这上面,甚至不要花时间在这上面。要学会,节省时间。
很多人不知道什么是编程思维,也时常有人问我。我也说不清楚,道不明白。
案例是很好的说明方式,如果之前我没有回答清楚的,这个案例应该足以说明一部分了。
蛇怎么画呢?
这个需要好好思考了,它跟前面有点不一样,这是真正需要逻辑思考的开始。
我们知道,程序,是由数据结构+算法组成的。
他们的关系,用贪吃蛇的例子来说,那就是,蛇的组成,是数据结构。蛇能够移动和吃到蛋,是算法。
换个例子,也可以说,组成机器人的材料,是结构。机器人行,走,跳,跃,是算法。
javascript里面,有两个最为常见的结构:数组和对象。
那么,用哪个来表示蛇的结构呢?答案是,用两者组合来表示。
首先,来看看蛇的特点。
头在前,尾在后。顺序是确定性的,固定的,吃到蛋后,尾部会增长,不会增长到头前面去。
能够表示这样一个蛇的结构,很明显,数组是非常合适的,因为数组里面的元素顺序是固定的,可以往后添加元素,添加元素后,顺序也是固定的。

 // 蛇元素数组
var snakeArray = [];
                       

蛇的结构确定了,接下来要干嘛呢?
确定蛇的位置。
理解了前面蛋的生成,相必这里也不用多说。
但是呢?我们知道,蛋,永远只有一个,不会同时出现2个蛋,也不会有多个蛋排在一起(当然,你可以这么做,不过这不在本教程范围内)
而蛇呢?它的各个身体部分,是有关联的。你不能头在第一行,身体第二节在第四行,第三节在第八行。对吧,这不合理。
所以,我们需要干嘛呢?还记得前面说过的吗?我们要用数组和对象的结合,去确定蛇的位置。那么,蛇的头部以及身体部分的每个元素,都有它自己的横轴和纵轴坐标。那么,怎么表示呢?如下。

 // 蛇元素数组
var snakeArray = [{x:0,y:0}];
                       

为了方便起见,我们一开始,只生成蛇的头部。所以,数组的第一个元素,就是蛇头的位置。
可以看到,它是一个对象。数组里面放对象,对象里面放坐标,是不是很优雅?这就是数据结构的威力。
如果体会不到这威力的话,我们举个反例,用双层数组去表示。

 // 蛇元素数组
var snakeArray = [[0,0]];

                       

数组的第一个元素还是个数组,数组的第0个元素,是横轴,第1个元素是纵轴。
比起上面的的结构,那个更好点?
很明显是上面那个,代码是写给人看的,是给人写的。越直观,越通俗的代码,越好。
好了,我们来确定一下,蛇头的位置,跟蛋一样,用两个随机数来生成。(这里其实涉及到,蛇头和蛋刚好在同一个位置怎么办呢?凉拌,如果看到这篇教程的这个部分,你还不懂得,速战速决,各个击破,兵来将挡,水来土掩的道理,那可以回家放牛了。)
正所谓取舍,取舍。它从某方面来说,并不代表放弃,而是先做最重要的部分,再处理边缘部分。

 // 草地元素
var grasslandElement = document.querySelector("#snake-grassland-3");
// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
var eggY = Math.floor(Math.random() * 10);
// 随机格子元素,通过草地元素下面的children line数组对应的随机数获取
var eggElement = grasslandElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '
'; // 蛇头的横坐标 var snakeX = Math.floor(Math.random() * 10); // 蛇头的纵坐标 var snakeY = Math.floor(Math.random() * 10); // 贪吃蛇蛇元素数组 var snakeArray = [{ x: snakeX, y: snakeY }]; // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } });

贪吃蛇往上移动

蛋和蛇头都画好了,每次刷新本页面,都会看到不同位置的蛇蛋和蛇头
那么接下来呢?当然是要控制蛇的移动,让蛇能够吃到蛋。
这里呢,我们使用键盘的方向键对蛇的移动进行控制。
在进行我们的代码编写之前呢,我们一定要谨记,不要意念编程。
至少有两个问题,需要首先理清楚。
1.如何监听键盘按下的操作?
2.按下之后,如何判断按的是哪个键?
解决办法很简单:做实验。

 // 监听键盘按下,打印按下的事件
document.addEventListener('keydown',function(event){
    console.dir(event);
});

当我们按下向上的方向键。
可以看到,有多种属性,都指明了按下的键的值。
那么,选择哪个来判断呢?为了更直观地描述,这里我们选择key属性。
记住,不要一口吃个胖子,我们先控制蛇向上移动,然后,再控制它四面八方移动。
在写代码之前,我们需要思考一个事情。
如何往上移动?
我们知道,蛇的位置,依靠x轴和y轴定位。
那么,往上移动,要修改的是x的值?还是y的值呢?
按照我们的惯性思维来说,肯定是修改y的值。
但是,大家还记得前面我们怎么画格子的吗?
x表示第几行,y表示这一行的第几列。
我往上移动,是不是相当于,我的行数变小了(从上往下,依次为第0行到到第9行。)
所以,该怎么设置呢?是不是x = x - 1,对吧。
行数减小了1行嘛,缩写就是x -= 1
那么,实现代码如下。

 // 监听键盘按下
document.addEventListener('keydown',function(event){
    // 如果按的是 上
    if(event.key === 'ArrowUp'){
        snakeHead.x -= 1;
    }
    // 生成贪吃蛇
    snakeArray.forEach(function(item, index) {
        // 随机格子元素,表示蛇的位置,同上
        var snakeElement = grasslandElement.children[item.x].children[item.y];
        // 生成蛇 - 数组的第一个元素是蛇头
        if (index === 0) {
            snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); });

可以发现,按上方向键的时候,蛇头是移动了,但是之前的位置并没有清除掉原来的蛇头,所以造成了多个残影。
那么,下一步,在生产下一个位置的贪吃蛇之前,把上一个贪吃蛇删除掉。
怎么删除呢?原理很简单。
不管是蛇还是蛇的身体,都有一个共同的snake类名,找到所有当前的snake类名,然后删除就行了。

// 监听键盘按下
document.addEventListener('keydown',function(event){
    console.dir(event);
    // 如果按的是 上
    if(event.key === 'ArrowUp'){
        snakeHead.x -= 1;
    }
    // 删除上一个贪吃蛇
    grasslandElement.querySelectorAll(".snake").forEach(function(item) {
        item.parentElement.removeChild(item);
    });
    // 生成贪吃蛇
    snakeArray.forEach(function(item, index) {
        // 随机格子元素,表示蛇的位置,同上
        var snakeElement = grasslandElement.children[item.x].children[item.y];
        // 生成蛇 - 数组的第一个元素是蛇头
        if (index === 0) {
            snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); });

这里要注意一个地方,我们删除的时候,使用了parentElement属性。
这是什么意思呢?表示当前蛇元素是父级元素,也就是cell类名元素。
为什么要用它呢?因为,原生js里面,添加一个子元素,或者删除一个子元素。
都要以该元素的父级为参照。(当然,元素自己删除自己可能也有方法,同学们自己去研究下吧)。



好了,往上移动的贪吃蛇可以了。

贪吃蛇上下左右移动

还记得前面说的吗?不要一口吃个胖子,一步步来,又稳又准。
搞定了往上移动,其他三个方向,那就很简单了。

 // 上方向键控制蛇往上移动
document.addEventListener("keydown", function(event) {
    console.dir(event);
    // 蛇头,贪吃蛇数组的第0个元素
    var 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;
    }
    // 删除上一个贪吃蛇
    grasslandElement.querySelectorAll(".snake").forEach(function(item) {
        item.parentElement.removeChild(item);
    });
    // 生成贪吃蛇
    snakeArray.forEach(function(item, index) {
        // 随机格子元素,表示蛇的位置,同上
        var snakeElement = grasslandElement.children[item.x].children[item.y];
        // 生成蛇 - 数组的第一个元素是蛇头
        if (index === 0) {
            snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); });

这里,只展示监听部分代码,前面的就不放了,以免代码过长。



如上图,大家会发现一个很有意思的问题。
蛋是怎么消失的?
我们的代码里面,只有删除蛇,并没有删除蛇蛋呀?
这是因为,蛇经过的格子,都会被蛇类型占据。
经过蛋格子的时候,蛋被替换成蛇元素,然后,当我们按下下一个方向的时候,蛇的上一个位置被删除了。
所以,蛋就不见了。
注意一下,这里并不是被吃掉,是被蛇元素覆盖了。
那么,接下来,就是写,蛇如何吃蛋了。

蛇吃蛋

又到了思考时刻。
蛇怎么吃蛋呢?
还记得我们的蛇结构吗?

 // 蛇头的横坐标
var snakeX = Math.floor(Math.random() * 10);
// 蛇头的纵坐标
var snakeY = Math.floor(Math.random() * 10);
// 蛇元素数组 - 目前只有一个蛇头
var snakeArray = [{ x: snakeX, y: snakeY }];

看到没?蛇本身是一个数组,蛇的头和身体,是这个数组的每一个元素。这个元素是个对象,记录了头和身体的横坐标和纵坐标的位置
那么我们要增加蛇的身体,是不是直接往数组里面,添加身体元素就行了?
所以,蛇吃饱了之后的样子,如下。

 var 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
我就问一个问题,怎么得出追加的身体部分的横坐标和纵坐标值?
所以,事情没有那么简单。
先看代码。

 // 草地元素
var grasslandElement = document.querySelector("#snake-grassland");
// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
var eggY = Math.floor(Math.random() * 10);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
var eggElement = grasslandElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '
'; // 蛇头的横坐标 var snakeX = Math.floor(Math.random() * 10); // 蛇头的纵坐标 var snakeY = Math.floor(Math.random() * 10); // 蛇元素数组 - 目前只有一个蛇头 var snakeArray = [{ x: snakeX, y: snakeY }]; // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 - 数组的第一个元素是蛇头 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); // 上方向键控制蛇往上移动 document.addEventListener("keydown", function(event) { // 蛇头,贪吃蛇数组的第0个元素 var snakeHead = snakeArray[0]; // 如果按的是 上 if (event.key === "ArrowUp") { snakeHead.x -= 1; // 蛇是否吃到蛋 if (snakeHead.x === eggX && snakeHead.y === eggY) { // 蛋被吃掉了,再生成一个 eggX = Math.floor(Math.random() * 10); // 蛋的随机y轴坐标,也就是第几列 eggY = Math.floor(Math.random() * 10); // 随机格子元素,通过草地元素下面的children line数组对应的随机数获取 eggElement = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var 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() * 10); // 蛋的随机y轴坐标,也就是第几列 eggY = Math.floor(Math.random() * 10); // 随机格子元素,通过草地元素下面的children line数组对应的随机数获取 eggElement = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var 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() * 10); // 蛋的随机y轴坐标,也就是第几列 eggY = Math.floor(Math.random() * 10); // 随机格子元素,通过草地元素下面的children line数组对应的随机数获取 eggElement = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var 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() * 10); // 蛋的随机y轴坐标,也就是第几列 eggY = Math.floor(Math.random() * 10); // 随机格子元素,通过草地元素下面的children line数组对应的随机数获取 eggElement = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x, y: snakeTail.y - 1 }); } } // 删除上一个贪吃蛇 grasslandElement.querySelectorAll(".snake").forEach(function(item) { item.parentElement.removeChild(item); }); // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 - 数组的第一个元素是蛇头 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); });

哦,天哪,代码量瞬间多了起来。
不过,仔细看的话,其实有部分代码逻辑是一样的。
可能有疑惑,先不管,先看效果。



疑问应该挺多的。
一个个解答。
解答之前呢,我们可以看到如上图,我们已经实现了,蛇能够吃蛋,能够长身体这两个功能。
但是呢,BUG也非常之明显,身体不跟着头走。
其次呢,我们之前说过了,蛇长身体,没这么简单。
怎么不简单呢?看看代码量增加了这么多就知道了。
具体增加了什么呢?
根据不同的方向键,身体增加的方式不一样。
也就是说,我按左方向吃了蛋,身体会追加到头的右侧。
按右方向键,身体会增加到头的左侧。
按上方向键,身体会增加到头的下侧。
按下方向键,身体会增加到头的上侧。
总而言之,身体永远不会出现在头移动方向上的的前方,左方和右方,只会增加到后方。
小小的身体增加,也蕴含了一小点的策略。NICE。
----------聪明的分割线---------
继续,我们还增加了什么功能呢?
蛇吃完蛋了,总不能就没得吃了吧,对不对?
所以,每次吃了蛋,就再生一个蛋。
逻辑部分很明显了,但是重复代码比较多,这个后面会讲优化。
那么,还剩下一个重头戏,身体不跟着头走!

身随头走

到了这个地方,就不仅仅是写代码的问题了,而是编程思维的问题,思考策略的问题,代码调试的艺术的问题。
具体怎么说呢?
首先,我们要定位问题。
问题不是已经定位了吗,身体不跟着头走啊!
是的,概况的讲,是这样。但是!
你能够确定,为了让身体跟着头走,哪些代码需要修改,哪些代码不需要修改,哪些代码需要重新整理逻辑吗?
所以,解决问题,可不能纸上谈兵,要有具体的分析,具体的判断。
很多人就是缺乏这一点,所以,代码学得很难受,写得更难受。
好了,我们来分析一下。
蛇吃蛋,蛋随机再生成,跟身体不随头走这个BUG,当然是无关的。
其次,每次吃了蛋之后,根据移动的方向,追加身体元素,这里也是毫无破绽。
那么,还剩下一个什么呢?移动的方式有问题啊!
怎么个有问题法?
仔细看代码,每次移动的按方向键的时候,只有蛇头加1,减1,减1,加1,对不对?
身体部分呢?风雨不动安如山。
所以,我们需要让身体部分,也动起来。
而且,是跟着蛇头动起来。
怎么跟着蛇头动呢?
举个例子。
你要坐火车,需要排队买票。
人很多,排成一排,这个是不是就跟我我们这个贪吃蛇一模一样?
假设第一个人是蛇头,第一个人买完票,走到一遍,那么之前第一个人的位置,是不是被第二个人占据?
然后呢?后面的人,以此类推,后一个人,占据前一个人的位置。
所以,我们要怎么做?蛇头移动的时候,要让身体部分,一个个占据前一个的位置。

 // 草地元素
var grasslandElement = document.querySelector("#snake-grassland");
// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
var eggY = Math.floor(Math.random() * 10);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
var eggElement = grasslandElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '
'; // 蛇头的横坐标 var snakeX = Math.floor(Math.random() * 10); // 蛇头的纵坐标 var snakeY = Math.floor(Math.random() * 10); // 蛇元素数组 - 目前只有一个蛇头 var snakeArray = [{ x: snakeX, y: snakeY }]; // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 - 数组的第一个元素是蛇头 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); // 上方向键控制蛇往上移动 document.addEventListener("keydown", function(event) { // 蛇头,贪吃蛇数组的第0个元素 var snakeHead = snakeArray[0]; // 如果按的是 上 if (event.key === "ArrowUp") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x - 1, y: snakeTail.y }); } } // 如果按的是 下 if (event.key === "ArrowDown") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x + 1, y: snakeTail.y }); } } // 如果按的是 左 if (event.key === "ArrowLeft") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x, y: snakeTail.y + 1 }); } } // 如果按的是 右 if (event.key === "ArrowRight") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x, y: snakeTail.y - 1 }); } } // 删除上一个贪吃蛇 grasslandElement.querySelectorAll(".snake").forEach(function(item) { item.parentElement.removeChild(item); }); // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 - 数组的第一个元素是蛇头 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); });

改造成功。
来看看,我们改造了什么?
主要是下面这段代码。

 // 递增移动,占据前一个位置
var 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)
为啥要定义它呢?用来保存蛇移动之后的数据,然后替换掉蛇移动之前的数据。
为啥要这么做呢?这是一个关于,值类型和引用类型的故事,这个故事我们这里不讲,不然这篇文章扯来扯去,可以写一本书了。
总而言之,我们的思路是没错的:用蛇的新位置,替换旧位置,蛇就移动了。
那么新位置怎么来呢?当然是来源于蛇本身。
所以,我们对蛇当前的数组,进行遍历,来生成新的位置数据。
首先第一点,蛇头是个特殊的元素。
怎么个特殊法?
它前面没有元素了,不能采取占位法去占据前一个位置,所以,它需要根据移动方向,增减横坐标或者纵坐标的值。
仔细看看,可以发现,它跟我们前面身体不随头走的时候,那里的蛇头增减逻辑是一样的。
那么,剩下的就是身体了。
身体很简单,把前一个的位置,赋值给自己,结束。

贪吃蛇自动移动

没错,结束了,很是突兀,就是这么神奇。
现在,来到这一节,贪吃蛇自动移动。
玩过完全手动控制,自己不会自动移动的贪吃蛇吗?
上面那个就是,这是一条非主流的贪吃蛇。
那么,我们把它变成主流的贪吃蛇。
自动移动,很容易冒出一个词:定时器。
没错,就是用它来实现,贪吃蛇自动移动。

 // 草地元素
var grasslandElement = document.querySelector("#snake-grassland");
// 蛋的随机x轴坐标,也就是第几行
var eggX = Math.floor(Math.random() * 10);
// 蛋的随机y轴坐标,也就是第几列
var eggY = Math.floor(Math.random() * 10);
// 随机格子元素,也就是蛋需要被放置的元素
// 通过草地元素下面的children line数组对应的随机数获取
var eggElement = grasslandElement.children[eggX].children[eggY];
// 给随机格子元素放入一个蛋
eggElement.innerHTML = '
'; // 蛇头的横坐标 var snakeX = Math.floor(Math.random() * 10); // 蛇头的纵坐标 var snakeY = Math.floor(Math.random() * 10); // 蛇元素数组 - 目前只有一个蛇头 var snakeArray = [{ x: snakeX, y: snakeY }]; // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 - 数组的第一个元素是蛇头 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); // 默认自动往右移动 var arrow = "ArrowRight"; // 移动循环函数 function MoveLoop (){ // 蛇头,贪吃蛇数组的第0个元素 var snakeHead = snakeArray[0]; // 如果按的是 上 if (arrow === "ArrowUp") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x - 1, y: snakeTail.y }); } } // 如果按的是 下 if (arrow === "ArrowDown") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x + 1, y: snakeTail.y }); } } // 如果按的是 左 if (arrow === "ArrowLeft") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x, y: snakeTail.y + 1 }); } } // 如果按的是 右 if (arrow === "ArrowRight") { // 递增移动,占据前一个位置 var 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 = grasslandElement.children[eggX].children[eggY]; // 给随机格子元素放入一个蛋 eggElement.innerHTML = '
'; // 蛇尾 - 蛇数组最有一个元素 var snakeTail = snakeArray[snakeArray.length - 1]; snakeArray.push({ x: snakeTail.x, y: snakeTail.y - 1 }); } } // 删除上一个贪吃蛇 grasslandElement.querySelectorAll(".snake").forEach(function(item) { item.parentElement.removeChild(item); }); // 生成贪吃蛇 snakeArray.forEach(function(item, index) { // 随机格子元素,表示蛇的位置,同上 var snakeElement = grasslandElement.children[item.x].children[item.y]; // 生成蛇 - 数组的第一个元素是蛇头 if (index === 0) { snakeElement.innerHTML = '
'; } else { snakeElement.innerHTML = '
'; } }); } // 上方向键控制蛇往上移动 document.addEventListener("keydown", function(event) { // 赋值全局方向变量 arrow = event.key; // 执行依次移动函数 MoveLoop(); }); setInterval(function(){ MoveLoop(); },500);

每半秒,也就是500毫秒,移动一次。
怎么做到的呢?
我们把方向提取到一个全局变量,arrow,用它来控制移动的反向。
然后,把监听内的手动代码,全部提取到一个外部函数,外部函数根据arrow的值,来决定蛇的移动方向。
然后,定时器开动,蛇就自动移动了。
代码里改动非常少,但是,我相信,有很多人想不到。
这就是编程思维,四两拨千斤,看透问题的本质。
我们需要锻炼这样的思维。
本网站的目的之一,就是锻炼通过一个个案例,一篇篇深入分析的文章,来培养同学们的编程思维。
为啥要用这种方式呢?
我想,很多同学应该都听说过,编程思维这个词,却很难理解,也很难讲得通。
这是为啥呢?因为这是一种思维方式,是很难言说的。
那么怎么能够分享这种思维呢?
很简单。
张三说某某饭点的某某菜很好吃,无论张三怎么描述,李四始终半信半疑。
那怎么办呢?带李四去吃一顿不就好了。
这个非常浅显易懂的道理,很多人不懂得,也不知道。
编程怎么学习?要从原理,从根本上理解。
我是陈随易,座右铭是:何以解忧,唯有代码。
我喜欢分享,喜欢技术,喜欢交友,目前致力于降低编程的学习难度,让天下没有难学的编程。
编程很简单,只是你还不曾学会编程思维而已,想学的话,我教你啊。

你可能感兴趣的:(贪吃蛇思路指迷)