JavaScript贪吃蛇游戏实现:从HTML5 Canvas到面向对象编程详解

作者:白露

时间:2026-10-23

浏览:440

本文详细解析基于JavaScript和HTML5 Canvas开发的贪吃蛇小游戏。通过Food、Snake和Game三个核心类的面向对象设计,讲解如何利用绝对定位实现网格移动、使用数组管理蛇身坐标、以及通过定时器与键盘事件控制游戏逻辑。文章涵盖匿名函数自调用、原型方法扩展及this指向处理等关键技术点,提供完整代码思路与实现细节。

文章目录

本文通过一个完整的贪吃蛇小游戏Demo,详细讲解JavaScript游戏开发的实现思路与代码结构,供读者参考学习。

近期跟随视频教程完成了一个贪吃蛇项目的开发,

在此记录其核心实现逻辑。

项目代码结构与匿名函数自调用

首先展示项目代码结构: 静态页面部分


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>贪吃蛇</title>
</head>
<style>
*{
  margin: 0;
  padding: 0;
}
  .map{
    width:800px;
    height: 600px;
    background-color: #ccc;
    position:relative;
  }

</style>
<body>
<!-- 画出地图,设置样式 -->
 <div class="map">

 </div>
</body>
<script src="food.js"></script>
<script src="snake.js"></script>
<script src="game.js"></script>
</html>

food.js


//食物就是一个对象,宽高横纵坐标,先定义构造函数,然后创建对象
(function (){
  var elements=[];//用来保存每个小方块食物
  function Food(x,y,width,height,color){
    //横纵坐标
    this.x=x||0;
    this.y=y||0;
    this.width=width||20;
    this.height=height||20;
    //背景颜色
    this.color=color||"green";
  }
  //为原型添加初始化的方法(作用:在页面上取显示这个食物)
  //因为食物要在地图上显示,所以,需要地图这个参数
  Food.prototype.init=function(map){
    //先删除这个食物
    //外部无法访问的函数
    remove()
    var div=document.createElement("div");
    //把这个div加到map中
    map.appendChild(div);
    //设置div的样式
    div.style.width=this.width+"px";
    div.style.height=this.height+"px";
    div.style.backgroundColor=this.color;
    // div.style.left=this.x+"px";

    //先脱离文档流
    div.style.position="absolute";
    //随机横纵坐标
    this.x=parseInt(Math.random()*(map.offsetWidth/this.width))*this.width;
    this.y=parseInt(Math.random()*(map.offsetHeight/this.height))*this.height;
    div.style.left=this.x+"px";
    div.style.top=this.y+"px";
    // Food.prototype.init=function(map){

    // }
    //把div加入到数组elements中
    elements.push(div);
  }

  function remove(){
    //elements数组中有这个食物
    for(var i=0;i<elements.length;i++){
      var ele=elements[i]
      //找到这个子元素的父级元素,然后删除这个子元素
      ele.parentNode.removeChild(ele);
      //再次把elements中的这个子元素也要删除
      elements.splice(i,1)
    }
  }
  //把Food暴露给Window,外部可以使用
  window.Food=Food;
}());

snake.js


//蛇
(function(){
  var elements=[];//存放小蛇的每个身体部分
  //蛇的构造函数
  function Snake(width,height,direction){
    //小蛇的每个部分的宽
    this.width=width||20;
    this.height=height||20;
    //身体
    this.body=[
      {x:3,y:2,color:"red"},
      {x:2,y:2,color:"orange"},
      {x:1,y:2,color:"orange"}
    ];

    this.direction=direction||"right";
  }
//蛇的初始化
  Snake.prototype.init=function(map){
    remove()
    //循环遍历创建div
    for(var i=0;i<this.body.length;i++){
      var obj=this.body[i];
      //创建div
      var div=document.createElement("div");
      //把div加入到map地图中
      map.appendChild(div);
      //设置div的样式;
      div.style.position="absolute";
      div.style.width=this.width+"px";
      div.style.height=this.height+"px";
      div.style.left=obj.x*this.width+"px";
      div.style.top=obj.y*this.height+"px";
      div.style.backgroundColor=obj.color;

      //把div加入到elements数组中--目的是删除
      elements.push(div)
    }
  }
  //蛇的移动
  Snake.prototype.move=function(food,map){
    //改变蛇身体位置
    var i=this.body.length-1; //2
    for(;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 "top":
        this.body[0].y-=1;
        break;
      case "bottom":
        this.body[0].y+=1;
        break;
    }

    //判断有没有吃到食物
    //小蛇的头的坐标和食物位置
    var headX=this.body[0].x*this.width;
    var headY=this.body[0].y*this.height;
    //食物的横纵坐标
    var foodX=food.x;
    var foodY=food.y;
    if(headX==foodX&&headY==foodY){
      //获取蛇的最后尾巴
      var last=this.body[this.body.length-1];
      //把最后的蛇尾复制一份
      this.body.push({
        x:last.x,
        y:last.y,
        color:last.color
      })
      //重新初始化食物
      food.init(map);
    }



  }

  //删除小蛇的私有函数
  function remove(){
    //获取数组
    var i=elements.length-1;
    for(;i>=0;i--){
      //先从当前的子元素中找到该子元素的父级元素,然后再弄死这个子元素
      var ele=elements[i];
      //从map地图上删除这个子元素div
      ele.parentNode.removeChild(ele);
      elements.splice(i,1);
    }
  }
  window.Snake=Snake;
}());

