贪吃蛇案例(面向对象编程)

1,042 阅读14分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

贪吃蛇游戏案例

分析需要封装的对象

rDAAoR.png

整个游戏中,我们可见的对象有两个,分别是食物、蛇,同时我们也可以把这个游戏的逻辑封装成对象,这样我们就抽象出来了三个对象

注意:这里可以使用自调用函数(立即执行函数)进行封装,通过window暴露对象

食物对象

分析属性和方法

属性:

  • 食物的背景色
  • 食物宽度和高速
  • 食物在整个游戏界面中的定位----top和left值

方法:

  • 食物要渲染到舞台上,所以要有一个渲染到舞台上的方法,并且需要随机定位
  • 删除食物方法,当蛇碰到食物后,需要删除这个食物,当然如果考虑到后期可能存在多个食物,就需要创建一个食物合集,容纳所有食物,后期通过某种方法删掉某个指定食物

代码实现

<!-- 新建index.html文件 -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入样式 -->
  <link rel="stylesheet" href="./css/index.css">
</head>

<body>
  <!-- 舞台元素 -->
  <div class="box" id="box"></div>
  <!-- 引入外部js文件(目前仅引入了三个,后面会更多) -->
  <script src="./js/tools.js"></script>
  <script src="./js/food.js"></script>
  <script src="./js/index.js"></script>
</body>

</html>
/* 新建index.css文件 */

/* 样式重置 */
* {
  margin: 0;
  padding: 0;
}

/* 舞台样式*/
.box {
  position: relative;
  width: 500px;
  height: 500px;
  background: #ccc;
}
//新建food.js文件

// 为了避免在全局定义变量或者函数导致命名混乱,使用自调用函数(立即执行函数)将Food定义在window上,后面使用window。Food或者直接之用Food调用构造函数即可
(function () {
  // 新建一个变量。存储食物的定位属性,这里设置为绝对定位
  var ps = 'absolute';
  // 新建构造函数
  function Food(obj) {
    // 为了避免用户传入的数据格式不正确,使用一个判断,如果用户传入的是对象类型数据,直接使用,否则使用空对象
    obj = obj instanceof Object ? obj : {};
    //设置五个属性的值,并且给予默认值以免用户没有输入
    //宽度
    this.width = obj.width || 20;
    //高度
    this.height = obj.height || 20;
    //定位top
    this.top = obj.top || 0;
    //定位left
    this.left = obj.left || 0;
    //背景色
    this.backgroundColor = obj.backgroundColor || 'skyblue';
    //使用一个属性容纳后面生成的元素节点,方便后期可能使用多个食物
    this.items = [];
  }
  //重新定义Food原型对象
  Food.prototype = {
    //手动指向Food
    constructor: Food,
    //渲染方法,新建元素并渲染到舞台上面
    //参数:要渲染到的的舞台--元素,案例中为box元素
    reader: function (father) {
      //设置元素的css样式
      //随机top定位值:使用工具对象tools的随机数方法,为避免先出现错位,我们把舞台分割成一个个小方块
      //随机定位top = 随机数(0,舞台高度/食物高度-1)*食物高度
      this.top = tools.getRandomNumber(0, father.clientHeight / this.height - 1) * this.height;
      //left定位值,原理同上
      this.left = tools.getRandomNumber(0, father.clientWidth / this.width - 1) * this.width;
      //创建一个元素节点
      var fooder = document.createElement('span');
      //设置元素节点属性
      //定位属性,使用变量,方便后期修改
      fooder.style.position = ps;
      //宽高
      fooder.style.width = this.width + 'px';
      fooder.style.height = this.height + 'px';
      //定位偏移量属性
      fooder.style.top = this.top + 'px';
      fooder.style.left = this.left + 'px';
      //背景色
      fooder.style.backgroundColor = this.backgroundColor;
      //把元素添加进舞台
      father.appendChild(fooder);
      //存储元素
      this.items.push(fooder);
    },
    //删除元素方法
    //参数1:舞台元素
    //参数2:要删除元素的下标
    remove: function (father, index) {
      father.removeChild(this.items[index]);
      this.items.splice(index, 1);
    }
  }
  // 把构造函数存储在window里面,由于window可以省略不写,所以后期可以直接使用Food
  window.Food = Food;
})()
//新建tools.js文件

