JavaScript实现贪吃蛇小游戏

骑驴找蚂蚁 · 2017-09-26

贪吃蛇是终多小游戏中的经典游戏了。在以前的非智能手机时代一般的手机都会有这个游戏。本篇教程是通过javascript纯div形式来实现这个小游戏而非以html5canvas实现。

snake.git

实现分析

需要一个网格场景来支持”蛇”的移动。比如我们的网格是一个15*10的矩阵(网格数: 150)。”蛇”在移动的时候变化的只有”蛇”整个身体的第一格和最后一格(可以将"蛇"定义为一个数组,数组的变化永远只有尾和头)。”蛇”的死亡是在移动到边界的下一次移动超出边界和移动到自己身体上整个游戏就结束了。”蛇”的移动方向需要捕获用户按下方向键的事件来控制移动方向。

snake-before-move-after.png

通过分析我们需要一个场景类,snake类,控制类,point类,事件处理函数。

场景类

场景类的主要功能就是创建一个场景,在场景内绘制自定义大小的网格。场景关链snake类,在创建场景的时候也同时创建了snake


    var Frame = function(options) {
        var defaults = {
                width: 900, // 场景过度
                height: 500, //场景高度
                grid: 30, //网格大小
                snake: {
                    direct: 'right' //默认移动方向
                },
                point: {}
            };
        this.options = _.merge(defaults, options);
        var wh = this.options.grid,
            x = Math.floor(this.options.width / (wh + 2)),
            y = Math.floor(this.options.height / (wh + 2));
        this.options.x = x;
        this.options.y = y;
        this.snake = null;
        this.frame = null; //主场景元素
    }
    Frame.prototype = {
        //创建场景,同时将网格填充致场景
        create: function() {
            var element = this.frame = document.createElement('div');
            element.style.margin = "50px auto";
            element.style.height = this.options.height + "px";
            element.style.width  = this.options.width + "px";
            element.style.position = "relative";
            var grids = this.createGrid();
            for(var i in grids) {
                element.appendChild(grids[i]);
            }
            document.body.appendChild(element);
            var common = {
                x: this.options.x,y: this.options.y, size: this.options.grid
            };
            this.snake = new Snake(
                _.merge(common, this.options.snake),
                new Point(_.merge(common, this.options.point))
            );
        },
        //创建网格矩阵
        createGrid: function() {
            var x = this.options.x,
                y = this.options.y,
                wh = this.options.grid,
                grids = [];
            for (var i = 1; i <= y; i++) {
                for (var j = 1; j <= x; j++) {
                    var element = document.createElement('div');
                    element.style.width = wh + "px";
                    element.style.height = wh+ "px";
                    //设置网格ID,为1 --- x * y
                    element.setAttribute('id', "grid_" + (i * x - x + j));
                    element.style.border = "1px dotted #ddd";
                    if (i !== 1) {
                        element.style.borderTop = "none";
                    }
                    if (j !== 1) {
                        element.style.borderLeft = "none";
                    }
                    element.style.float = "left";
                    grids.push(element);
                }
            }
            return grids;
        }
    };

Snake类

Snake类的主要功能是snake的移动、吃、方向、重绘操作,本类需要关联Point类。在移动的时候需要判断有没有吃的操作和游戏结束事件的触发。


var Snake = function(options, point) {
        var defaults = {
                grid: 30,
                color: "green",
                x: 0,
                y: 0,
                direct: 'left',
                sourceColor: '#FFF'
            },
            length = 1;
        this.options = _.merge(defaults, options);
        var gridSize = this.options.x * this.options.y,
            snakeLocus = [],
            endLocus = null,
            direct = this.options.direct,
            angle = {"left": 90, "right": 270, "up": 0, "down": 180};
        var create = function() {
            var start = gridSize / 2;
            snakeLocus.push(start);
        };

        this.setDirection = function(value) {
            if (Math.abs(angle[direct] - angle[value]) === 180 && length > 1) {
                return ;
            }
            direct = value;
        };
        this.getDirection = function() {
            return direct;
        };
        this.getSnakeLocus = function() {
            return snakeLocus;
        };
        this.push = function(value) {
            snakeLocus.push(value);
        };
        this.pop = function() {
            endLocus = snakeLocus.pop();
        };
        this.unshift = function(value) {
            snakeLocus.unshift(value);
        };
        this.getEndLocus = function() {
            return endLocus;
        };
        this.getPoint = function() {
            return point;
        };
        this.length = function() {
            return length;
        };
        this.incremnt = function() {
            length++;
        };
        create.call(this);
        this.create();
    };

    Snake.prototype = {
        //重绘函数
        create: function() {
            var locus = this.getSnakeLocus(),
                endLocus = this.getEndLocus();
                point  = this.getPoint();
            for (var i in locus) {
                document.getElementById('grid_' + locus[i]).style.backgroundColor = this.options.color;
            }
            if (point.getPosition() === locus[0]) {
                this.eat(endLocus);
            } else {
                if (endLocus) {
                    document.getElementById('grid_' + endLocus).style.backgroundColor = this.options.sourceColor;
                }
            }
        },
        //移动事件
        move: function() {
            var locus = this.getSnakeLocus(),
                direct = this.getDirection(),
                first = locus[0],
                isOver = false;
            switch (direct) {
                case 'left':
                    first -= 1;
                    if (first % this.options.x === 0) isOver = true;
                    break;
                case 'up':
                    first -= this.options.x;
                    if (first < 0) isOver = true;
                    break;
                case 'right':
                    first += 1;
                    if (first % this.options.x === 1) isOver = true;
                    break;
                case 'down':
                    first += this.options.x;
                    if (first > (this.options.x * this.options.y)) isOver = true;
                    break;
            }
            //吃到自己
            if (_.inArray(locus, first)) {
                isOver = true;
            }
            if (isOver) {
                Events.trigger('over', this);
            } else {
                this.pop();
                this.unshift(first);
                this.create();
            }
        },
        //吃事件
        eat: function(end) {
            this.push(end);
            this.getPoint().random(this.getSnakeLocus());
            this.incremnt();
        }
    };

