看起来好像很复杂的贪吃蛇,到底是怎么用JavaScript去实现的?
其实你只要会用setInterval去现实一个时钟,
会使用JavaScript的键盘响应事件
会使用JavaScript去操作html节点
也就是HTML DOM与BOM,这个问题应该不会有太大的问题。
下面就来一步一步地,剖析怎么用JavaScript,也就是说完全可以记事本写一个html文件,放在任意一个浏览器中,把贪吃蛇搞起来!
一、基本目标
写一个贪吃蛇游戏,相信不用介绍了,这个游戏实在是太出名了,
能用wasd、上下左右与网页上的button按钮组件,来控制蛇的移动方向
按P可以暂停。
二、制作过程
1、布置场景。
这个实在是太简单。就用纯HTML语言布置几行文本,一个纯黑色的400x300的map图层,与一个放置上下左右按钮的key图层。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>贪吃蛇</title> </head> <body> <p> <strong>贪吃蛇~</strong> </p> <p> 按P暂停~wasd或↑↓←→可控制方向~ </p> <p> 积分: <span id="count"></span> </p> <div id="map" style="background-color: #000000; width: 400px; height: 300px; position: absolute;"></div> <div id="key" style="margin-top: 300px; margin-left: 150px; position: absolute;"> <table> <tr align="center"> <td colspan="3"> <input type="button" value="↑" onclick="snake.setKey(38);" /> </td> </tr> <tr> <td> <input type="button" value="←" onclick="snake.setKey(37);" /> </td> <td> <input type="button" value="↓" onclick="snake.setKey(40);" /> </td> <td> <input type="button" value="→" onclick="snake.setKey(39);" /> </td> </tr> </table> </div> </body> </html>
上下左右的输入应该为&uarr;&darr;&larr;&rarr;
2、脚本的基本框架
之后,在</html>下面怒写下<script></script>,然后在这两个标签中间,对此网页进行编程
脚本脚本,按常理就应该在网页的脚下,但现在根据需要都基本镶嵌在网页之中了。
3、键盘响应事件
说这个之前,必须提及部分关于Javascript理论知识,
虽然翻阅两门语言的产生背景,可以发现Javascript跟Java一点关系都没有,但Javascript中同样存在类似C语言函数与主函数的概念。
window.onload = function() {...}相当于主函数,网页一载入就执行此函数中的内容,而其余的function,如function snake() {...},则相当于等待被调用的函数,
如果你需要在主函数中调用此函数,那么必须此前使用函数声明,比如我要使用snake()函数,就必须在window.onload = function() {...}函数之前,使用var snake = new snake();声明此函数。
函数中,调用函数内变量需要在之前加this.,使用函数内变量同样需要用到this.
在函数中,声明一个方法需要使用类似于this.setKey = function(code) {}的形式。
/*声明使用snake()函数*/ var snake = new snake(); window.onload = function() { /*键盘响应事件,javascript的特定写法,为了兼容所有浏览器,code是用户所按键位的键值*/ document.onkeydown = function(event) { var code; if (window.event) { code = window.event.keyCode; } else { code = event.keyCode; } if (code == 80) alert("pause"); /*调用snake函数中的setKey方法*/ snake.setKey(code); } } function snake() { /*蛇前进方向默认为向右,传过来的键值的不同,可以改变蛇的前进方向*/ this.direct = 'right'; this.setKey = function(code) { switch (code) { case 37: this.direct = 'left'; break; case 38: this.direct = 'top'; break; case 39: this.direct = 'right'; break; case 40: this.direct = 'bottom'; break; case 65: this.direct = 'left'; break; case 87: this.direct = 'top'; break; case 68: this.direct = 'right'; break; case 83: this.direct = 'bottom'; default: break; } } }
4.蛇的绘制,定义一条最基本的蛇
定义“蛇”是由若干个10x10方块组成,
视图把整个400x300的场景想象为是20x10个10x10的方格组成,为一个横轴为0-20,纵轴为0-10的坐标系
组成“蛇”的每个方块都有自己的横坐标与纵坐标。
那么蛇就在场景移动。
其中蛇尾是需要与背景蛇同样颜色的方块,这与蛇的移动有关,在蛇的移动中会详细解释。
具体到程序中就是一个二维数组,是由若干个:第一个元素为方块的横坐标,第二个元素为方块的纵坐标 ,第三个元素为方块的颜色的数组,组成的数组。
在function snake() {}定义二维数组body,this.body = [ [ 3, 2, '#ffffff' ], [ 2, 2, '#ffffff' ], [ 1, 2, '#ffffff' ], [ 0, 2, '#000000' ] ];与this.display = function() {}方法,
在window.onload = function() {} “主函数”中,声明调用函数snake中的snake.display()方法。
于是<script></script>中的代码就变成如下情况:
var snake = new snake(); window.onload = function() { snake.display(); document.onkeydown = function(event) { var code; if (window.event) { code = window.event.keyCode; } else { code = event.keyCode; } if (code == 80) alert("pause"); snake.setKey(code); } } function snake() { this.direct = 'right'; this.setKey = function(code) { switch (code) { case 37: this.direct = 'left'; break; case 38: this.direct = 'top'; break; case 39: this.direct = 'right'; break; case 40: this.direct = 'bottom'; break; case 65: this.direct = 'left'; break; case 87: this.direct = 'top'; break; case 68: this.direct = 'right'; break; case 83: this.direct = 'bottom'; default: break; } } /*初始化蛇,长度为3,蛇身的最后一个方格是不属于蛇的,为后面的移动方法this.move=function(){}做铺垫的*/ this.body = [ [ 3, 2, '#ffffff' ], [ 2, 2, '#ffffff' ], [ 1, 2, '#ffffff' ], [ 0, 2, '#000000' ] ]; this.display = function() { for ( var i = 0; i < this.body.length; i++) { /*创建一个图层节点<div></div>此图层的大小为10x10,颜色就是二维数组中每个元素的第三个元素,对齐方式当然是任意*/ /*其位置等于二维数组中每个元素的第一个元素乘以10,高度为二维数组中每个元素的第二个元素等于10*/ /*之后把这个图层节点放到<div id="map"></div>中,创建完第一个,就创建第二个……以此类推,保护最后那个与背景色相同的蛇身方块*/ var bodycell; bodycell = document.createElement("div"); bodycell.style.width = 10 + "px"; bodycell.style.height = 10 + "px"; bodycell.style.backgroundColor = this.body[i][2]; bodycell.style.position = "absolute"; bodycell.style.left = this.body[i][0] * 10 + "px"; bodycell.style.top = this.body[i][1] * 10 + "px"; document.getElementById("map").appendChild(bodycell); } } }
5.蛇的移动,此乃最难的一步,最关键的一步,整个程序是否能够憋出来,就看程序猿是否能够弄清楚这一步是怎么实现的
整体思想是把前面一个方格元素的横纵坐标赋值给后面一个元素的横纵坐标,也就是说,使后面一个元素的横纵坐标的值等于前面一个方格元素的横纵坐标的值,
而蛇头的横纵坐标则根据此时的移动方向this.direct来对横纵坐标进行处理。
之后确定好新的横纵坐标之后,重绘这条蛇。
至于为什么最后一个蛇身元素要与背景色一样,那是因为,你需要把蛇尾那个方格抹去。
蛇,随着this.move()中被window.onload = function() {} 主函数是在不停地调用,也就是通过setInterval('snake.move()', 100);每隔100毫秒也就是0.1秒就调用一次snake.move(),this.display()也在不停地调用,换而言之就是蛇不地被重绘。
那么每一次重绘,之前的画出来的蛇依旧是存在的,你必须把上一次重绘出来的蛇抹掉,那么蛇才能不停的移动,当然,一会儿判断蛇是否撞到自己,最后那个方格也不判断之列了。
至于上一次重绘出来的蛇怎么地抹掉,你要么选择清屏,但这不是好办法,程序你没法写,于是就把蛇尾定义成与背景一样的颜色,蛇在不停地移动,自然而然地就把之前的作图痕迹抹得干干净净了!
在想不通就在初始化的时候,把最后一个方块的颜色换成其他,整个过程你会看得清清楚楚。
于是,我们的脚本程序就大致变成这个模样了:
<script> var snake = new snake(); window.onload = function() { snake.display(); food.display(); document.onkeydown = function(event) { var code; if (window.event) { code = window.event.keyCode; } else { code = event.keyCode; } if (code == 80) alert("pause"); snake.setKey(code); } setInterval('snake.move()', 100); } function snake() { this.setKey = function(code) { switch (code) { case 37: this.direct = 'left'; break; case 38: this.direct = 'top'; break; case 39: this.direct = 'right'; break; case 40: this.direct = 'bottom'; break; case 65: this.direct = 'left'; break; case 87: this.direct = 'top'; break; case 68: this.direct = 'right'; break; case 83: this.direct = 'bottom'; default: break; } } this.body = [ [ 3, 2, '#ffffff' ], [ 2, 2, '#ffffff' ], [ 1, 2, '#ffffff' ], [ 0, 2, '#000000' ] ]; this.display = function() { for ( var i = 0; i < this.body.length; i++) { var bodycell; bodycell = document.createElement("div"); bodycell.style.width = 10 + "px"; bodycell.style.height = 10 + "px"; bodycell.style.backgroundColor = this.body[i][2]; bodycell.style.position = "absolute"; bodycell.style.left = this.body[i][0] * 10 + "px"; bodycell.style.top = this.body[i][1] * 10 + "px"; document.getElementById("map").appendChild(bodycell); } } this.direct = 'right'; this.move = function() { /*因为是使后面一个元素的横纵坐标的值等于前面一个方格元素的横纵坐标的值,所以必须从后面开始处理,从前面开始处理会出现错误的*/ for ( var i = this.body.length - 1; i > 0; i--) { this.body[i][0] = this.body[i - 1][0]; this.body[i][1] = this.body[i - 1][1]; } switch (this.direct) { case 'left': this.body[0][0] = this.body[0][0] - 1; break; case 'top': this.body[0][1] = this.body[0][1] - 1; break; case 'right': this.body[0][0] = this.body[0][0] + 1; break; case 'bottom': this.body[0][1] = this.body[0][1] + 1; break; } this.display(); } } </script>
6.蛇是否撞到墙了?
相当简单,只要判断蛇头的纵横坐标是否等于边界的纵横坐标。
当然,游戏结束,就不能让这条蛇继续移动了,那么,我们就打断程序对snake.move()的不停调用即可。
同时弹窗,显示积分count与提示信息,此count一会儿在对食物的处理中计算,为一个全局变量,刷新一下页面,让游戏重新开始,让用户继续挑战,玩上瘾什么的就最好了。
要判断的地方一共有4个,因为有四面墙。
在snake.move()中加入判断,于是程序就变成这个样子:
<script> /*用来积分的,处理在食物模块中*/ var count = -1; var snake = new snake(); /*用来打断循环调用snake.move()的*/ var timer; window.onload = function() { snake.display(); food.display(); document.onkeydown = function(event) { var code; if (window.event) { code = window.event.keyCode; } else { code = event.keyCode; } if (code == 80) alert("pause"); snake.setKey(code); } timer = setInterval('snake.move()', 100); } function snake() { this.setKey = function(code) { switch (code) { case 37: this.direct = 'left'; break; case 38: this.direct = 'top'; break; case 39: this.direct = 'right'; break; case 40: this.direct = 'bottom'; break; case 65: this.direct = 'left'; break; case 87: this.direct = 'top'; break; case 68: this.direct = 'right'; break; case 83: this.direct = 'bottom'; default: break; } } this.body = [ [ 3, 2, '#ffffff' ], [ 2, 2, '#ffffff' ], [ 1, 2, '#ffffff' ], [ 0, 2, '#000000' ] ]; this.display = function() { for ( var i = 0; i < this.body.length; i++) { var bodycell; bodycell = document.createElement("div"); bodycell.style.width = 10 + "px"; bodycell.style.height = 10 + "px"; bodycell.style.backgroundColor = this.body[i][2]; bodycell.style.position = "absolute"; bodycell.style.left = this.body[i][0] * 10 + "px"; bodycell.style.top = this.body[i][1] * 10 + "px"; document.getElementById("map").appendChild(bodycell); } } this.direct = 'right'; this.move = function() { for ( var i = this.body.length - 1; i > 0; i--) { this.body[i][0] = this.body[i - 1][0]; this.body[i][1] = this.body[i - 1][1]; } switch (this.direct) { case 'left': this.body[0][0] = this.body[0][0] - 1; break; case 'top': this.body[0][1] = this.body[0][1] - 1; break; case 'right': this.body[0][0] = this.body[0][0] + 1; break; case 'bottom': this.body[0][1] = this.body[0][1] + 1; break; } /*history.go(0)为刷新本页,clearTimeout(timer)用来打断timer所指向的循环调用*/ if (this.body[0][0] == 40 || this.body[0][0] == -1 || this.body[0][1] == -1 || this.body[0][1] == 30) { alert("Game Over," + "积分:" + count); history.go(0); clearTimeout(timer); } this.display(); } } </script>
7.蛇是否撞到自己了?
继续在snake.move()中加入判断,
加入一个循环,判断蛇头的纵横坐标是否等于蛇身所有方块的纵横坐标,当然,最后一个蛇身方块就不要判断了,这是用来抹掉作图痕迹的方块来的,
如果等于,就是游戏结束了,就同上面的三部曲,弹窗,刷新,中断函数循环调用
8.最后一步,食物的生成,与蛇吃到食物之后怎么处理?
这一步的逻辑还是有点难度的,难就难在应该想透:
在snake.move()函数中加入遇到食物的判断,这个判断有点类似于数据结构中对单链表删除处理的步骤,具体见下图:
如果蛇吃到食物之后,
就应该压一个元素进蛇身this.body这一二维数组,就是this.body这一二维数组在最后添加一个元素。
加入元素的纵横坐标等于蛇此时倒数第二个元素的纵横坐标。
此元素的颜色任意,但是应该把此时蛇身的倒数第二个元素的颜色设置为背景色,倒数第三个元素颜色设置为蛇身颜色,因为蛇在下一步的重绘中,后一个元素的所有东西,就变成前一个元素的所有。
之后是不难题了,重绘食物就是绘出一个小方块,纵横坐标在400x300这个场景中产生,
Math.random()能够产生一个从0-1的随机小数,Math.floor();可以取上限,
同时全局计分变量count自增,更新一下,在html部分静态行内文本<span id="count"></span>的数据。
食物一开始就要显示,所以在window.onload = function() {}要放入food.display(),
于是乎,这个贪吃蛇游戏就大功告成,全代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>贪吃蛇</title> </head> <body> <p> <strong>贪吃蛇~</strong> </p> <p> 按P暂停~wasd或↑↓←→可控制方向~ </p> <p> 积分: <span id="count"></span> </p> <div id="map" style="background-color: #000000; width: 400px; height: 300px; position: absolute;"></div> <div id="key" style="margin-top: 300px; margin-left: 150px; position: absolute;"> <table> <tr align="center"> <td colspan="3"> <input type="button" value="↑" onclick="snake.setKey(38);" /> </td> </tr> <tr> <td> <input type="button" value="←" onclick="snake.setKey(37);" /> </td> <td> <input type="button" value="↓" onclick="snake.setKey(40);" /> </td> <td> <input type="button" value="→" onclick="snake.setKey(39);" /> </td> </tr> </table> </div> </body> </html> <script> var count = -1; var snake = new snake(); var food = new food(); var timer; window.onload = function() { snake.display(); food.display(); document.onkeydown = function(event) { var code; if (window.event) { code = window.event.keyCode; } else { code = event.keyCode; } if (code == 80) alert("pause"); snake.setKey(code); } timer = setInterval('snake.move()', 100); } function food() { var foodcell; this.x = null; this.y = null; this.display = function() { count = count + 1; document.getElementById("count").innerHTML = count.toString(); this.x = Math.floor(Math.random() * 39); this.y = Math.floor(Math.random() * 29); foodcell = document.createElement("div"); foodcell.style.width = 10 + "px"; foodcell.style.height = 10 + "px"; foodcell.style.backgroundColor = "#ffff00"; foodcell.style.position = "absolute"; foodcell.style.left = this.x * 10 + "px"; foodcell.style.top = this.y * 10 + "px"; document.getElementById("map").appendChild(foodcell); } } function snake() { this.setKey = function(code) { switch (code) { case 37: this.direct = 'left'; break; case 38: this.direct = 'top'; break; case 39: this.direct = 'right'; break; case 40: this.direct = 'bottom'; break; case 65: this.direct = 'left'; break; case 87: this.direct = 'top'; break; case 68: this.direct = 'right'; break; case 83: this.direct = 'bottom'; default: break; } } this.body = [ [ 3, 2, '#ffffff' ], [ 2, 2, '#ffffff' ], [ 1, 2, '#ffffff' ], [ 0, 2, '#000000' ] ]; this.display = function() { for ( var i = 0; i < this.body.length; i++) { var bodycell; bodycell = document.createElement("div"); bodycell.style.width = 10 + "px"; bodycell.style.height = 10 + "px"; bodycell.style.backgroundColor = this.body[i][2]; bodycell.style.position = "absolute"; bodycell.style.left = this.body[i][0] * 10 + "px"; bodycell.style.top = this.body[i][1] * 10 + "px"; document.getElementById("map").appendChild(bodycell); } } this.direct = 'right'; this.move = function() { for ( var i = this.body.length - 1; i > 0; i--) { this.body[i][0] = this.body[i - 1][0]; this.body[i][1] = this.body[i - 1][1]; } switch (this.direct) { case 'left': this.body[0][0] = this.body[0][0] - 1; break; case 'top': this.body[0][1] = this.body[0][1] - 1; break; case 'right': this.body[0][0] = this.body[0][0] + 1; break; case 'bottom': this.body[0][1] = this.body[0][1] + 1; break; } if (this.body[0][0] == food.x && this.body[0][1] == food.y) { var x = this.body[this.body.length - 1][0]; var y = this.body[this.body.length - 1][1]; this.body.push( [ x, y, 'black' ]); this.body[this.body.length - 1][2] = "#000000"; this.body[this.body.length - 2][2] = "#ffffff"; food.display(); } if (this.body[0][0] == 40 || this.body[0][0] == -1 || this.body[0][1] == -1 || this.body[0][1] == 30) { alert("Game Over," + "积分:" + count); history.go(0); clearTimeout(timer); } for ( var i = 1; i < this.body.length - 1; i++) { if (this.body[0][0] == this.body[i][0] && this.body[0][1] == this.body[i][1]) { alert("Game Over," + "积分:" + count); history.go(0); clearTimeout(timer); } } this.display(); } } </script>
完结撒花~一共179行代码,代码不长,有几个关键的逻辑比较难,克服了,就能够写出来了。没什么神秘的。
或许有时间,可以试试c,c#,java,java swing+awt,安卓,c++mfc……贪吃蛇?