目录

前言

一、效果展示

二、制作结构

三、蛇的创造

四、运动的蛇

1.运动

 2.方向移动

五、吃下食物,越来越长

六、死亡判定

七、越吃越快

八、结尾

HTML:

Game.js:

Food.js:


前言

        虽然在写这个项目很多东西都没有学过,但是根据这个视频的内容我还是跟着把贪吃蛇项目写了出来:web前端实战项目系列,JavaScript面相对象开发贪吃蛇(全套教程),并且一步步搞懂内部的东西。同时感谢我们的大佬的文章:JavaScript贪吃蛇给我带来了很多的帮助。贪吃蛇项目的制作我觉得更多是需要懂得制作的思路,接下来让我们寻找其中的重点,解析这个过程。


一、效果展示

首先让我们看看实际上的效果是什么样子的:贪吃蛇小游戏

这里有一个参考网站给大家看。我这里上一份我制作的贪吃蛇样子:

JavaScript编的贪吃蛇 js写贪吃蛇_javascript


JavaScript编的贪吃蛇 js写贪吃蛇_javascript_02

 我们可以看到,有这么几个重大的功能:

  1. 点击开始游戏
  2. 蛇会出现,而且按照指定方向移动
  3. 吃到食物之后会增长,同时食物会随机出现
  4. 撞到墙壁会死亡,吃到自己身体会死亡(没录进动图里面,但是有这个效果的!)
  5. 可以暂停游戏
  6. 速度会越来越快
  7. 结束之后会刷新页面

 接下来就让我们来分步骤来解说这个贪吃蛇该怎么制作。


二、制作结构

       贪吃蛇可以用很多中方式移动,我们可以使用定位的方式,每次需要移动的时候,就用定位移动一段距离。又或者像我学习视频这样,用表格来制作,制作一个大的表格存储数据。首先我们需要创造一个Game.js作为一个中介者,执行其他的js。之后,我们开始字啊Game.js里面写代码。

我们首先需要准备一个可以初始化数据的函数:

function Game() {}

       把我们的数据写进如,比如this.col =15,this.row=15 ,创建15列,15行这样的方式。之后如果要写需要使用的一些变量作为开关,或者其他数据的初始化,也需要写入这个Game初始化代码里面。

之后我们需要把表格添加到我们的主页面中的盒子里面:

<div id="app" class="app"></div>

       那么我们需要用什么方式添加呢?这里需要提到一个过去的案例,以前做过一个倒三角,正三角的星星排列。那么我们放入表格需要这样的顺序:

  1.  创建了table
  2. 插入tr
  3. 在tr里面插入td

       这个本质和我们插入星星的方式是一样的,也就是在第一行的时候我需要开始插入每一颗星星,第一行插入结束之后,才会进入下一行,这和tr里面插入td本质是一样的。我们来看看插入星星的时候用的代码:

var str = '';
        var num1 = prompt('需要打印几行:');
        var num2 = prompt('需要打印几列:');
        for (var i = 1; i <= num1; i++) {
            for (var j = 1; j <= num2; j++) {
                str = str + '★';
            }
            str = str + '\n';
        }
        console.log(str);

       我们可以看到,用两个for循环去书写,嵌套,只需要在外面的循环插入tr,里面的循环插入td,按照这样的思路,我们的函数就能构写出来:

Game.prototype.init = function() {
    this.dom = document.createElement("table");
    var tr, td;
    //遍历行和列上树
    for (var i = 0; i < this.row; i++) {
        //遍历行,创建结点上树
        tr = document.createElement('tr');
        for (var j = 0; j < this.col; j++) {
            //遍历列,创建结点上树
            td = document.createElement('td');
            //追加到tr
            tr.appendChild(td);
        }
        //追加节点上树
        this.dom.appendChild(tr);
    }
    //表格上树
    document.getElementById('app').appendChild(this.dom);
}

       这里需要注意的是:添加的tr需要td在全部添加到tr里面之后,再去插入,否则tr直接插入,td就没有被插入到tr里面了。最后我们把整个表格添加进去就可以获得这个结构了。可以给这个结构添加CSS样式查看它的样子。

记得要在主页面去调用这个js:

<script>

var game = new Game();

</script>


三、蛇的创造