Note:
设置方向的时候,不能设置当前方向相反的方向。在随机point的时候不能是当前sanke所在的网格内。在吃到自己和移动网格之外说明游戏结束。snake第一个网格的位位置和point的position位置重合触发吃的事件。

Point类

Point类主要实现随机分布一个点到网格中,不可以分布出snake内的点。


var Point = function(options) {
        var defaults = {
            color: "red",
            size: 0,
            x: 0,
            y: 0
        };
        this.options = _.merge(defaults, options);
        var gridSize = this.options.x * this.options.y,
            position = 0;
        this.setPosition = function (outer) {
            position = Math.floor(Math.random() * gridSize + 1);
            if (outer.length > 0) {
                if (_.inArray(outer, position)) {
                    this.setPosition(outer);
                }
            }
        };
        this.getPosition = function() {
            return position;
        };
        this.random([]);
    };
    Point.prototype = {
        random: function(outer) {
            this.setPosition(outer);
            var position = this.getPosition();
            var grid = document.getElementById("grid_" + position);
            grid.style.backgroundColor = this.options.color;
        }
    }

Control类

Control类主要实现事件的绑定,snake移动定时器,游戏的暂停和开始。


var Control = function(options) {
        var defaults = {
                time: 500
            },
            frame = new Frame(),
            clock = null,
            space = 1,
            _this = this;
        this.move = function() {
            clock = setInterval(
                function() {
                    frame.snake.move.call(frame.snake);
                },
                defaults.time
            );
        };
        Events.on("keyup", function(e) {
            switch (e.which) {
                //移动的方向绑定
                case 37:
                    frame.snake.setDirection('left');
                    break;
                case 38:
                    frame.snake.setDirection('up');
                    break;
                case 39:
                    frame.snake.setDirection('right');
                    break;
                case 40:
                    frame.snake.setDirection('down');
                    break;
                    //暂停
                case 19:
                case 32:
                    Events.trigger('stop', _this);
                    break;

            }
        });
        var over = function() {
            var gameOver = document.createElement('div');
            gameOver.innerHTML = "Game Over";
            gameOver.style.font = "normal bold 100px source code pro,arial,sans-serif";
            gameOver.style.color = "red";
            gameOver.style.width = "100%";
            gameOver.style.textAlign = "center";
            gameOver.style.position = "absolute";
            gameOver.style.top = "80px";
            var restart = document.createElement('a');
            restart.setAttribute('href', "javascript:;");
            restart.text = "重新开始";
            restart.style.display = "block";
            restart.style.fontSize = "50px";
            restart.style.color = "#f4645f";
            Events.on('click', function() {
                gameOver.remove();
            }, restart);
            gameOver.appendChild(restart);
            frame.frame.appendChild(gameOver);
        };
        //游戏结束触发事件
        Events.on('over', function() {
            clearInterval(clock);
            over();
        });
        //游戏结束触发事件
        Events.on('stop', function() {
            if (space % 2 === 0) {
                this.move();
            } else {
                clearInterval(clock);
            }
            space++;
        });
        frame.create();
    };
    Control.prototype = {
        start: function() {
            this.move();
        }
    };

Note:
space变量是为了控制暂停和开始的,偶数开始奇数与之相反。

辅助函数


var _ = {
        merge: function(source, child) {
            for (var key in child) {
                source[key] = child[key];
            }
            return source;
        },
        extend: function(source, parent, pro) {

        },
        inArray: function(array, position) {
            if (array.includes) {
                return array.includes(position);
            }
            if (array.indexOf(position) >= 0) {
                return true;
            }
            return false;
        }
    };

    var _listening = [];
    var Events = {
        on: function(type, callback, element) {
            var ele = element || window;
            var listener = ele.addEventListener || function(type, callback) {
                element.attachEvent("on" + type, callback);
            };
            listener(type, callback);
            _listening.push({'event': type, 'element': element, 'callback': callback});
        },
        remove: function(type, element, callback) {
            var ele = element || window;
            var listener = ele.removeEventListener || ele.detachEvent;
            listener('on' + type, callback);
        },
        trigger: function(type, object, options) {
            for (var i in _listening) {
                if (_listening[i].event === type) {
                    _listening[i].callback.apply(object || window, options || []);
                }
            }
        }
    };

完结

以上代码就是整个游戏的源代码,其实还有几个功能没有去实现。实现起来实际是挺简单的,本篇的代码有很多地方可以优化(比如:可以从网格数组中拿到单个网格对象,而且需要通过getElementById来获取对象), 代码组织也不是很好。代码我们可以慢慢的优化,最主要的是实现思路

源码地址

https://github.com/TianLiangZhou/loocode-example/tree/master/snake

推荐阅读