// 工具对象,用来存储会用到的一些方法
var tools = {
  // 生成随机数
  getRandomNumber: function (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1) + min);
  },
  // 生成随机颜色
  getRandomColor: function () {
    return 'rgb(' + this.getRandomNumber(0, 255) + ',' + this.getRandomNumber(0, 255) + ',' + this.getRandomNumber(0, 255) + ')';
  }
}

蛇对象

属性和方法

属性:

  • 每一节蛇节的宽高
  • 每一节的特殊属性:定位left和top值,背景颜色(蛇头和蛇身不相同)
  • 蛇的运动方向

方法:

  • 渲染到舞台上的方法(reader)

代码实现

// 将蛇的构造函数加载到window之上,原理同food一样
(function () {
  //创建一个变量,表示蛇节的定位方式,这里设置为绝对定位
  var ps = 'absolute';

  // 蛇的构造函数
  function Snick(opt) {
    // 判断传入参数是否为对象类型,如果是使用,否则使用空对象
    opt = opt instanceof Object ? opt : {};
    // 写入蛇节的宽高属性和默认值
    this.width = opt.width || 20;
    this.height = opt.height || 20;
    // 把每一节蛇身集合在一个数组里面,后期便于修改
    // 蛇身数组:内部每一个元素都是一个对象,存储蛇节的定位属性和背景颜色
    this.body = [
        // 第一个表示蛇头,剩下的表示蛇身体的每一节,注意蛇头和蛇身体被禁颜色不同
        {
          x: 3,
          y: 2,
          color: 'red'
        }, {
          x: 2,
          y: 2,
          color: 'blue'
        }, {
          x: 1,
          y: 2,
          color: 'blue'
        }
      ],
      // 默认蛇的运动方向,这里默认向右,也可以是上、下、左
      this.direction = 'right';
  }
  // 重新定义构造函数原型对象
  Snick.prototype = {
    // 手动指向构造函数
    constructor: Snick,
    // 渲染方法
    // 参数:舞台元素
    reader: function (father) {
      // 我们要把每一节蛇渲染到舞台上,所以要遍历整个蛇身数组
      // 新建两个变量,可以节省每次都读取this.body.length
      for (let i = 0, len = this.body.length; i < len; i++) {
        // 创建新的元素节点
        var me = document.createElement('span');
        // 设置元素节点行内样式
        // 定位方式
        me.style.position = ps;
        // 宽高
        me.style.width = this.width + 'px';
        me.style.height = this.height + 'px';
        // 定位偏移量属性
        me.style.top = this.body[i].y * this.height + 'px';
        me.style.left = this.body[i].x * this.width + 'px';
        // 背景颜色
        me.style.backgroundColor = this.body[i].color;
        // 把每一节插入到舞台
        father.appendChild(me);
      }
    }
  }
  // 把整个构造函数赋值给window
  window.Snick = Snick;
})()

游戏对象

属性和方法

属性:

  • 食物对象实例
  • 蛇对象实例
  • 舞台对象

方法:

  • 初始化(渲染食物和蛇初始化)

代码实现

//创建新game.jss

// 写在自调用函数(立即执行函数内部),同样把构造函数放到window对象上
(function () {
  // 游戏对象构造函数
  // 参数:舞台元素
  function Game(map) {
    // 三个属性
    // 食物实例
    this.food = new Food();
    // 蛇实例
    this.snick = new Snick();
    // 舞台元素
    this.map = map;
  }
  // 充血游戏对象原型对象
  Game.prototype = {
    // 执行构造函数
    constructor: Game,
    // 初始化方法,把蛇和食物初始化到舞台上
    inIt: function () {
      this.food.reader(this.map);
      this.snick.reader(this.map);
    }
  }
  // 把游戏构造函数放到window之上
  window.Game = Game;
})()