在js的文件里面,我们再创建一个Snake.js,用来书写蛇的js代码。

我们首先创建一个蛇的对象的数组,在里面塞入我们想要的它出现的位置:

function Snake() {
    // 蛇的初始化身体
    this.body = [
        { "row": 3, "col": 5 },
        { "row": 3, "col": 4 },
        { "row": 3, "col": 3 },
        { "row": 3, "col": 2 }
    ];
}

之后,我们给蛇添加它的头和它的身体颜色。

回到Game.js,书写一个设置颜色,设置头部的代码

// 设置颜色的方法
Game.prototype.setColor = function(row, col, color) {
    // 让表格的第几行第几列设置什么颜色
    this.dom.getElementsByTagName('tr')[row].getElementsByTagName('td')[col].style.background = color;
};
Game.prototype.setHead = function(row, col) {
    var img = document.createElement('img');
    img.src = 'img/snake.png';
    img.classList.add('head');
    this.dom.getElementsByTagName('tr')[row].getElementsByTagName('td')[col].appendChild(img);
}

之后我们需要去渲染这个蛇的内容,需要在Snake.js里面写入:

Snake.prototype.render = function() {
    // 蛇的渲染
    game.setHead(this.body[0].row, this.body[0].col);
    for (var i = 1; i < this.body.length; i++) {
        game.setColor(this.body[i].row, this.body[i].col, '#9ccc65');
    }
}

        this.body[0].row, this.body[0].col是蛇的头部的位置,剩下的setcol就是根据我们的对象里写入的数组内容依次遍历,在后面染上对应长度的颜色。

这样,射的身体和头部的代码就准备完毕了,之后就是重要的运动内容。


四、运动的蛇

       我们制作运动的蛇,原理是让蛇头运动起来,给td添加样式来显示蛇的样子,之后我们需要在蛇运动之前清除样式,然后改变蛇在表格中的位置,重新再一次渲染这个蛇的样子,来实现每隔一段时间时间,蛇就会朝着一个方向运动的能力。

1.运动

       制作运动的蛇,首先我们需要让他动起来,那么我们就写一个代码,让他的头部可以按照一个方向移动。我们写一个switch的代码来选择方向运动:

Snake.prototype.update = function() {
    switch (this.direction) {
        case 'R':
            // 向右
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col + 1 });
            break;
    }
}

       但是这个时候蛇还是停止的,不会动,那么需要做什么让他动起来呢?我们用定时器,把代码放入定时器中运行:

Game.prototype.start = function() {
var self = this;
self.timer = setInterval(function() {
            // 蛇的渲染
            game.snake.update();
            // 渲染蛇
            game.snake.render();
}, 500);
}

       我们这样设置肯定是不对的,因为我们还缺少一些擦除样式的代码:

Game.prototype.clear = function() {
    //遍历表格,擦除画布
    for (var i = 0; i < this.row; i++) {
        for (var j = 0; j < this.col; j++) {
            this.dom.getElementsByTagName('tr')[i].getElementsByTagName('td')[j].style.background = '';
            this.dom.getElementsByTagName('tr')[i].getElementsByTagName('td')[j].innerHTML = '';
        }
    }
};

       用这样的函数,每次代码执行的时候,先把表格擦除样式,然后再加入蛇的样式,重复这样下来,就可以制作出蛇的样子。但是光是这样还不够,因为我们的蛇会拖着长长的尾巴。这是因为我们的蛇头移动之后,长度会增加一节,二这个动作会导致对象内的数组越装越多。所以我们需要在蛇移动的时候,删除末尾的一节,保持这个蛇身体的长度,那么蛇移动的代码的完整样子就是:

Snake.prototype.update = function() {
    switch (this.direction) {
        case 'R':
            // 向右
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col + 1 });
            break;
    }
this.body.pop();
}

       这样就可以实现朝着一个方向运动的蛇了。来看看完整的代码:

Snake.js:

Snake.prototype.update = function() {
    switch (this.direction) {
        case 'R':
            // 向右
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col + 1 });
            break;
        case 'D':
            // 向下
            this.body.unshift({ "row": this.body[0].row + 1, "col": this.body[0].col });
            break;
        case 'L':
            // 向左
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col - 1 });
            break;
        case 'U':
            // 向上
            this.body.unshift({ "row": this.body[0].row - 1, "col": this.body[0].col });
            break;
    }
        this.body.pop();
};

