• 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

    首先,可以把游戏的逻辑想象成一个不断变换的数据结构,把游戏的界面想象成由一组像素格子组成的长方形,界面渲染程序定时读取游戏数据结构,将数据结构中不同的值表示成不同的颜色并画在游戏界面上。

    因此,常量TRANSVERSEVERTICAL分别代表游戏数据结构的最大边界,也就是游戏界面横向和纵向的像素点个数。

    常量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 检查游戏是否结束,分别检测游戏的第一个节点是否落在 TRANSVERSEVERTICAL常量定义的范围之外(撞墙)和是否落在蛇身节点的位置之上(咬到自己)。

    getLastNode 获得蛇身的最后一个结果

    通过SnakeNodeSnake这两个类,便抽象出了贪食蛇的结构和特性,但是现在这条蛇只是一个逻辑结构,是不会动的, 更不能玩。接下来我们便让这条蛇游动起来, 还可以控制它的方向, 让它去觅食并越长越长越游越快。

    //贪食蛇游戏
    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');
        }

    执行的操作分别是

    1. 实例化蛇的第一个节点,事实上刚开始也只有一个节点,位置设置在界面的中间。

    2. 随机生成一个方向并设置

    3. 实例化Snake类,以head(第一个节点)作为构造函数参数

    4. 引用canvas,获取canvascontext对象

    至此,游戏已经初始化完成,然而,此刻的游戏是静止的,我们还需要调用start方法让游戏开始

        this.start = function() {
            status = GAME_START;
            moveTimer = window.setTimeout(move , computeMoveInterval());
            foodTimer = window.setInterval(createFood, 5000);
            bindEvent();
        }

    此方法执行的操作分别是

    1. 将游戏的状态设置成 GAME_START常量的值(表示游戏开始)

    2. 让蛇身持续移动

    3. 每5秒生成一个食物

    4. 绑定交互事件,也就是我们用键盘的方向键上下左右控制蛇游动的方向的事件

    先看被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();
        }
    1. 方法里面还有一次setTimeout调用,起的到作用和setInterval相同

    2. 设置蛇游动的方向

    3. 调用蛇的move方法移动

    4. 获得蛇头的位置,检查它是否与物品的位置重叠,假如重叠那么表示蛇吃到了食物,因为会调用蛇的addNode方法为蛇增加一个结点,并且触发onEatOne事件用来通知外部的事件监听,再将初吃掉的食物从食物列表中拿掉

    5. 判断游戏是否结束,假如没结束那么就执行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方法可用的数据结构还需要调用两个方法,分别是getMaparrayToMap

    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函数的逻辑如下

    1. 建一个二维数组,元素个数等于TRANSVERSE * VERTICAL

    2. 获取蛇身所占的位置列表,转换成二维数组

    3. 获得食物所占的位置列表,转换成二维数组

    4. 通过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文件

    游戏是我多年前写的,代码略显青涩,函数和变量的命名也是词不达意,但大致意思能表达清楚,大家就将就着看吧。

  • 相关阅读:
    SUSE 安装 iServer、iDesktop启动异常问题
    各系统勒索补丁下载地址
    centos 安装atom 笔记
    转载---SuperMap GIS 9D SP1学习视频播单
    SuSE的命令安装软件 zypper
    Leaflet客户端学习笔记
    设置UI控件的Layer属性(边框可见,边框颜色,边框宽度,边框圆角)
    安全清理Xcode 缓存垃圾
    OC变量命名禁忌
    iOS中的应用启动原理
  • 原文地址:https://www.cnblogs.com/aspwebchh/p/6631785.html
Copyright © 2020-2023  润新知