// 测试代码
// 获取舞台
var map = document.getElementById('box');
// 创建游戏实例
var game = new Game(map);
// 执行游戏初始化
game.inIt();

游戏逻辑实现

蛇的运动和游戏结束

我们需要给蛇添加运动方法,我们可以使用之前的蛇实例中的reader方法实现(可以使用定时器每隔一定时间就执行一次蛇的reader方法),但是注意,调用方法之前我们需要先删除掉渲染出来的蛇,否则你会发现出现了两条蛇

蛇的删除方法

思路:

  1. 要想删除我们必须要找到要删除的元素
  2. 我们之前蛇的渲染方法里面把每次添加到舞台上的蛇节都存在一个数组属性(this.items)里面
  3. 在蛇的原型上添加删除方法(remove)就可以调用这个属性的数组遍历进行删除
  4. 之后再调用蛇的渲染方法(reader)
// snick.js文件

//在snick构造函数上添加蛇节数组
this.items = [];

// 给Snick添加删除蛇方法
// 删除蛇方法
// 参数:舞台元素
remove: function (father) {
  // 遍历蛇节数组,从舞台元素中删除每一项
  for (let i = 0, l = this.items.length; i < l; i++) {
    // 删除节点
    father.removeChild(this.items[i]);
  }
  // 重置蛇节数组
  this.items = [];
}

蛇位置更新方法

思路:

  1. 给蛇实例新建一个更新蛇位置方法(upDate)
  2. 方法内部先执行蛇节(snick.body)内部位置的更新(每次蛇的移动,蛇身体(除蛇头外)都会到达上一个蛇节的位置,所以我们更新蛇节位置把上一节的位置赋值给下一节,蛇头根据运动方向自行调整)
// snick.js文件

// 在snick原型对象上添加
// 蛇节位置更新方法
    upDate: function () {
      // 循环遍历蛇节数组,把每一节蛇的定位设置为上一节的定位
      for (let i = this.body.length - 1; i > 0; i--) {
        this.body[i].x = this.body[i - 1].x;
        this.body[i].y = this.body[i - 1].y;
      }
      // 根据运动方向修改蛇头定位
      switch (this.direction) {
        case 'right':
          this.body[0].x++;
          break;
        case 'left':
          this.body[0].x--;
          break;
        case 'top':
          this.body[0].y--;
          break;
        case 'buttom':
          this.body[0].y++;
          break;
      }
    }

让蛇运动起来并且超出游戏边缘停止游戏

运动

我们已经在蛇的内部定义了蛇删除方法、位置更新方法、渲染方法,所以想要蛇运动起来我们只需要不断重复执行删除、更新、渲染方法即可

游戏结束

我们在每次蛇运动的时候判断蛇头是否超出了游戏舞台边缘,我们可以判断蛇头的x和y值(份数),当x或者Y小于0或者大于最大分数即可停止游戏

最终实现

我们可以把这一整套流程写在一个函数内部,这样就不需要写在构造函数内部,在函数内写一个定时器,不断执行蛇的删除、位置更新、渲染方法,而且每次要判断是否超出舞台边缘。

注意this指向问题,因为如果写在外部函数中可能会导致this失效,所以我们要手动把this

//game.js