//蛇
(function(){
  var elements=[];//存放小蛇的每个身体部分
  //蛇的构造函数
  function Snake(width,height,direction){
    //小蛇的每个部分的宽
    this.width=width||20;
    this.height=height||20;
    //身体
    this.body=[
      {x:3,y:2,color:"red"},
      {x:2,y:2,color:"orange"},
      {x:1,y:2,color:"orange"}
    ];

    this.direction=direction||"right";
  }
//蛇的初始化
  Snake.prototype.init=function(map){
    remove()
    //循环遍历创建div
    for(var i=0;i<this.body.length;i++){
      var obj=this.body[i];
      //创建div
      var div=document.createElement("div");
      //把div加入到map地图中
      map.appendChild(div);
      //设置div的样式;
      div.style.position="absolute";
      div.style.width=this.width+"px";
      div.style.height=this.height+"px";
      div.style.left=obj.x*this.width+"px";
      div.style.top=obj.y*this.height+"px";
      div.style.backgroundColor=obj.color;

      //把div加入到elements数组中--目的是删除
      elements.push(div)
    }
  }
  //蛇的移动
  Snake.prototype.move=function(food,map){
    //改变蛇身体位置
    var i=this.body.length-1; //2
    for(;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 "top":
        this.body[0].y-=1;
        break;
      case "bottom":
        this.body[0].y+=1;
        break;
    }

    //判断有没有吃到食物
    //小蛇的头的坐标和食物位置
    var headX=this.body[0].x*this.width;
    var headY=this.body[0].y*this.height;
    //食物的横纵坐标
    var foodX=food.x;
    var foodY=food.y;
    if(headX==foodX&&headY==foodY){
      //获取蛇的最后尾巴
      var last=this.body[this.body.length-1];
      //把最后的蛇尾复制一份
      this.body.push({
        x:last.x,
        y:last.y,
        color:last.color
      })
      //重新初始化食物
      food.init(map);
    }



  }

  //删除小蛇的私有函数
  function remove(){
    //获取数组
    var i=elements.length-1;
    for(;i>=0;i--){
      //先从当前的子元素中找到该子元素的父级元素,然后再弄死这个子元素
      var ele=elements[i];
      //从map地图上删除这个子元素div
      ele.parentNode.removeChild(ele);
      elements.splice(i,1);
    }
  }
  window.Snake=Snake;
}());

game.js


//游戏
(function(){
   var that=null;
  //游戏的构造函数
  function Game(map){
    this.food=new Food();
    this.snake=new Snake();
    this.map=map;//地图
    that=this;
  }
  Game.prototype.init=function(){
    //初始化游戏
    //食物初始化
    this.food.init(this.map);
    this.snake.init(this.map);

    this.runSnake(this.food,this.map)
    this.bindKey();
  }

  Game.prototype.runSnake=function(food,map){
    //自动的去移动
    var timeId=setInterval(function(){
      //此时的this是window
      //蛇的移动
      this.snake.move(food,map);
      //初始化蛇
      this.snake.init(map);
      //横坐标最大值
      var maxX=map.offsetWidth/this.snake.width;
      //获取纵坐标的最大值
      var maxY=map.offsetHeight/this.snake.height;
      //蛇头的坐标
      var headX=this.snake.body[0].x;
      var headY=this.snake.body[0].y;
      //判断横坐标
      if(headX<0||headX>=maxX){
        clearInterval(timeId)
        alert("游戏结束")
      }
      //判断纵坐标
      if(headY<0||headY>maxY){
        clearInterval(timeId)
        alert("游戏结束")
      }
      console.log(headX)
    }.bind(that),150)
  }

  Game.prototype.bindKey=function(){
    //获取用户的按键,改变小蛇的方向
    document.addEventListener("keydown",function(e){
      //获取案件的值
      switch(e.keyCode){
        case 37:
          this.snake.direction="left";
          break;
        case 38:
          this.snake.direction="top";
          break;
        case 39:
          this.snake.direction="right";
          break;
        case 40:
          this.snake.direction="bottom";
          break;
      }
    }.bind(that),false)
  }


  window.Game=Game;

}());