Snake.prototype.render = function() {
    // 蛇的渲染
    game.setHead(this.body[0].row, this.body[0].col);
    for (var i = 1; i < this.body.length; i++) {
        game.setColor(this.body[i].row, this.body[i].col, '#9ccc65');
    }
}

定时器:

Game.prototype.start = function() {
var self = this;
self.timer = setInterval(function() {
            // 清楚屏幕
            game.clear();
            // 蛇的渲染
            game.snake.update();
            // 渲染蛇
            game.snake.render();
}, 500);
}

 2.方向移动

       之后,我们需要用键盘的数值来控制蛇的方向,这个时候我们需要注意三个点:

  1. 蛇头要跟着转动
  2. 蛇要跟着键盘按键改变方向
  3. 蛇头不会往回走

       首先我们写下按键之后改变方向的代码:

//设置键盘监听事件
Game.prototype.bindEvent = function() {
    var self = this;
    // 键盘事件
    document.onkeydown = function(event) {
        switch (event.keyCode) {
            case 37:
                // 按下←键
                self.snake.direction("L");
                break;
            case 38:
                //按下↑键
                self.snake.direction("U");
                break;
            case 39:
                //按下→键
                self.snake.direction("R");
                break;
            case 40:
                // 按下↓键
                self.snake.direction("D");
                break;
        }
    }
}

       利用时间委托,当我们按下按键的时候,判断按键的keycode数值,然后做出改变数值的效果。然后我们需要加上判断,当我们为左运动,不能按右键这样的逻辑,添加我们的判断条件:

//设置键盘监听事件
Game.prototype.bindEvent = function() {
    var self = this;
    // 键盘事件
    document.onkeydown = function(event) {
        switch (event.keyCode) {
            case 37:
                // 按下←键
                // 先进行判断,如果当前的方向是向右移动,此事我们不能按左键
                if (self.snake.direction == 'R') {
                    return;
                }
                self.snake.direction("L");
                break;
            case 38:
                //按下↑键
                // 先进行判断,如果当前的方向是向下移动,此事我们不能按上键
                if (self.snake.direction == 'D') {
                    return;
                }
                self.snake.direction("U");
                break;
            case 39:
                //按下→键
                // 先进行判断,如果当前的方向是向左移动,此事我们不能按右键
                if (self.snake.direction == 'L') {
                    return;
                }
                self.snake.direction("R");
                break;
            case 40:
                // 按下↓键
                // 先进行判断,如果当前的方向是向上移动,此事我们不能按下键
                if (self.snake.direction == 'U') {
                    return;
                }
                self.snake.direction("D");
                break;
        }
    }
}

       这样我们确实防止了回头的情况,但是按键不是只能按一个,如果我们在更新之前,判定了正确的方向,那么这个时候我们再按回头的方向键,机会导致出现回头的情况,我们必须去避免这种bug。这里有一个方法,就是把正确方向的数据存起来,这个时候我们的蛇还是原来的方向运动,直到更新的时候,才把正确的数值传给蛇。

那么我们就需要写一个传入数据的函数:

// 蛇的方向改变,防止的是在一次渲染之前会出现调头的情况
Snake.prototype.changeDirection = function(d) {
    this.willDirection = d;
}

       然后添加初始化的方向和初始化的正确数值:

function Snake() {
    // 蛇的初始化身体
    this.body = [
        { "row": 3, "col": 5 },
        { "row": 3, "col": 4 },
        { "row": 3, "col": 3 },
        { "row": 3, "col": 2 }
    ];
    // 信息量,设置的运动方向
    this.direction = 'R';
    // 即将改变的方向,目的就是为了防止原地调头的情况
    this.willDirection = 'R';
}

       之后给方向键代码做一些改变:

