之前闲时开发过一个简单的网页版贪食蛇游戏程序,现在把程序的实现思路写下来,供有兴趣同学参考阅读。
代码的实现比较简单,整个程序由三个类,一组常量和一些游戏逻辑以外的初始化和控制代码组成,总共 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');
- }
执行的操作分别是
至此,游戏已经初始化完成,然而,此刻的游戏是静止的,我们还需要调用 start 方法让游戏开始
- this.start = function() {
- status = GAME_START;
- moveTimer = window.setTimeout(move , computeMoveInterval());
- foodTimer = window.setInterval(createFood, 5000);
- bindEvent();
- }
此方法执行的操作分别是
先看被 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();
- }
再来看 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 函数的逻辑如下
最终的数组结构从可视的角度来表示大概是这个样子
[null,null,null,null,null,
null,null,null,,null,
null,null,null,null,null,
null,null,,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;
- }
- }
- }
这个方法很简单,就是用来监听方向键的事件,然后控制蛇的方向以达到操作游戏的效果。
至此,整个游戏的逻辑也就开发完成了。麻雀虽小,但五脏俱全,这个游戏玩法虽然很少,但确实是一个正儿八经的贪食蛇游戏。附上可运行的源代码的链接地址
就一个 html 文件
游戏是我多年前写的,代码略显青涩,函数和变量的命名也是词不达意,但大致意思能表达清楚,大家就将就着看吧。
本文首发于微信公众号 「带你撸出一手好代码」,欢迎关注获取更多原创分享。
来源: http://www.cnblogs.com/aspwebchh/p/6631785.html