在很早学 QuickBasic 的时候,就有一个蛇吃食物的游戏,当时是作为 Basic 的一个示例提供的。当时只顾得玩了,竟没想到要把那些高人写的代码好好分析一下。现在做 Web 开发的培训,有一个用 Javascript 写的类似的小游戏,就花了大半天好好看了一下代码,觉得写的不错。
功能:蛇吃食物有四种情况:走到的地方有食物;走到的地方没有食物;走到的地方是墙壁;走到的地方是自己的身体。吃到食物后,蛇的身体会变长;碰到墙壁或咬到自己 Game Over,询问是否重新开始。
整个游戏是在一个<div>框子(地图)里展开的,蛇有地图里的一系列<div>构成,初始状态(刚刚打开页面)蛇是一个<div>框,蛇头和蛇尾是在一起的,就是这个<div>框,以后吃了食物后<div>个数会变多,蛇就会长长。食物用一个<span>框表示,初始时和蛇一样其位置是随机产生的。蛇和食物的坐标用<div>和<span>的绝对位置表示。
算法的关键是当蛇移动到新的位置后,判断前述的几种情况,做出相应的处理。而二维数组 Map[][] 就是判断的依据。二维数组 Map[][]通过其元素的值来表示蛇身、食物和空地。蛇身、食物和空地的值分别为’S'、’F’ 和 ‘0′。
用 Javascript 编程和其他语言的一个大的区别就是键盘的控制代码不同。js是通过 Key=event.keyCode 语句获取键盘码,再在多分支 switch 结构里分别处理。看完这些代码就会对浏览器里的键盘控制有所了解了。
当然该段代码的最大价值还是让我们了解到网页游戏的大概写法,而算法其实是最重要的,把一个游戏的设想变为一堆包含许多的函数的结构化的代码,是值得我们借鉴和学习的。这里的难点是确定蛇是怎么移动的。
代码里有我的注释,结合我的以上大概分析介绍差不多能看懂了。
题外问题:
本人在调试的时候打算给原来的代码加一段地图内方格显示功能,就写了个ShowGrid()函数,采用代码生成方格。但由于在2层循环内完成方格显示,效率十分低下,生成15*10的方格需要大约6秒。生成30*20 的方格竟然需要几分钟。基本不能采用此方法。改进以后,不是生成多个包含单个单元格的表格,而是生成一个包含若干单元格的表格,结果效率有巨大的提高。
改进后,有如下功能:
- 可改变单元格数;
- 可控制暂停;
- 速度控制功能;
- 蛇头、蛇身和蛇尾形状美化;
- 可控制显示和隐藏背景图片、方格线;
- 修正了长度等于或大于2节的蛇体,按倒退键时结束游戏的错误;
进一步改进建议:
- 控制蛇头的方向,可用动画图片代替蛇头。
- 蛇身和蛇尾也可用图片代替。
- 增加声音;
- 增加换肤功能:背景、方格、蛇的皮肤可定制;
- 自动演示功能;
……
- <HTML>
- <head>
- <meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″>
- <title>贪吃蛇</title>
- <style>
- body{font-size: 12px;}
- .food
- {
- background: url(”images/food1.gif”) no-repeat;
- /*background-color:green;*/
- overflow:hidden;
- position:absolute;
- }
- .snake_head
- {
- background-color: red;
- border:3px solid gold;
- position:absolute;
- }
- .snake_body
- {
- background-color: orange;
- border:2px dotted white;
- position:absolute;
- }
- .snake_tail
- {
- background-color: peachpuff;
- border:2px dotted white;
- position:absolute;
- }
- .grid
- {
- border-style:solid;
- border-color:#0000ff;
- border-top-width:0;
- border-right-width:1;
- border-bottom-width:1;
- border-left-width:0;
- padding:0;
- }
- .mainmap
- {
- position:absolute;
- border-style:outset;
- border-color:#0000ff;
- border-width:5;
- }
- .table
- {
- position:absolute;
- overflow:hidden;
- border-collapse:collapse;
- }
- </style>
- </head>
- <BODY>
- <div id=”help”>
- 红色方块表示蛇头,按方向键控制蛇吃食物<br><br>
- 行数:
- <select id=”rows”>
- <option>25</option>
- <script language=”javascript”>
- for(var ct=10;ct<=30;ct++)
- document.write(’<option>’ + ct + ‘</option>’);
- </script>
- </select>
- <br>
- 列数:
- <select id=”cells”>
- <option>30</option>
- <script language=”javascript”>
- for(var ct=10;ct<=40;ct++)
- document.write(’<option>’ + ct + ‘</option>’);
- </script>
- </select>
- <br>
- <button onclick=”reCreateMap()”> 设置 </button>
- <br><br>
- 【初始化:】F5 功能键<br>
- 【暂停:】S 字母键<br><br>
- 【显示/隐藏网格:】G 字母键<br><br>
- 【加速:】PageUP 键<br>
- 【减速:】PageDown 键<br>
- </div>
- 【速度:】<span id=”speed” style=”color:red”></span>
- <br><br><div id=”mapxy”></div>
- <div
- <br><br>
- 蛇只能吃食物,<br>不能触墙,<br>也不能咬自己
- </div>
- <script langyage=”javascript”>
- var Rows = document.getElementById(’rows’).options[0].text;
- var Cells = document.getElementById(’cells’).options[0].text;
- var Num = 20; //正方形格子的边长
- var SpeedUp = 5000;
- var Times = 200;
- var Start = 0;
- var ShowGrid = true;
- var ShowBackground = true;
- var BorderWidth = 5;
- var MainMap = null;
- var AllDiv = new Array();
- var AllSpan = new Array();
- var Sx = Sy = 0;
- var Map = null;
- var GoX,GoY,LastX,LastY;
- var moving = null;
- var AllDiv=null, AllSpan=null;
- //重新创建地图
- function reCreateMap()
- {
- Rows = document.getElementById(’rows’).options[document.getElementById(’rows’).selectedIndex].text;
- Cells = document.getElementById(’cells’).options[document.getElementById(’cells’).selectedIndex].text;
- document.body.removeChild(MainMap);
- CreateMap();
- }
- //创建地图
- function CreateMap()
- {
- BW = eval(Cells * Num + 2 * BorderWidth); //宽度
- BH = eval(Rows * Num + 2 * BorderWidth); //高度
- //document.body.innerHTML+=’<div id=MainMap style=position:absolute;left:’+(document.body.clientWidth-BW)/2+’;top:’+(document.body.clientHeight-BH)/2+’;width:’+BW+’;height:’+BH+’;border-width:’+BorderWidth+’;></div>’
- MainMap = document.createElement(’div’);
- MainMap.className = ‘mainmap’;
- if(ShowBackground)
- MainMap.style.backgroundImage = “url(’images/bgpic.jpg’)”;
- MainMap.style.left = (document.body.clientWidth - BW) / 2;
- MainMap.style.top = (document.body.clientHeight - BH) / 2;
- MainMap.style.width = BW;
- MainMap.style.height = BH;
- document.body.appendChild(MainMap);
- //创建全局数组Map[]
- /*
- Map = new Array() //创建全局数组Map[]
- for(y=0; y<Rows; y++)
- {
- Map[y]=new Array() //创建全局二维数组Map[][],初始值为’0′
- for(x=0; x<Cells; x++)
- Map[y][x] = ‘0′ //’0′值表示“空地”
- }
- */
- Map = new Array();
- for(y = 0; y < Rows; y++)
- {
- //创建全局二维数组Map[][],初始值为’0′
- Map.push(new Array());
- for( x = 0; x < Cells; x++)
- Map[y].push(’0′); //’0′值表示“空地”
- }
- //显示地图内格子
- if (ShowGrid)
- MainMap.appendChild(CreateGrid(’ ‘));
- //创建全局变量Sx,赋予随机数
- Sx = parseInt(Math.random() * Cells);
- //创建全局变量Sy,赋予随机数
- Sy = parseInt(Math.random() * Rows);
- //生成蛇 - div
- CreateSnake();
- //生成食物 - span
- CreatFood();
- //创建全局数组AllDiv,保存着蛇身各节<div>。AllDiv[0]为蛇尾。MainMap是div的ID
- AllDiv = MainMap.getElementsByTagName(’div’); //等价于AllDiv = MainMap.all.tags(’DIV’)
- //创建全局数组AllSpan,始终只有一个元素AllSpan[0]
- AllSpan = MainMap.getElementsByTagName(’span’); //等价于AllSpan = MainMap.all.tags(’SPAN’)
- }
- //创建地图内格子(Rows*Cells 大小的 Table)
- function CreateGrid(celltext)
- {
- var table = document.createElement(’table’);
- table.className = ‘table’;
- table.style.left = 0;
- table.style.top = 0;
- table.setAttribute(’id’,'grid’);
- table.setAttribute(’border’,'0′); //table.border = ‘0′;
- table.setAttribute(’cellspacing’,'0′);
- table.setAttribute(’cellpadding’,'0′);
- var tbody = document.createElement(”tbody”);
- table.insertBefore(tbody, null);
- for (var i=0; i<Rows; i++)
- {
- var tr = document.createElement(’tr’);
- for(var j=0; j<Cells; j++)
- {
- var td = document.createElement(’td’);
- td.className = ‘grid’;
- td.width = Num; //td.style.width = Num;
- td.height = Num; //td.style.height = Num;
- var text = document.createTextNode(celltext);
- td.insertBefore(text, null);
- tr.insertBefore(td, null);
- }
- tbody.insertBefore(tr, null);
- }
- return table;
- }
- //创建蛇的位置,赋予值’S’
- function CreateSnake()
- {
- //<div>表示蛇身,通过调用本函数,可以累加到若干个,蛇身变长
- //注意 y 和 x 是“蛇”<div> 的自定义属性。一直保存着蛇身体各节的Map坐标,与Map[][]联系
- //初始时,蛇头、蛇尾是同一个位置
- var div = document.createElement(’div’);
- div.className = ’snake_head’;
- div.setAttribute(’x',Sx);
- div.setAttribute(’y',Sy);
- div.style.left = Sx * Num;
- div.style.top = Sy * Num;
- div.style.width = Num;
- div.style.height = Num;
- MainMap.appendChild(div);
- Map[Sy][Sx] = ‘S’; //Snake首字母
- //蛇身长度
- //document.all.mapxy.innerHTML = AllDiv.length;
- if (AllDiv!=null)
- {
- if (AllDiv.length>1)
- {
- AllDiv[0].className = ’snake_tail’; //蛇尾
- for (var i=1;i<AllDiv.length-1;i++) //蛇身
- AllDiv[i].className = ’snake_body’;
- }
- }
- }
- //创建食物的位置,赋予初始值’F’
- //食物的初始位置不能与蛇的初始位置相同,只能在空地放置食物。
- //若随机产生的2位置相同,则递归执行,直到不相同为止
- function CreatFood()
- {
- Fx = parseInt(Math.random() * Cells);
- Fy = parseInt(Math.random() * Rows);
- if(Map[Fy][Fx] == ‘0′) //如果是空地
- {
- MainMap.appendChild(CreatSpan(Fx * Num,Fy * Num,Num));
- Map[Fy][Fx] = ‘F’; //Food首字母
- }
- else
- {
- CreatFood(); //递归
- }
- }
- //生成食物的span
- function CreatSpan(l,t,num)
- {
- var span = document.createElement(’span’);
- span.style.left = l;
- span.style.top = t;
- span.style.width = num;
- span.style.height = num;
- span.className = ‘food’;
- return span;
- }
- //主移动–判断蛇头前面的是什么
- function Move()
- {
- //自动行走,Map[Sy][Sx]为蛇头前面位置
- Sx += GoX;
- Sy += GoY;
- //碰墙,重新开始
- if(Sy < 0 || Sy >= Rows)
- {
- Move1();
- }
- else
- {
- SnakeFront = Map[Sy][Sx];
- switch(SnakeFront)
- {
- case ‘0′: //蛇前是空地
- Move2();
- break;
- case ‘F’: //蛇前面是食物
- Move3();
- break;
- case ‘S’: //蛇前面是自己的身体
- Move1();
- break;
- default: //啥都不是就是超出地图范围game over
- Move1();
- }
- }
- }
- //重新开始
- function Move1()
- {
- if(confirm(”Game Over,重新开始?”))
- {
- //window.location.reload();
- reCreateMap();
- Start = 0;
- }
- else
- window.close();
- }
- //蛇行走到的当前位置是空地时
- function Move2()
- {
- //蛇走开后,把原位置设置为’0′,表示是空地
- //把蛇数组当前元素删除,在下面的 CreateSnake()语句重新生成
- Map[AllDiv[0].getAttribute(’y')][AllDiv[0].getAttribute(’x')] = ‘0′;
- var fatherNode = AllDiv[0].parentNode; //AllDiv[0].removeNode(true);
- fatherNode.removeChild(AllDiv[0]);
- //在新的位置生成蛇的<div>
- CreateSnake();
- //再次移动
- moving = setTimeout(’Move()’,Times);
- }
- //蛇行走到的当前位置是食物时
- function Move3()
- {
- //蛇数组当前元素不删除,<div>累加一次,蛇长长一节
- CreateSnake();
- //把食物数组当前元素删除,在下面的 CreatFood()语句重新生成
- var parentNode = AllSpan[0].parentNode; //AllSpan[0].removeNode(true)
- parentNode.removeChild(AllSpan[0]);
- //再次随机生成食物
- CreatFood();
- //再次移动
- moving = setTimeout(’Move()’,Times);
- }
- //蛇行加速
- function oTimes(step)
- {
- if(step>0 && Times>30)
- Times -= step;
- if(step<0 && Times<400)
- Times -= step;
- document.getElementById(’speed’).innerHTML = Times;
- //If(Times > 5) setTimeout(’oTimes()’, SpeedUp);
- }
- //绑定键盘事件
- document.onkeydown = KeyDown;
- //按键
- function KeyDown(e)
- {
- if(e)
- Key = e.keyCode;
- else
- Key = window.event.keyCode
- switch(Key)
- {
- case 37: //左方向键
- Dir(-1,0);
- break
- case 39: //右方向键
- Dir(1,0);
- break
- case 38: //上方向键
- Dir(0,-1);
- break
- case 40: //下方向键
- Dir(0,1);
- break
- case 33: //PageUp - speed up
- oTimes(5);
- break;
- case 34: //PageDown - speed down
- oTimes(-5);
- break;
- case 66: // B - Show/Hidden Background
- ShowBackground = !ShowBackground;
- if(ShowBackground)
- {
- MainMap.style.backgroundImage = “url(’images/bgpic.jpg’)”;
- //MainMap.style.backgroundRepeat=”repeat-x repeat-y”;
- //MainMap.style.backgroundRepeat=”no-repeat”;
- }
- else
- MainMap.style.backgroundImage = “url(”)”;
- break;
- case 71: // G - Show/Hidden Grid
- ShowGrid = !ShowGrid;
- if(ShowGrid)
- MainMap.appendChild(CreateGrid(’ ‘));
- else
- MainMap.removeChild(document.getElementById(’grid’));
- break;
- case 83: // s - stop
- clearTimeout(moving);
- Start = 0;
- break;
- }
- return false
- }
- function Dir(x,y)
- {
- GoX = x;
- GoY = y;
- if( Start == 0 )
- {
- LastX = x;
- LastY = y;
- clearTimeout(moving);
- Start = 1;
- Move();
- }
- else
- {
- /* 增加的反方向判断 */
- //检查上次行动方向和本次行动方向,如果方向正好相反那么还是保持上次的前进方向
- if (GoX + LastX == 0 && GoY + LastY == 0)
- {
- GoX = LastX;
- GoY = LastY;
- }
- else
- { //如果不一样则记住本次的行动方向
- LastX = GoX;
- LastY = GoY;
- }
- }
- }
- //绘制地图
- CreateMap();
- document.getElementById(’speed’).innerHTML = Times;
- </script>
- </BODY>
- </HTML>