//设置键盘监听事件
Game.prototype.bindEvent = function() {
    var self = this;
    // 键盘事件
    document.onkeydown = function(event) {
        switch (event.keyCode) {
            case 37:
                // 按下←键
                // 先进行判断,如果当前的方向是向右移动,此事我们不能按左键
                if (self.snake.direction == 'R') {
                    return;
                }
                self.snake.changeDirection("L");
                self.d = 'L';
                break;
            case 38:
                //按下↑键
                // 先进行判断,如果当前的方向是向下移动,此事我们不能按上键
                if (self.snake.direction == 'D') {
                    return;
                }
                self.snake.changeDirection("U");
                self.d = 'U';
                break;
            case 39:
                //按下→键
                // 先进行判断,如果当前的方向是向左移动,此事我们不能按右键
                if (self.snake.direction == 'L') {
                    return;
                }
                self.snake.changeDirection("R");
                self.d = 'R';
                break;
            case 40:
                // 按下↓键
                // 先进行判断,如果当前的方向是向上移动,此事我们不能按下键
                if (self.snake.direction == 'U') {
                    return;
                }
                self.snake.changeDirection("D");
                self.d = 'D';
                break;
        }
    }
}

       最后,我们给蛇的方向运动添加添加正确数值的操作:

Snake.prototype.update = function() {
    // 让当前的direction接受一下willDirection
    this.direction = this.willDirection;
    switch (this.direction) {
        case 'R':
            // 向右
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col + 1 });
            break;
        case 'D':
            // 向下
            this.body.unshift({ "row": this.body[0].row + 1, "col": this.body[0].col });
            break;
        case 'L':
            // 向左
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col - 1 });
            break;
        case 'U':
            // 向上
            this.body.unshift({ "row": this.body[0].row - 1, "col": this.body[0].col });
            break;
    }
        this.body.pop();
};

       运动的蛇的代码就完成了。之后在设置蛇头的代码里面添加你需要的转头操作:

switch (this.d) {
        case 'R':
            //初始方向
            break;
        case 'D':
            img.style.transform = 'rotate(90deg)';
            break;
        case 'L':
            img.style.transform = 'rotate(180deg)';
            break;
        case 'U':
            img.style.transform = 'rotate(-90deg)';
            break;
    }

       记得在Game函数里面添加初始化的方向。


五、吃下食物,越来越长

       我们写一个food的js,书写一个随机生成的食物:

this.row = parseInt(Math.random() * gameSnake.row);
        this.col = parseInt(Math.random() * gameSnake.col);

然后渲染出去:

Food.prototype.render = function() {
    game.setHTML(this.row, this.col);
}

       我们创造的食物,当他和蛇头接触的时候,我们需要做出判断:和蛇头完全重合的时候,就需要清空食物所在的位置,然后重新生成食物。

让我们看看怎么书写的代码:

function Food(gameSnake) {
    // 备份this
    var self = this;
    //食物的位置
    // 下面的do-while循环语句作用是先创建一个row和col然后判断这个数值在不在蛇身上
    do {
        this.row = parseInt(Math.random() * gameSnake.row);
        this.col = parseInt(Math.random() * gameSnake.col);
    } while ((function() {
            // 遍历蛇的row和col然后和food新随机出来的row和col判断是否重合
            for (var i = 0; i < gameSnake.snake.body.length; i++) {
                if (gameSnake.snake.body[i].row == self.row && gameSnake.snake.body[i].col == self.col) {
                    return true;
                }
            }
            return false;
        })());

}

       写完蛇的食物代码,我们再看看蛇应该如何增长身体。

       我们知道,我们的蛇实际上是在不断增长的,为了保持他的身体长度,所以需要删除一节来保持长度。那么其实原理就很好理解了,我们只需要在蛇判定吃到食物的时候,先不删除最后一节,直到下一次的时候继续删除最后一节的内容,我们的蛇就会保持增长一节的身体继续行动,也就是增长身体的过程了。接下来让我们看看代码是怎么实现的:

// 蛇吃食物
    // 判断如果当前的蛇的头部没有喝食物进行重合,就代表此时没有迟到食物,此时就进行尾巴删除,如果重合吃到了,不进行尾部删除
    if (this.body[0].row == game.food.row && this.body[0].col == game.food.col) {
        //创建新的食物
        // 此时头部增加了,尾巴没有删除
        game.food = new Food(game);
    } else {
        this.body.pop();
    }

这段代码记得要放进update的函数里面。


六、死亡判定