// 写在自调用函数(立即执行函数内部),同样把构造函数放到window对象上
(function () {
  //定义一个变量,指向构造函数的this
  var that;
  // 游戏对象构造函数
  // 参数:舞台元素
  function Game(map) {
    // 三个属性
    // 食物实例
    this.food = new Food();
    // 蛇实例
    this.snick = new Snick();
    // 舞台元素
    this.map = map;
    //在构造函数内把this赋值给that
    that = this;
  }
  // 重写游戏对象原型对象
  Game.prototype = {
    // 执行构造函数
    constructor: Game,
    // 初始化方法,把蛇和食物初始化到舞台上
    inIt: function () {
      this.food.reader(this.map);
      this.snick.reader(this.map);
      // 调用蛇运动函数,执行蛇的运动和游戏结束操作
      snickRun();
    }
  }
  // 蛇运动函数,私有函数,只能在内部调用,注意这里要使用that代替this
  function snickRun() {
    // 创建定时器,不断重复执行蛇运动和判断游戏结束
    var timer = setInterval(function () {
      //执行蛇删除方法
      that.snick.remove(that.map);
      // 执行蛇位置更新方法
      that.snick.upDate();
      // 执行蛇渲染方法
      that.snick.reader(that.map);
      //计算最大份数,当发现蛇头的x或者y超过最大份数游戏结束
      var maxX = that.map.offsetWidth / that.snick.width;
      var maxY = that.map.offsetHeight / that.snick.height;
      // 判断蛇头位置,一旦发现蛇头超过了边界,删除定时器,提示游戏结束
      if (that.snick.body[0].x < 0 || that.snick.body[0].x >= maxX || that.snick.body[0].y < 0 || that.snick.body[0].y >= maxY) {
        // 删除定时器
        clearInterval(timer);
        // 提示游戏结束
        alert('游戏结束');
      }
    }, 150)
  }
  // 把游戏构造函数放到window之上
  window.Game = Game;
})()

// 测试代码
// 获取舞台
var map = document.getElementById('box');
// 创建游戏实例
var game = new Game(map);
// 执行游戏初始化
game.inIt();

控制蛇运动方向

思路:我们主要是依靠键盘控制蛇的运动方向,所以我们需要监控键盘按下的事件,识别按下的方向键,从而更改蛇的运动方向

// 监听键盘按下事件,更改蛇运动方向
  function chageDirection() {
    // 给整个页面添加监听键盘按下事件,确定蛇运动方向,注意这里要判断一下原始方向,不要设置相反方向
    document.onkeydown = function (e) {
      // 获取按下按键键值
      var code = e.keyCode;
      // 获取当前方向
      var direction = that.snick.direction;
      // 更改方向
      switch (code) {
        case 37:
          that.snick.direction = direction === 'right' ? direction : 'left';
          console.log(that.snick.direction);
          break;
        case 38:
          that.snick.direction = direction === 'down' ? direction : 'up';
          console.log(that.snick.direction);
          break;
        case 39:
          that.snick.direction = direction === 'left' ? direction : 'right';
          console.log(that.snick.direction);
          break;
        case 40:
          that.snick.direction = direction === 'up' ? direction : 'down';
          console.log(that.snick.direction);
      }
    }
  }

吃到食物食物消失、蛇变长

思路

1、判断蛇头的定位是否等于食物的定位,如果想等,代表他们重合了,证明吃到食物了,则执行食物的删除方法

2、吃到食物之后要生成新的食物,新食物要执行食物的渲染方法

3、吃到食物要增长蛇身,就是在蛇的body数组中push一个数据,这个数据和当前蛇的最后一项的数据相同

// 写在蛇的运动函数里面--snickRun()

// 判断蛇头和食物是否重合,判断定位
if (that.snick.body[0].x * that.snick.width === that.food.left && that.snick.body[0].y * that.snick.height === that.food.top) {
  //如果重合
  //删除食物
  that.food.remove(that.map, 0);
  // 新建食物
  that.food.reader(that.map);
  //创建一个变量获取蛇的最后一节
  var last = that.snick.body[that.snick.body.length - 1];
  // 在蛇身体最后添加一节
  that.snick.body.push({
    x: last.x,
    y: last.y,
    color: 'blue'
  });
}

