JavaScript贪食蛇游戏制作详解
之前闲时开发过一个简单的网页版贪食蛇游戏程序,现在把程序的实现思路写下来,供有兴趣同学参考阅读。
代码的实现比较简单,整个程序由三个类,一组常量和一些游戏逻辑以外的初始化和控制代码组成,总共400多行JavaScript。
游戏中的三个类分别是「组成蛇身体的节点」「蛇」「贪食蛇游戏」的抽象,常量用来表示游戏中的各种状态。
先从常量讲起
var TRANSVERSE = 30;
var VERTICAL = 40; var LEFT = 1;
var RIGHT = 2;
var TOP = 3;
var BOTTOM = 4; var GAME_START = 1;
var GAME_STOP = 2;
var GAME_OVER = 3
首先,可以把游戏的逻辑想象成一个不断变换的数据结构,把游戏的界面想象成由一组像素格子组成的长方形,界面渲染程序定时读取游戏数据结构,将数据结构中不同的值表示成不同的颜色并画在游戏界面上。
因此,常量TRANSVERSE和VERTICAL分别代表游戏数据结构的最大边界,也就是游戏界面横向和纵向的像素点个数。
常量LEFT、RIGHT、TOP、BOTTOM分别代表贪食蛇上下左右的走向
常量GAME_START、GAME_STOP、GAME_OVER代表游戏的三个状态,游戏进行中、游戏暂停中、游戏结束
游戏中的三个类是游戏的逻辑实现,相对复杂
贪食蛇蛇身由一系列相互引用的节点组成,是一个链表结构,如下图