蛇死亡的条件是:

  • 碰撞到墙壁
  • 咬到自己的身体

针对这两点,我们来构写代码:

       判断蛇的头如果位置超过了我们设置的表格,就结束游戏,同时停止我们的定时器:

// 死亡的判断——撞击墙壁
    if (this.body[0].col > game.col - 1 || this.body[0].row > game.row - 1 || this.body[0].col < 0 || this.body[0].row < 0) {
        alert('游戏结束!'); //删除是因为当前的头赠是不合法的,因为游戏结束了
        this.body.shift();
        clearInterval(game.timer);
        location.reload();
    }

       这里我们可以看到使用了shift的方法,因为当头超过表格之后,系统就会报错说你的头部已经不在表格里,找不到这个对象,所以需要把头部超出去的部分删除。同时我们用location.reload的方法刷新页面,就可以结束这个游戏了。

       之后我们再去循环遍历蛇的身体,如果说头部和蛇身体的部位发生重合,那么也用一样的方式判定死亡:

// 死亡判断——咬到自己
    for (var i = 1; i < this.body.length; i++) {
        // 如果能够判断死亡,也就是当前的蛇的头部和身体的某个部分row和col完全重合的时候
        if (this.body[0].col == this.body[i].col && this.body[0].row == this.body[i].row) {
            alert('游戏结束!'); //删除是因为当前的头赠是不合法的,因为游戏结束了
            this.body.shift();
            clearInterval(game.timer);
            location.reload();
        }
    }

       这样我们的死亡判定也一起写好了。同样,这些过程都是update更新的时候就需要做的判定,所以都要写在update的函数里面。


七、越吃越快

       蛇每次吃东西的时候速度都会越来越快,我们需要怎么做到这个过程呢?

  • 首先我们给定时器设置一个非常快的帧数,让他会不停的刷新。在这个过程中,我们建立一个变量f,让他进行一个快速的自增加的算法。
  • 这个时候我们知道,我们蛇的身体是会不断增加的,那么我们就设置一个变量,计算这个差值,当我们的f % 变量 == 0的时候,我们才执行和这个update函数。

       什么意思呢?就是我们通过控制update函数的更新速度,来达到可以控制蛇速度的方法。假如我们的蛇开始是3,我们让30 - 3 = 27,那么不断变化的f只有 % 27 == 0的时候,才会执行,也就是每次在f为27的1的倍数,2的倍数,3的倍数的时候,才会去执行update。而吃到食物之后,长度为4,那么每次为26的1的倍数,2的倍数,3的倍数的时候,才会去执行update。这样就成功做到了加速蛇的速度的功能。让我们来看看是怎么书写的代码:

Game.prototype.start = function() {
    // 帧编号
    var self = this;
    this.f = 0;
        self.timer = setInterval(function() {
            // 定时器里面的核心就是游戏渲染的本质,清楚屏幕,更新,渲染。
            game.f++;
            // 清楚屏幕
            game.clear();
            //蛇的更新速度,当蛇边长的时候,速度要加快
            var during = game.snake.body.length < 30 ? 30 - game.snake.body.length : 1;
            // 蛇的渲染
            game.f % during == 0 && game.snake.update();
            // 渲染蛇
            game.snake.render();
            // 渲染食物
            game.food.render();
        }, 20);
}

八、结尾

       最后,我们来处理一下其他的细节:

       首先我们开始界面是一个动图制作的按钮,然后点击之后,才会开始游戏,其实很简单,只需要把定时器装进按钮的点击事件去执行就可以了。

按钮的html和CSS代码:

<style>
      .style {
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0;
            top: 0;
            background-color: rgba(0, 0, 0, 0);
            z-index: 1;
            text-align: center;
        }
        
        .start {
            display: block;
            z-index: 9999;
        }
        
        .stop {
            display: none;
        }
        
        button {
            width: 200px;
            height: 100px;
            background: url('img/btn1.gif') no-repeat center -50px;
            background-size: cover;
            z-index: 999;
            border: transparent;
            margin-top: 50%;
            transform: translateY(-50%);
            background-color: transparent;
            cursor: pointer;
            z-index: 9999;
        }