//初始化游戏对象
var gm=new Game(document.querySelector(".map"));
gm.init()

此处插入一个关于匿名函数自调用(IIFE)的三种常见写法的小插曲。

第一种写法

项目代码结构与匿名函数自调用

第二种写法

项目代码结构与匿名函数自调用

第三种写法

项目代码结构与匿名函数自调用

特别注意:匿名函数表达式末尾必须加分号(;)。若遗漏,后续代码可能因语法解析错误引发难以排查的异常。

推荐采用第三种写法,因其语义清晰且易于阅读。

代码展示完毕后,接下来分析具体的实现思路。

第一步

创建游戏画布。

将画布宽度设置为800px,高度设置为600px。由于小蛇需要在画布内自由移动,需脱离标准文档流,因此给画布元素添加position:relative;样式,并设置背景色为灰色#ccc。

画布创建完成后,即可开始编写核心逻辑代码。

食物模块(Food.js)实现逻辑

Food.js 代码分析

首先定义贪吃蛇所需的“食物”对象,在food.js中创建一个自定义构造函数。

定义“食物Food”的x坐标、y坐标、宽度、高度及颜色属性。

项目代码结构与匿名函数自调用

利用逻辑或(||)运算符设置默认值:若左侧值为假,则取右侧值。因此,实例化对象时若未传参,将自动使用默认值。

在Food原型上定义init初始化方法。

首先创建一个div元素,并将该对象引用保存至div变量中。

将该div添加至地图容器中,并逐步设置其宽度、高度、背景颜色及绝对定位样式。

如何确定食物的具体位置?

将地图视为坐标系,用地图宽度除以食物宽度得到网格份数。x=1代表1个食物宽度的单位长度,x=3代表3个单位长度。

高度同理处理。

此处取随机数并乘以地图被切分的总份数,从而获得随机的 X 和 Y 坐标,再乘以宽度和高度即可得到不超出地图范围的随机坐标。例如:Math.random(0,5) 的结果不包括 5。

项目代码结构与匿名函数自调用

由于“食物”会被贪吃蛇吃掉,

必须创建一个方法来“销毁”食物,因此定义了一个remove函数。

同时,在上方创建了一个数组elements用于存放所有生成的“食物”div元素对象,便于后续删除。每次初始化食物时,将对象追加至elements数组。

项目代码结构与匿名函数自调用

遍历elements数组,通过每个div对象的父级元素,利用removeChild方法将其自身从DOM中移除项目代码结构与匿名函数自调用。

由于elements数组保存了引用,删除食物变得便捷。每次初始化前调用remove函数即可实现“消灭”旧食物并生成新食物的逻辑。

由于所有函数均封装在自调用的匿名函数中,内部的Food对象在外部无法直接访问。

如何解决?

通过调用window对象,将配置好的Food对象暴露给全局window,这样在其他模块中即可实例化Food。

小蛇模块(Snake.js)实现逻辑

Snake.js 代码分析

贪吃蛇身体的实现原理与食物类似。同样建立一个elements数组,用于存放小蛇移动时产生的旧“身体”对象,以便删除。由于均为局部变量,尽管两个数组同名,但不会发生冲突。

为小蛇设置宽、高,默认值与食物相同。同时设置方向属性direction,默认值为“right”(向右移动)。

游戏开始时,小蛇需具备初始长度。此处设置一个红色“脑袋”和两节身体,红色便于识别。

若需让小蛇变长,只需在body数组中追加对象即可体现“越吃越长”的效果。

项目代码结构与匿名函数自调用


小蛇基本属性配置完毕,下一步是初始化小蛇。