每一个节点是SnakeNode类的一个实例
//组成蛇的节点,一个链表结构
var SnakeNode = function(point) {
var prevDirection, currDirection,
next,
pos = point; //获得下一个
this.getNext = function() {
return next;
} //设置下一个
this.setNext = function(el) {
next = el;
} //设置方向
this.setDirection = function(value) {
currDirection = value;
} //获得方向
this.getDirection = function() {
return currDirection;
} //计算结点下一个位置
this.computePosition = function() {
pos = SnakeNode.getNextPoint( pos, currDirection );
if( next ) {
next.computePosition();
}
if( prevDirection != currDirection ) {
prevDirection = currDirection;
if( next ){
next.setDirection(currDirection);
}
}
} //获得位置
this.getPosition = function(){
return pos;
}
} //通过方向计算相对与当前位置的下一个位置
SnakeNode.getNextPoint = function (point, direction)
{
var newPoint = {};
switch(direction)
{
case LEFT:
newPoint.x = point.x - 1;
newPoint.y = point.y ;
break;
case RIGHT:
newPoint.x = point.x + 1;
newPoint.y = point.y;
break;
case TOP:
newPoint.x = point.x;
newPoint.y = point.y - 1;
break;
case BOTTOM:
newPoint.x = point.x;
newPoint.y = point.y + 1;
break;
}
return newPoint;
}
蛇身节点有四个属性
prevDirection 上一次移动时的蛇身走向
currDirection 当前蛇身走向
next 节点的下一个节点
pos 节点的位置
六个方法
getNext 获得节点的下一个节点
setNext 设置节点的下一个节点
setDirection 设置节点的方向
getDirection 获得节点的方向
computePosition 计算节点移动后的目标位置
getPosition 获得节点的位置
SnakeNode.getNextPoint 这个方法是一个静态方法, 不属于节点实例, 它的功能是根据方向计算出某一个坐标的下一个坐标, 比如说10和10是某个节点当前的坐标, 那么它向左移动一个单位后坐标就是9和10;向右移动一个单位后坐标就是11和10,同理向上和向下坐标分别是10,9和10,11。
computePosition需要特点说明一下,它在计算出自身移动后的目标位置以后,还会调用它引用的下一个节点的 computePosition方法,然后下一个节点再次执行相同的操作,一直到蛇身的最后一个节点为止,这就是链表的特性。同时如果方向发了变化,这个方法还会把当前节点的方向同步给它引用的下一个节点,就是靠这一点, 蛇身每一个节点的走向才能一致。
通过这一系列属性和方法就能表示出蛇身的节点特性了。
类Snake是整条蛇的抽象表示,代码如下
//蛇
var Snake = function( head ) {
var snake = head;
var isGameover = false;
var self = this; //为蛇增加一个节点
this.addNode = function() {
var lastNode = getLastNode();
var point = lastNode.getPosition();
var reverse;
switch(lastNode.getDirection()) {
case LEFT:
reverse = RIGHT;
break;
case RIGHT:
reverse = LEFT;
break;
case TOP:
reverse = BOTTOM;
break;
case BOTTOM:
reverse = TOP;
break;
}
var newPoint = SnakeNode.getNextPoint(point, reverse);
var node = new SnakeNode(newPoint);
node.setDirection(lastNode.getDirection());
lastNode.setNext(node);
} //获所所有蛇节点的位置
this.getAllNodePos = function() {
var posList = new Array;
var node = snake;
do{
posList.push(node.getPosition());
node = node.getNext();
}while(node);
return posList;
} //获得蛇长度
this.getLength = function() {
var count = 0;
var node = snake;
while(node) {
count ++;
node = node.getNext();
}
return count;
} //游戏是否结束
this.isGameover = function() {
return isGameover;
}
//移动
this.move = function() {
if (!isGameover) {
snake.computePosition();
}
checkGameover();
}
//根据方向导航
this.setDirection = function (direction) {
if( !isGameover ) snake.setDirection(direction);
}
//获得蛇头位置
this.getHeadPos = function() {
return snake.getPosition();
}
//获得蛇头方向
this.getHeadDirection = function() {
return snake.getDirection();
}
var checkGameover = function() {
var l = snake.getPosition();
var cl = self.getAllNodePos();
if(l.x < 0 || l.x >= TRANSVERSE || l.y < 0 || l.y >= VERTICAL ) {
isGameover = true;
return;
}
for(var i = 0 ; i < cl.length ; i ++) {
if(l != cl[i] && cl[i].x == l.x && cl[i].y == l.y) {
isGameover = true;
return;
}
}
} var getLastNode = function() {
var node = snake.getNext();
while( node ){
var nextNode = node.getNext();
if(!nextNode) return node;
node = nextNode;
}
return snake;
}
}
这个类有三个属性
snake是蛇的脑袋节点,因为是一个链表,所以通过蛇的脑袋就可以访问到蛇的尾巴,因此,蛇的脑袋就可以表示一条蛇了。
isGameover游戏是否结束
self是实例自身的引用,跟游戏逻辑的表示没有任何关系。
八个公有方法
addNode 给蛇身增加一个结点,当蛇吃到食物时会调用这个方法,这个方法会把新的节点追加到最后一个节点(蛇尾)的后面。其中局部变量reverse是用来计算新节点的位置用的,假如当前节点的方向是向右的,那么下一个节点肯定在当前节点的左边,以此类推, reverse变量就是当前节点相反方向的值,细节请结合代码理解。
getAllNodePos 获得蛇身所有节点的位置。
getLength 获得蛇身长度(蛇身节点个数)
isGameover 游戏是否结束
move 移动蛇身,调用一次整个蛇身便移动一下,这里的移动仅仅是数据结构变化,具体效果需要将数据结构结果渲染至页面。
setDirection 设置蛇的游动方向
getHeadPos 获得蛇身的第一个节点(蛇头)的位置
getHeadDirection 获得蛇(蛇头)游动的方向
二个私有方法
checkGameover 检查游戏是否结束,分别检测游戏的第一个节点是否落在 TRANSVERSE和VERTICAL常量定义的范围之外(撞墙)和是否落在蛇身节点的位置之上(咬到自己)。
getLastNode 获得蛇身的最后一个结果
通过SnakeNode和Snake这两个类,便抽象出了贪食蛇的结构和特性,但是现在这条蛇只是一个逻辑结构,是不会动的, 更不能玩。接下来我们便让这条蛇游动起来, 还可以控制它的方向, 让它去觅食并越长越长越游越快。
//贪食蛇游戏
var SnakeGame = function() {
var snake ;
var moveTimer, randomTimer;
var currDirection;
var foods = [];
var status = GAME_STOP;
var context; var self = this; this.onEatOne = function(){}; var getRandom = function(notin) {
var avaiable = [];
for(var y = 0 ; y < VERTICAL ; y ++)
{
for(var x = 0 ; x < TRANSVERSE; x++ ) {
var j = 0;
var avaiableFlag = true;
while( j < notin.length ){
var el = notin[j];
if( el.x == x && el.y == y ) {
notin.splice(j,1);
avaiableFlag = false;
break;
}
j++;
}
if(avaiableFlag) avaiable.push({ x: x , y: y });
}
}
var rand = Math.floor(Math.random() * avaiable.length);
return avaiable[rand];
} //导航
var navigate = function(direction) {
var sd = snake.getHeadDirection();
var d ;
if((sd == LEFT || sd == RIGHT) && (direction == TOP || direction == BOTTOM)) d = direction;
if((sd == TOP || sd == BOTTOM) && (direction == LEFT || direction == RIGHT)) d = direction;
if(d) currDirection = d;
} var move = function() {
moveTimer = window.setTimeout( move, computeMoveInterval() );
if(currDirection) snake.setDirection( currDirection );
snake.move();
var lc = snake.getHeadPos();
for(var i = 0 ; i < foods.length ; i ++) {
if(lc.x == foods[i].x && lc.y == foods[i].y) {
snake.addNode();
self.onEatOne();
foods.splice( i, 1 );
break;
}
}
if(snake.isGameover()){
gameover();
return;
}
draw();
} var createFood = function() {
var notin = snake.getAllNodePos().concat(foods);
var rand = getRandom(notin);
foods.push(rand);
} var arrayToMap = function(array) {
var map = {};
for(var i = 0 , point ; point = array[i++];) map[[point.x , point.y]] = null;
return map;
} //获得当前游戏数据结构
var getMap = function() {
var board = new Array;
for (var y = 0 ; y < VERTICAL; y++) {
for (var x = 0 ; x < TRANSVERSE ; x++) {
board.push({ x: x, y: y });
}
}
var cl = snake.getAllNodePos();
var food = arrayToMap(foods);
cl = arrayToMap(cl);
board = arrayToMap(board);
for(var key in cl) board[key] = 'snake';
for(var key in food) board[key] = 'food';
return board;
} //获得分数
this.getScore = function() {
return snake.getLength() - 1;
} //获得级别
this.getLevel = function() {
var score = self.getScore();
var level = 0;
if(score <= 5) level = 1;
else if(score <= 12) level = 2;
else if(score <= 22) level = 3;
else if(score <= 35) level = 4;
else if(score <= 50) level = 5;
else if(score <= 75) level = 6;
else if(score <= 90) level = 7;
else if(score <= 100) level = 8;
else level = 9;
return level;
} var computeMoveInterval = function() {
var speed = {
'1':200,
'2':160,
'3':120,
'4':100,
'5':80,
'6':60,
'7':40,
'8':20,
'9':10
}
var level = self.getLevel();
return speed[level];
} var gameover = function () {
status = GAME_OVER;
window.clearTimeout(moveTimer);
window.clearInterval(foodTimer);
unBindEvent();
alert('游戏结束');
} //获得游戏状态
this.gameState = function () {
return status;
} //游戏开始
this.start = function() {
status = GAME_START;
moveTimer = window.setTimeout(move , computeMoveInterval());
foodTimer = window.setInterval(createFood, 5000);
bindEvent();
} //暂停游戏
this.stop = function() {
status = GAME_STOP;
window.clearTimeout(moveTimer);
window.clearInterval(foodTimer);
unBindEvent();
} this.initialize = function( canvasId ) {
var head = new SnakeNode({ x: Math.ceil(TRANSVERSE / 2), y: Math.ceil(VERTICAL / 2) });
head.setDirection([LEFT, RIGHT , TOP , BOTTOM][Math.floor(Math.random() * 4)])
snake = new Snake(head); var canvas = document.getElementById(canvasId);
context = canvas.getContext('2d');
} //画界面
var draw = function () {
context.fillStyle = '#fff';
context.fillRect(0, 0, 300, 400);
var map = getMap();
for (var key in map) {
var pointType = map[key];
var x = key.split(',')[0];
var y = key.split(',')[1]; if (pointType == 'snake') {
context.fillStyle = '#000';
} else if (pointType == 'food') {
context.fillStyle = '#f00';
} else {
continue;
}
context.fillRect( x * 10, y * 10, 10, 10 );
}
} //绑定事件
var bindEvent = function () {
document.body.onkeydown = function (e) {
e = e || window.event;
var keyCode = e.keyCode;
switch (keyCode) {
case 37:
navigate(LEFT);
break;
case 38:
navigate(TOP);
break;
case 39:
navigate(RIGHT);
break;
case 40:
navigate(BOTTOM);
break;
}
}
} //取消绑定
var unBindEvent = function () {
document.body.onkeydown = null;
}
}
SnakeGame类算不上某一种结构抽象, 它仅仅是一组功能的封装, 其中包括人机交互事件、将数据结构转换成界面和一系列组成游戏的功能。此类比较复杂,就不以讲解之前两个类的方法讲解了。我们从类的实例化为入口开始讲解,然后再逐步扩展至类中的其它方法和属性。
var game = new SnakeGame();
实例化对象,调用构造函数后,类的几个属性被声明或初始化。
var snake ;
var moveTimer, randomTimer;
var currDirection;
var foods = [];
var status = GAME_STOP;
var context; var self = this; this.onEatOne = function(){};
snake 也就是Snake类的实例
moveTimer 使蛇身运动的setTimeout函数的返回值, clearTimeout此值后,表示游戏暂停
randomTimer 随机产生食物的setInterval函数的返回值,clearInterval后停止生成食物,表示游戏暂停
foods 食物,因为会有多个食物产生,因为初始化为数组来存放食物
status 游戏状态,初始化状态为暂停中
context 游戏界面的canvas对象
self 没有表示实例自身, 跟游戏不相关
onEatOne 并不是属性, 而是游戏的一个事件, 当蛇吃到食物时, 此函数(事件)会被调用以用来通知监听者
game.initialize("snake");
初始化游戏,initialize方法的参数是游戏界面的canvas的元素ID,这个方法的细节如下
this.initialize = function( canvasId ) {
var head = new SnakeNode({ x: Math.ceil(TRANSVERSE / 2), y: Math.ceil(VERTICAL / 2) });
head.setDirection([LEFT, RIGHT , TOP , BOTTOM][Math.floor(Math.random() * 4)])
snake = new Snake(head);
var canvas = document.getElementById(canvasId);
context = canvas.getContext('2d');
}
执行的操作分别是
实例化蛇的第一个节点,事实上刚开始也只有一个节点,位置设置在界面的中间。
随机生成一个方向并设置
实例化Snake类,以head(第一个节点)作为构造函数参数
引用canvas,获取canvas的context对象
至此,游戏已经初始化完成,然而,此刻的游戏是静止的,我们还需要调用start方法让游戏开始
this.start = function() {
status = GAME_START;
moveTimer = window.setTimeout(move , computeMoveInterval());
foodTimer = window.setInterval(createFood, 5000);
bindEvent();
}
此方法执行的操作分别是
将游戏的状态设置成 GAME_START常量的值(表示游戏开始)
让蛇身持续移动
每5秒生成一个食物
绑定交互事件,也就是我们用键盘的方向键上下左右控制蛇游动的方向的事件
先看被setTimeout调用的move方法
var move = function() {
moveTimer = window.setTimeout( move, computeMoveInterval() );
if(currDirection) snake.setDirection( currDirection );
snake.move();
var lc = snake.getHeadPos();
for(var i = 0 ; i < foods.length ; i ++) {
if(lc.x == foods[i].x && lc.y == foods[i].y) {
snake.addNode();
self.onEatOne();
foods.splice( i, 1 );
break;
}
}
if(snake.isGameover()){
gameover();
return;
}
draw();
}
方法里面还有一次setTimeout调用,起的到作用和setInterval相同
设置蛇游动的方向
调用蛇的move方法移动
获得蛇头的位置,检查它是否与物品的位置重叠,假如重叠那么表示蛇吃到了食物,因为会调用蛇的addNode方法为蛇增加一个结点,并且触发onEatOne事件用来通知外部的事件监听,再将初吃掉的食物从食物列表中拿掉
判断游戏是否结束,假如没结束那么就执行draw方法将数据结果渲染至游戏界面
再来看 computeMoveInterval 方法,这个方法是setTimeout的第二个参数,在这里表达的意思就是定时执行move方法的时间间隔。
var computeMoveInterval = function() {
var speed = {
'1':200,
'2':160,
'3':120,
'4':100,
'5':80,
'6':60,
'7':40,
'8':20,
'9':10
}
var level = self.getLevel();
return speed[level];
}
随着游戏的进行,游戏的级别会增加,随着级别增加, 这个值越小, 也就是说move方法被执行的频率就越高,因此蛇游动的速度会越快, 游戏难度也就越大。
createFood每5秒被调用一次生成一个食物
var createFood = function() {
var notin = snake.getAllNodePos().concat(foods);
var rand = getRandom(notin);
foods.push(rand);
}
蛇身体所占的位置和已有食物的位置被排除掉,显然食物不能生成在已被占用的位置上。
最后,我们来讲一下draw方法,它的作用是将游戏的数据结构转换为可视化界面
var draw = function () {
context.fillStyle = '#fff';
context.fillRect(0, 0, 300, 400);
var map = getMap();
for (var key in map) {
var pointType = map[key];
var x = key.split(',')[0];
var y = key.split(',')[1];
if (pointType == 'snake') {
context.fillStyle = '#000';
} else if (pointType == 'food') {
context.fillStyle = '#f00';
} else {
continue;
}
context.fillRect( x * 10, y * 10, 10, 10 );
}
}
将游戏结构转换成draw方法可用的数据结构还需要调用两个方法,分别是getMap和arrayToMap
var arrayToMap = function(array) {
var map = {};
for(var i = 0 , point ; point = array[i++];) map[[point.x , point.y]] = null;
return map;
}
var getMap = function() {
var board = new Array;
for (var y = 0 ; y < VERTICAL; y++) {
for (var x = 0 ; x < TRANSVERSE ; x++) {
board.push({ x: x, y: y });
}
}
var cl = snake.getAllNodePos();
var food = arrayToMap(foods);
cl = arrayToMap(cl);
board = arrayToMap(board);
for(var key in cl) board[key] = 'snake';
for(var key in food) board[key] = 'food';
return board;
}
arrayToMap的作用其实是将一个一维数组转换为二维数组(并不是真正的二维数组,但是为了方便表达就借用二维数组这种结构),只是JavaScript的二维数组表示的有点奇葩,是一个map,所以这个函数的名称就被命名为arrayToMap
getMap函数的逻辑如下
建一个二维数组,元素个数等于TRANSVERSE * VERTICAL
获取蛇身所占的位置列表,转换成二维数组
获得食物所占的位置列表,转换成二维数组
通过null、snake、food三种值区分空、蛇身节点、食物
最终的数组结构从可视的角度来表示大概是这个样子
[null,null,null,null,null,
null,null,null,food,null,
null,null,null,null,null,
null,null,food,null,null,
null,null,snake,snake,null,
null,null,snake,null,null]
这个结构会随着move方法的调用而不断变化, draw方法就不断的将数据结构渲染至canvas上,整条蛇因此也就动了起来。
最后我们来看bindEvent方法
var bindEvent = function () {
document.body.onkeydown = function (e) {
e = e || window.event;
var keyCode = e.keyCode;
switch (keyCode) {
case 37:
navigate(LEFT);
break;
case 38:
navigate(TOP);
break;
case 39:
navigate(RIGHT);
break;
case 40:
navigate(BOTTOM);
break;
}
}
}
这个方法很简单,就是用来监听方向键的事件,然后控制蛇的方向以达到操作游戏的效果。
至此,整个游戏的逻辑也就开发完成了。麻雀虽小,但五脏俱全,这个游戏玩法虽然很少,但确实是一个正儿八经的贪食蛇游戏。附上可运行的源代码的链接地址
http://pan.baidu.com/s/1o7VIcWy
就一个html文件
游戏是我多年前写的,代码略显青涩,函数和变量的命名也是词不达意,但大致意思能表达清楚,大家就将就着看吧。
JavaScript贪食蛇游戏制作详解的更多相关文章
- javascript中=、==、===区别详解
javascript中=.==.===区别详解今天在项目开发过中发现在一个小问题.在判断n==""结果当n=0时 n==""结果也返回了true.虽然是个小问题 ...
- Javascript 调试利器 Firebug使用详解
Javascript 调试利器 Firebug使用详解 有时候,为了更清楚方便的查看输出信息,我们可能需要将一些调试信息进行分组输出,那么可以使用console.group来对信息进行分组,在组信息输 ...
- JavaScript中return的用法详解
JavaScript中return的用法详解 最近,跟身边学前端的朋友了解,有很多人对函数中的this的用法和指向问题比较模糊,这里写一篇博客跟大家一起探讨一下this的用法和指向性问题. 1定义 t ...
- javascript 中合并排序算法 详解
javascript 中合并排序算法 详解 我会通过程序的执行过程来给大家合并排序是如何排序的... 合并排序代码如下: <script type="text/javascript& ...
- javascript中的this作用域详解
javascript中的this作用域详解 Javascript中this的指向一直是困扰我很久的问题,在使用中出错的机率也非常大.在面向对象语言中,它代表了当前对象的一个引用,而在js中却经常让我觉 ...
- JavaScript中this的用法详解
JavaScript中this的用法详解 最近,跟身边学前端的朋友了解,有很多人对函数中的this的用法和指向问题比较模糊,这里写一篇博客跟大家一起探讨一下this的用法和指向性问题. 1定义 thi ...
- 高性能JavaScript模板引擎实现原理详解
这篇文章主要介绍了JavaScript模板引擎实现原理详解,本文着重讲解artTemplate模板的实现原理,它采用预编译方式让性能有了质的飞跃,是其它知名模板引擎的25.32 倍,需要的朋友可以参考 ...
- JavaScript对象的property属性详解
JavaScript对象的property属性详解:https://www.jb51.net/article/48594.htm JS原型与原型链终极详解_proto_.prototype及const ...
- javascript常用经典算法实例详解
javascript常用经典算法实例详解 这篇文章主要介绍了javascript常用算法,结合实例形式较为详细的分析总结了JavaScript中常见的各种排序算法以及堆.栈.链表等数据结构的相关实现与 ...
随机推荐
- UUID错误
在Archive项目时,出现了“Your build settings specify a provisioning profile with the UUID “”, however, no suc ...
- Js 跨域CORS报错 Response for preflight has invalid HTTP status code 405
问题 公司项目H5调用接口遇到Response for preflight has invalid HTTP status code 405这样的错误,是使用PUT方式提交请求接口.Content-T ...
- JS事件流理解
事件是用户或浏览器自身执行的某种动作,如click,load和mouseover都是事件的名字. 事件是javaScript和DOM之间的桥梁. 你若触发,我便执行--事件发生,调用它的处理函数执行相 ...
- 安装msdn出现的问题及解决
安装msdn出现的问题及解决 用xx.iso 镜象文件安装 运行第一个镜象文件的setup.exe安装到一部分提示:安装程序无法打开文件 C:\Documents and Settings\empty ...
- 继续学习ant
今天由于打电话,打了两个小时的电话,结果一下子错过了学习的时间段,表示很惭愧,不过查了一些资料,感觉还不错,明天继续学习吧! ant入门到精通Ant 的最完整build.xml解释ant实用实例Ant ...
- 【WC2015】混淆与破解 (Goldreich-Levin 算法)
这个嘛= =直接贴VFK的题解就行了吧,感觉自己还是差别人太多 http://vfleaking.blog.uoj.ac/blog/104 讲得挺明白了的说,但还是挺难理解的说,中间实现部分简直不要太 ...
- 零基础HTML编码学习笔记
任务目的 了解HTML的定义.概念.发展简史 掌握常用HTML标签的含义.用法 能够基于设计稿来合理规划HTML文档结构 理解语义化,合理地使用HTML标签来构建页面 任务描述:完成一个HTML页面代 ...
- java学习笔记——IO部分(遍历文件夹)
用File类写的一个简单的工具,遍历文件夹,获取该文件夹下的所以文件(含子目录下的文件)和文件大小: /** * 列出指定目录下(包含其子目录)的所有文件 * @author syskey * */ ...
- mongodb c api编译
1. autoconf-latest.tar.gz http://ftp.gnu.org/gnu/autoconf/ tar xzvf autoconf-latest.tar.gz ./configu ...
- Node.js~在linux上的部署~外网不能访问node.js网站的解决方法
这是上一篇node.js部署到linux上的后续文章,当我们安装完node.js之后,建立了sailsjs的网站,然后在外面电脑上无法访问这个网站,这个问题我们如何去解决? 解决思路: 查看linux ...