</style>  
<body>
    <h3 id="f" class="f">帧编号:0</h3>
    <h3 id="score" class="score">分数:0</h3>
    <div class="box">
        <div class="start style">
            <button></button>
        </div>
        <div class="style on">
            <img src="img/btn4.png" alt="" class="pause">
        </div>
        <div id="app" class="app"></div>
    </div>
    <script src="js/Game.js"></script>
    <script src="js/Snake.js"></script>
    <script src="js/Food.js"></script>
</body>
<script>
    var game = new Game();
</script>

       同时里面我们看到有一个on的盒子,就是我设置的暂停按钮的盒子。这里把剩下的css代码补上:

.pause {
            display: none;
            position: absolute;
            width: 70px;
            height: 70px;
            top: 50%;
            left: 50%;
            margin-top: -35px;
            margin-left: -35px;
        }

       暂停按钮的内容,这里需要注意的重点就是,需要用节流阀。节流阀的变量初始化记得写到Game的函数里面。

接下来把完整的代码展示给大家,感谢大家的收看。

HTML:

<style>
        * {
            box-sizing: border-box;
            padding: 0;
            margin: 0;
        }
        
        h3 {
            position: absolute;
            left: 0;
            top: 0;
            width: 200px;
            height: 50px;
        }
        
        .score {
            top: 20px;
        }
        
        .box {
            width: 625px;
            height: 625px;
            margin: 20px auto;
            position: relative;
        }
        
        .app {
            position: relative;
            background: #FCE4EC;
            border: 20px solid #F8BBD0;
        }
        
        table {
            border-collapse: collapse;
            box-sizing: border-box;
        }
        
        td {
            position: relative;
            width: 25px;
            height: 25px;
            text-align: center;
            background-color: #fce4ec;
            border-radius: 15px;
            background-size: 100% 100%;
        }
        
        .style {
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0;
            top: 0;
            background-color: rgba(0, 0, 0, 0);
            z-index: 1;
            text-align: center;
        }
        
        .start {
            display: block;
            z-index: 9999;
        }
        
        .stop {
            display: none;
        }
        
        button {
            width: 200px;
            height: 100px;
            background: url('img/btn1.gif') no-repeat center -50px;
            background-size: cover;
            z-index: 999;
            border: transparent;
            margin-top: 50%;
            transform: translateY(-50%);
            background-color: transparent;
            cursor: pointer;
            z-index: 9999;
        }
        
        .head {
            position: absolute;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
        }
        
        .pause {
            display: none;
            position: absolute;
            width: 70px;
            height: 70px;
            top: 50%;
            left: 50%;
            margin-top: -35px;
            margin-left: -35px;
        }
    </style>
</head>

<body>
    <h3 id="f" class="f">帧编号:0</h3>
    <h3 id="score" class="score">分数:0</h3>
    <div class="box">
        <div class="start style">
            <button></button>
        </div>
        <div class="style on">
            <img src="img/btn4.png" alt="" class="pause">
        </div>
        <div id="app" class="app"></div>
    </div>
    <script src="js/Game.js"></script>
    <script src="js/Snake.js"></script>
    <script src="js/Food.js"></script>
</body>
<script>
    var game = new Game();
</script>

Game.js:

var box = document.querySelector('.style');
var btn = box.querySelector('button');
var on = document.querySelector('.on');
var pause = on.querySelector('.pause');