升级食物个数\撞到自身游戏结束

思路:类似于上面的吃到食物消失蛇变长

  1. 我们要在初始化的时候就渲染多个食物

  2. 每移动一次我们都要判断蛇头和每个食物是否重合,这时就要遍历食物数组

    //game.js
    inIt: function () {
        // 初始化添加三个食物
        this.food.reader(this.map);
        this.food.reader(this.map);
        this.food.reader(this.map);
        this.snick.reader(this.map);
        // 调用蛇运动函数,执行蛇的运动和游戏结束操作
        snickRun();
        // 执行更改方向函数,监听全局的按下键盘事件
        chageDirection();
      }
    }
    function snickRun() {
        // 创建定时器,不断重复执行蛇运动和判断游戏结束
        var timer = setInterval(function () {
          //执行蛇删除方法
          that.snick.remove(that.map);
          // 执行蛇位置更新方法
          that.snick.upDate();
          // 执行蛇渲染方法
          that.snick.reader(that.map);
          //计算最大份数,当发现蛇头的x或者y超过最大份数游戏结束
          var maxX = that.map.offsetWidth / that.snick.width;
          var maxY = that.map.offsetHeight / that.snick.height;
    
          // 多个食物,遍历食物数组,判断是否和蛇头相撞
          for (let i = 0, l = that.food.items.length; i < l; i++) {
            //获取蛇头定位和食物定位
            var headX = that.snick.body[0].x * that.snick.width,
              headY = that.snick.body[0].y * that.snick.height,
              foodX = parseFloat(that.food.items[i].style.left),
              foodY = parseFloat(that.food.items[i].style.top);
            //判断蛇头和食物重叠
            if (headX === foodX && headY === foodY) {
              //重叠删除食物
              that.food.remove(that.map, i);
              //创建一个新的食物
              that.food.reader(that.map);
              //创建一个变量获取蛇的最后一节
              var last = that.snick.body[that.snick.body.length - 1];
              // 在蛇身体最后添加一节
              that.snick.body.push({
                x: last.x,
                y: last.y,
                color: 'blue'
              });
            }
          }
          // 判断蛇头位置,一旦发现蛇头超过了边界,删除定时器,提示游戏结束
          if (that.snick.body[0].x < 0 || that.snick.body[0].x >= maxX || that.snick.body[0].y < 0 || that.snick.body[0].y >= maxY) {
            // 删除定时器
            clearInterval(timer);
            // 提示游戏结束
            alert('游戏结束');
          }
    
          // 添加蛇头碰到身体也结束游戏
          // 遍历自身除蛇头外所有蛇节,,判断是否相撞
          for (let i = 1, l = that.snick.body.length; i < l; i++) {
            // 判断是否相撞,满足条件结束游戏
            if (that.snick.body[0].x === that.snick.body[i].x && that.snick.body[0].y === that.snick.body[i].y) {
              // 删除定时器
              clearInterval(timer);
              // 提示游戏结束
              alert('游戏结束');
            }
          }
        }, 150)
      }
    

最终代码实现

注:这里没有使用代码压缩和合并代码操作,有没有给自调用函数传参,后期可以你自己执行一下

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入样式 -->
  <link rel="stylesheet" href="./css/index.css">
</head>

<body>
  <!-- 舞台元素 -->
  <div class="box" id="box"></div>
  <!-- 引入外部js文件 -->
  <script src="./js/tools.js"></script>
  <script src="./js/food.js"></script>
  <script src="./js/snick.js"></script>
  <script src="./js/game.js"></script>
  <script src="./js/index.js"></script>
</body>

</html>

Index.css

/* 样式重置 */
* {
  margin: 0;
  padding: 0;
}

/* 舞台样式 */
.box {
  position: relative;
  width: 500px;
  height: 500px;
  background: #ccc;
}

Tools.js