如前所述,elements数组用于存放“小蛇”的旧身体。初始化前需调用remove函数,遍历elements数组,采用与删除食物相同的方法清除旧蛇身。

项目代码结构与匿名函数自调用

执行删除操作后,即可专心进行初始化。

蛇身较长,如何确定每一节的具体位置?

利用上方定义的body数组,它存放了小蛇身体的所有部分。

遍历该数组,根据每个对象的属性创建新的div对象,统一设置其宽、高、left、top值。将创建好的对象再次存入elements数组,方便下一次删除。

项目代码结构与匿名函数自调用

定义初始化方法后,需考虑小蛇的移动实现。

作为贪吃蛇游戏,需与玩家互动,由玩家操控小蛇走向。

自定义构造函数中设置了direction属性,需利用此属性判断小蛇走向。

至于更改方向的操作,将在后续代码中实现。

定义move函数,传入两个参数:food(食物对象)和map(地图对象)。

为何需要传入这些参数?

虽然本Demo仅有一条小蛇,但此写法保留了同时运行多个游戏实例的可能性。

首先获取body数组长度存入i,然后倒序遍历i。根据i作为索引,从蛇尾巴向蛇脑袋遍历。想象贪吃蛇的移动过程:蛇身总是沿着脑袋走过的路径移动,无论绕直角或正方形,蛇身都会跟随。因此,每次移动只需控制蛇头,让蛇身各节获取前一节的坐标即可。

倒序遍历中,将第i节身体的x和y属性赋值给前一节身体。

蛇身坐标更新完成后,判断蛇头移动方向。由于只有4个固定方向,使用switch语句较为方便。

根据上、下、左、右不同情况,对头部的x和y进行增减操作。

贪吃蛇游戏的核心是吃食物,需实现该过程。

分别计算蛇头和食物的X和Y坐标。

进行判断:

当蛇头的x、y坐标与食物的x、y坐标完全相等时,

延长一节蛇身。此处复制最后一节蛇身体对象并追加至body数组。注意!

由于对象是引用类型,必须拆开赋值以避免引用共享问题。

最后,再次调用食物初始化方法,生成新的食物。

项目代码结构与匿名函数自调用

同样,将Snake对象暴露给window,供下方Game.js调用。

项目代码结构与匿名函数自调用

游戏控制模块(Game.js)实现逻辑

Game.js 代码分析

在Game.js开头定义变量that。

用于保存this的指向,供后续方法使用。

分别实例化“食物”对象和“贪吃蛇”对象。

传入地图对象并赋值。

项目代码结构与匿名函数自调用、

属性设置完毕。

既然是游戏,需设定游戏规则。

当小蛇到达地图边界时,视为撞墙死亡,游戏结束。

此外,尚未实现小蛇的移动逻辑,接下来继续实现。

定义runSnake函数,传入food和map对象。

首先定义一个计时器,存入timeId变量。

调用蛇的move(移动)和init(初始化)函数。

小蛇成功移动后,判断其是否到达边界。

计算地图宽度最多能被蛇头宽度分为几份,高度同理。

获取蛇头自身的x和y坐标。

判断:若蛇头x<0,说明越过左边界;若超过maxX,说明越过右边界。

y坐标判断同理。

若越过边界,清除定时器,并执行弹窗提示。

注意,在定时器方法后使用了bind并传入了预先定义的that(即保存的this指向)。若不这样做,由于setInterval内部this指向window,会导致代码错误,无法找到相关方法和属性。

项目代码结构与匿名函数自调用

接下来实现键盘控制小蛇移动的功能。

根据keycode更改snake对象的direction属性。

此处的this指向不正确,指向的是触发事件的对象,无法直接调用snake对象。因此必须改变指向,在bind中传入(that)。

将Game对象暴露给window。

项目代码结构与匿名函数自调用

接着定义初始化游戏的函数。

分别调用food对象的init函数、小蛇的init函数,并调用runSnake函数开启定时器使小蛇运行。

最后绑定keydown事件。

项目代码结构与匿名函数自调用

最后一步

实例化一个Game对象。

调用gm的init方法,贪吃蛇小Demo即实现。

项目代码结构与匿名函数自调用

效果展示

项目代码结构与匿名函数自调用

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript数组操作技巧总结》、《JavaScript排序算法总结》、《JavaScript遍历算法与技巧总结》、《JavaScript查找算法技巧总结》及《JavaScript错误与调试技巧总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章