function Game() {
    //行数
    this.row = 23;
    //列数
    this.col = 25;
    //初始化分数
    this.score = 0;
    // 初始化结点
    this.init();
    // 实例化蛇类
    this.snake = new Snake();
    //食物
    this.food = new Food(this);
    // 执行定时器任务
    this.start();
    // 键盘的事件监听
    this.bindEvent();
    //初始化蛇的头方向
    this.d = 'R';
    //初始化暂停
    this.flag = 0;
}
Game.prototype.init = function() {
    this.dom = document.createElement("table");
    var tr, td;
    //遍历行和列上树
    for (var i = 0; i < this.row; i++) {
        //遍历行,创建结点上树
        tr = document.createElement('tr');
        for (var j = 0; j < this.col; j++) {
            //遍历列,创建结点上树
            td = document.createElement('td');
            //追加到tr
            tr.appendChild(td);
        }
        //追加节点上树
        this.dom.appendChild(tr);
    }
    //表格上树
    document.getElementById('app').appendChild(this.dom);
}
Game.prototype.clear = function() {
    //遍历表格,擦除画布
    for (var i = 0; i < this.row; i++) {
        for (var j = 0; j < this.col; j++) {
            this.dom.getElementsByTagName('tr')[i].getElementsByTagName('td')[j].style.background = '';
            this.dom.getElementsByTagName('tr')[i].getElementsByTagName('td')[j].innerHTML = '';
        }
    }
};
//设置头部
Game.prototype.setHead = function(row, col) {
    var img = document.createElement('img');
    img.src = 'img/snake.png';
    img.classList.add('head');
    this.dom.getElementsByTagName('tr')[row].getElementsByTagName('td')[col].appendChild(img);
    switch (this.d) {
        case 'R':
            //初始方向
            break;
        case 'D':
            img.style.transform = 'rotate(90deg)';
            break;
        case 'L':
            img.style.transform = 'rotate(180deg)';
            break;
        case 'U':
            img.style.transform = 'rotate(-90deg)';
            break;
    }
}

// 设置颜色的方法
Game.prototype.setColor = function(row, col, color) {
    // 让表格的第几行第几列设置什么颜色
    this.dom.getElementsByTagName('tr')[row].getElementsByTagName('td')[col].style.background = color;
};
// 渲染食物
Game.prototype.setHTML = function(row, col) {
    this.dom.getElementsByTagName('tr')[row].getElementsByTagName('td')[col].style.backgroundImage = 'url(./img/food2.png)';
};
//设置键盘监听事件
Game.prototype.bindEvent = function() {
    var self = this;
    // 键盘事件
    document.onkeydown = function(event) {
        switch (event.keyCode) {
            case 37:
                // 按下←键
                // 先进行判断,如果当前的方向是向右移动,此事我们不能按左键
                if (self.snake.direction == 'R') {
                    return;
                }
                self.snake.changeDirection("L");
                self.d = 'L';
                break;
            case 38:
                //按下↑键
                // 先进行判断,如果当前的方向是向下移动,此事我们不能按上键
                if (self.snake.direction == 'D') {
                    return;
                }
                self.snake.changeDirection("U");
                self.d = 'U';
                break;
            case 39:
                //按下→键
                // 先进行判断,如果当前的方向是向左移动,此事我们不能按右键
                if (self.snake.direction == 'L') {
                    return;
                }
                self.snake.changeDirection("R");
                self.d = 'R';
                break;
            case 40:
                // 按下↓键
                // 先进行判断,如果当前的方向是向上移动,此事我们不能按下键
                if (self.snake.direction == 'U') {
                    return;
                }
                self.snake.changeDirection("D");
                self.d = 'D';
                break;
        }
    }
}
Game.prototype.start = function() {
    // 帧编号
    var self = this;
    this.f = 0;

    btn.addEventListener('click', function() {
        box.classList.add('stop');
        box.classList.remove('start');
        self.timer = setInterval(function() {
            // 定时器里面的核心就是游戏渲染的本质,清楚屏幕,更新,渲染。
            game.f++;
            document.getElementById("f").innerHTML = "帧编号:" + game.f;
            // 渲染分数
            document.getElementById("score").innerHTML = "分数为:" + game.score;
            // 清楚屏幕
            game.clear();
            //蛇的更新速度,当蛇边长的时候,速度要加快
            var during = game.snake.body.length < 30 ? 30 - game.snake.body.length : 1;
            // 蛇的渲染
            console.log(during);
            game.f % during == 0 && game.snake.update();
            // 渲染蛇
            game.snake.render();
            // 渲染食物
            game.food.render();
        }, 20);
    });
    on.addEventListener('click', function() {
        if (self.flag == 0) {
            self.flag = 1;
            pause.style.display = 'block';
            clearInterval(self.timer);
        } else if (self.flag == 1) {
            self.flag = 0;
            pause.style.display = 'none';
            self.timer = setInterval(function() {
                // 定时器里面的核心就是游戏渲染的本质,清楚屏幕,更新,渲染。
                game.f++;
                document.getElementById("f").innerHTML = "帧编号:" + game.f;
                // 渲染分数
                document.getElementById("score").innerHTML = "分数为:" + game.score;
                // 清楚屏幕
                game.clear();
                //蛇的更新速度,当蛇边长的时候,速度要加快,为当前30-当前身体长度的,否则为1
                var during = game.snake.body.length < 30 ? 30 - game.snake.body.length : 1;
                // 蛇的渲染
                game.f % during == 0 && game.snake.update();
                // 渲染蛇
                game.snake.render();
                // 渲染食物
                game.food.render();
            }, 20);
        }
    });

}