// 工具对象,用来存储会用到的一些方法
var tools = {
  // 生成随机数
  getRandomNumber: function (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1) + min);
  },
  // 生成随机颜色
  getRandomColor: function () {
    return 'rgb(' + this.getRandomNumber(0, 255) + ',' + this.getRandomNumber(0, 255) + ',' + this.getRandomNumber(0, 255) + ')';
  }
}

food.js

// 为了避免在全局定义变量或者函数导致命名混乱,使用自调用函数(立即执行函数)将Food定义在window上,后面使用window。Food或者直接之用Food调用构造函数即可
(function () {
  // 新建一个变量。存储食物的定位属性,这里设置为绝对定位
  var ps = 'absolute';
  // 新建构造函数
  function Food(obj) {
    // 为了避免用户传入的数据格式不正确,使用一个判断,如果用户传入的是对象类型数据,直接使用,否则使用空对象
    obj = obj instanceof Object ? obj : {};
    //设置五个属性的值,并且给予默认值以免用户没有输入
    //宽度
    this.width = obj.width || 20;
    //高度
    this.height = obj.height || 20;
    //定位top
    this.top = obj.top || 0;
    //定位left
    this.left = obj.left || 0;
    //背景色
    this.backgroundColor = obj.backgroundColor || 'skyblue';
    //使用一个属性容纳后面生成的元素节点,方便后期可能使用多个食物
    this.items = [];
  }
  //重新定义Food原型对象
  Food.prototype = {
    //手动指向Food
    constructor: Food,
    //渲染方法,新建元素并渲染到舞台上面
    //参数:要渲染到的的舞台--元素,案例中为box元素
    reader: function (father) {
      //设置元素的css样式
      //随机top定位值:使用工具对象tools的随机数方法,为避免先出现错位,我们把舞台分割成一个个小方块
      //随机定位top = 随机数(0,舞台高度/食物高度-1)*食物高度
      this.top = tools.getRandomNumber(0, father.clientHeight / this.height - 1) * this.height;
      //left定位值,原理同上
      this.left = tools.getRandomNumber(0, father.clientWidth / this.width - 1) * this.width;
      //创建一个元素节点
      var fooder = document.createElement('span');
      //设置元素节点属性
      //定位属性,使用变量,方便后期修改
      fooder.style.position = ps;
      //宽高
      fooder.style.width = this.width + 'px';
      fooder.style.height = this.height + 'px';
      //定位偏移量属性
      fooder.style.top = this.top + 'px';
      fooder.style.left = this.left + 'px';
      //背景色
      fooder.style.backgroundColor = this.backgroundColor;
      //把元素添加进舞台
      father.appendChild(fooder);
      //存储元素
      this.items.push(fooder);
    },
    //删除元素方法
    //参数1:舞台元素
    //参数2:要删除元素的下标
    remove: function (father, index) {
      father.removeChild(this.items[index]);
      this.items.splice(index, 1);
    }
  }
  // 把构造函数存储在window里面,由于window可以省略不写,所以后期可以直接使用Food
  window.Food = Food;
})()

Snick.js

// 将蛇的构造函数加载到window之上,原理同food一样
(function () {
  //创建一个变量,表示蛇节的定位方式,这里设置为绝对定位
  var ps = 'absolute';

  // 蛇的构造函数
  function Snick(opt) {
    // 判断传入参数是否为对象类型,如果是使用,否则使用空对象
    opt = opt instanceof Object ? opt : {};
    // 写入蛇节的宽高属性和默认值
    this.width = opt.width || 20;
    this.height = opt.height || 20;
    // 把每一节蛇身集合在一个数组里面,后期便于修改
    // 蛇身数组:内部每一个元素都是一个对象,存储蛇节的定位属性和背景颜色
    this.body = [
        // 第一个表示蛇头,剩下的表示蛇身体的每一节,注意蛇头和蛇身体被禁颜色不同
        {
          x: 3,
          y: 2,
          color: 'red'
        }, {
          x: 2,
          y: 2,
          color: 'blue'
        }, {
          x: 1,
          y: 2,
          color: 'blue'
        }
      ],
      // 默认蛇的运动方向,这里默认向右,也可以是上、下、左
      this.direction = 'right';
    // 创建蛇节数组
    this.items = [];
  }
  // 重新定义构造函数原型对象
  Snick.prototype = {
    // 手动指向构造函数
    constructor: Snick,
    // 渲染方法
    // 参数:舞台元素
    reader: function (father) {
      // 我们要把每一节蛇渲染到舞台上,所以要遍历整个蛇身数组
      // 新建两个变量,可以节省每次都读取this.body.length
      for (let i = 0, len = this.body.length; i < len; i++) {
        // 创建新的元素节点
        var me = document.createElement('span');
        // 设置元素节点行内样式
        // 定位方式
        me.style.position = ps;
        // 宽高
        me.style.width = this.width + 'px';
        me.style.height = this.height + 'px';
        // 定位偏移量属性
        me.style.top = this.body[i].y * this.height + 'px';
        me.style.left = this.body[i].x * this.width + 'px';
        // 背景颜色
        me.style.backgroundColor = this.body[i].color;
        // 把每一节插入到舞台
        father.appendChild(me);
        this.items.push(me);
      }
    },
    // 删除蛇方法
    // 参数:舞台元素
    remove: function (father) {
      // 遍历蛇节数组,从舞台元素中删除每一项
      for (let i = 0, l = this.items.length; i < l; i++) {
        // 删除节点
        father.removeChild(this.items[i]);
      }
      // 重置蛇节数组
      this.items = [];
    },
    // 蛇节位置更新方法
    upDate: function () {
      // 循环遍历蛇节数组,把每一节蛇的定位设置为上一节的定位
      for (let i = this.body.length - 1; i > 0; i--) {
        this.body[i].x = this.body[i - 1].x;
        this.body[i].y = this.body[i - 1].y;
      }
      // 根据运动方向修改蛇头定位
      switch (this.direction) {
        case 'right':
          this.body[0].x += 1;
          break;
        case 'left':
          this.body[0].x -= 1;
          break;
        case 'up':
          this.body[0].y -= 1;
          break;
        case 'down':
          this.body[0].y += 1;
          break;
      }
    }
  }
  // 把整个构造函数赋值给window
  window.Snick = Snick;
})()

Game.js