Food.js:

function Food(gameSnake) {
    // 备份this
    var self = this;
    //食物的位置
    // 下面的do-while循环语句作用是先创建一个row和col然后判断这个数值在不在蛇身上
    do {
        this.row = parseInt(Math.random() * gameSnake.row);
        this.col = parseInt(Math.random() * gameSnake.col);
    } while ((function() {
            // 遍历蛇的row和col然后和food新随机出来的row和col判断是否重合
            for (var i = 0; i < gameSnake.snake.body.length; i++) {
                if (gameSnake.snake.body[i].row == self.row && gameSnake.snake.body[i].col == self.col) {
                    return true;
                }
            }
            return false;
        })());

}
Food.prototype.render = function() {
    game.setHTML(this.row, this.col);
}

Snake.js:

function Snake() {
    // 蛇的初始化身体
    this.body = [
        { "row": 3, "col": 5 },
        { "row": 3, "col": 4 },
        { "row": 3, "col": 3 },
        { "row": 3, "col": 2 }
    ];
    // 信息量,设置的运动方向
    this.direction = 'R';
    // 即将改变的方向,目的就是为了防止原地调头的情况
    this.willDirection = 'R';
}
// 蛇的运动
Snake.prototype.update = function() {
    // 让当前的direction接受一下willDirection
    this.direction = this.willDirection;
    switch (this.direction) {
        case 'R':
            // 向右
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col + 1 });
            break;
        case 'D':
            // 向下
            this.body.unshift({ "row": this.body[0].row + 1, "col": this.body[0].col });
            break;
        case 'L':
            // 向左
            this.body.unshift({ "row": this.body[0].row, "col": this.body[0].col - 1 });
            break;
        case 'U':
            // 向上
            this.body.unshift({ "row": this.body[0].row - 1, "col": this.body[0].col });
            break;
    }
    // 死亡的判断——撞击墙壁
    if (this.body[0].col > game.col - 1 || this.body[0].row > game.row - 1 || this.body[0].col < 0 || this.body[0].row < 0) {
        alert('游戏结束!您的得分是:' + game.score + '分'); //删除是因为当前的头赠是不合法的,因为游戏结束了
        this.body.shift();
        clearInterval(game.timer);
        location.reload();
    }
    // 死亡判断——咬到自己
    for (var i = 1; i < this.body.length; i++) {
        // 如果能够判断死亡,也就是当前的蛇的头部和身体的某个部分row和col完全重合的时候
        if (this.body[0].col == this.body[i].col && this.body[0].row == this.body[i].row) {
            alert('游戏结束!您的得分是:' + game.score + '分'); //删除是因为当前的头赠是不合法的,因为游戏结束了
            this.body.shift();
            clearInterval(game.timer);
            location.reload();
        }
    }
    // 蛇吃食物
    // 判断如果当前的蛇的头部没有喝食物进行重合,就代表此时没有迟到食物,此时就进行尾巴删除,如果重合吃到了,不进行尾部删除
    if (this.body[0].row == game.food.row && this.body[0].col == game.food.col) {
        //创建新的食物
        // 此时头部增加了,尾巴没有删除
        game.food = new Food(game);
        // 加分数
        game.score++;
        //让帧编号归0,因为蛇会突进一下
        game.f = 0;
    } else {
        this.body.pop();
    }
};
// 蛇的方向改变,防止的是在一次渲染之前会出现调头的情况
Snake.prototype.changeDirection = function(d) {
    this.willDirection = d;
}
Snake.prototype.render = function() {
    // 蛇的渲染
    game.setHead(this.body[0].row, this.body[0].col);
    for (var i = 1; i < this.body.length; i++) {
        game.setColor(this.body[i].row, this.body[i].col, '#9ccc65');
    }
}