// 写在自调用函数(立即执行函数内部),同样把构造函数放到window对象上
(function () {
  //定义一个变量,指向构造函数的this
  var that;
  // 游戏对象构造函数
  // 参数:舞台元素
  function Game(map) {
    // 三个属性
    // 食物实例
    this.food = new Food();
    // 蛇实例
    this.snick = new Snick();
    // 舞台元素
    this.map = map;
    //在构造函数内把this赋值给that
    that = this;
  }
  // 重写游戏对象原型对象
  Game.prototype = {
    // 执行构造函数
    constructor: Game,
    // 初始化方法,把蛇和食物初始化到舞台上
    inIt: function () {
      // 初始化添加三个食物
      this.food.reader(this.map);
      this.food.reader(this.map);
      this.food.reader(this.map);
      this.snick.reader(this.map);
      // 调用蛇运动函数,执行蛇的运动和游戏结束操作
      snickRun();
      // 执行更改方向函数,监听全局的按下键盘事件
      chageDirection();
    }
  }
  // 蛇运动函数,注意这里要使用that代替this
  function snickRun() {
    // 创建定时器,不断重复执行蛇运动和判断游戏结束
    var timer = setInterval(function () {
      //执行蛇删除方法
      that.snick.remove(that.map);
      // 执行蛇位置更新方法
      that.snick.upDate();
      // 执行蛇渲染方法
      that.snick.reader(that.map);
      //计算最大份数,当发现蛇头的x或者y超过最大份数游戏结束
      var maxX = that.map.offsetWidth / that.snick.width;
      var maxY = that.map.offsetHeight / that.snick.height;

      // 多个食物,遍历食物数组,判断是否和蛇头相撞
      for (let i = 0, l = that.food.items.length; i < l; i++) {
        //获取蛇头定位和食物定位
        var headX = that.snick.body[0].x * that.snick.width,
          headY = that.snick.body[0].y * that.snick.height,
          foodX = parseFloat(that.food.items[i].style.left),
          foodY = parseFloat(that.food.items[i].style.top);
        //判断蛇头和食物重叠
        if (headX === foodX && headY === foodY) {
          //重叠删除食物
          that.food.remove(that.map, i);
          //创建一个新的食物
          that.food.reader(that.map);
          //创建一个变量获取蛇的最后一节
          var last = that.snick.body[that.snick.body.length - 1];
          // 在蛇身体最后添加一节
          that.snick.body.push({
            x: last.x,
            y: last.y,
            color: 'blue'
          });
        }
      }
      // 判断蛇头位置,一旦发现蛇头超过了边界,删除定时器,提示游戏结束
      if (that.snick.body[0].x < 0 || that.snick.body[0].x >= maxX || that.snick.body[0].y < 0 || that.snick.body[0].y >= maxY) {
        // 删除定时器
        clearInterval(timer);
        // 提示游戏结束
        alert('游戏结束');
      }

      // 添加蛇头碰到身体也结束游戏
      // 遍历自身除蛇头外所有蛇节,,判断是否相撞
      for (let i = 1, l = that.snick.body.length; i < l; i++) {
        // 判断是否相撞,满足条件结束游戏
        if (that.snick.body[0].x === that.snick.body[i].x && that.snick.body[0].y === that.snick.body[i].y) {
          // 删除定时器
          clearInterval(timer);
          // 提示游戏结束
          alert('游戏结束');
        }
      }
    }, 150)
  }
  // 监听键盘按下事件,更改蛇运动方向
  function chageDirection() {
    // 给整个页面添加监听键盘按下事件,确定蛇运动方向,注意这里要判断一下原始方向,不要设置相反方向
    document.onkeydown = function (e) {
      // 获取按下按键键值
      var code = e.keyCode;
      // 获取当前方向
      var direction = that.snick.direction;
      // 更改方向
      switch (code) {
        case 37:
          that.snick.direction = direction === 'right' ? direction : 'left';
          console.log(that.snick.direction);
          break;
        case 38:
          that.snick.direction = direction === 'down' ? direction : 'up';
          console.log(that.snick.direction);
          break;
        case 39:
          that.snick.direction = direction === 'left' ? direction : 'right';
          console.log(that.snick.direction);
          break;
        case 40:
          that.snick.direction = direction === 'up' ? direction : 'down';
          console.log(that.snick.direction);
      }
    }
  }
  // 把游戏构造函数放到window之上
  window.Game = Game;
})()

index.js

(function () {
  // 获取舞台
  var map = document.getElementById('box');
  // 创建游戏实例
  var game = new Game(map);
  // 执行游戏初始化
  game.inIt();
})()

性能优化

减少发送HTTP请求次数

把所有代码放到合并到一个文件内部,注意引用顺序

压缩js代码

可以使用在线压缩工具--tool.oschina.net/jscompress/

自调用函数的问题

如果两个自调用函数紧挨着,而中间没有分号的话会导致问题

解决方案:在自调用函数后面或者前面加分号,最好方法是在自调用函数之前加分号

自调用函数参数

  • 在前面的代码中,自执行函数可以添加window的形参和实参
  • undefined可能会在低版本浏览器中可能会更改,在自调用函数里封装undefined可以防治undefined被篡改

例如:

(function(window,undefined){
  函数体
})(window,undefined)