贪吃蛇游戏实现思路、优化方法与源码

921 阅读8分钟

贪吃蛇游戏

  • 游戏的目的是用来体会 JavaScript 高级语法的使用,暂时不需要具备抽象对象的能力。使用面向对象的方式分析问题,需要一个漫长的积累过程。

搭建页面

  • 一个容器盛放游戏场景 div#stage,设置样式.

分析对象

  • 食物对象 Food
  • 蛇对象 Snake
  • 游戏对象 Game

创建工具类

  • 实现随机数、随机颜色方法

创建食物对象

  • 创建 Food 的构造函数,并设置属性
    • 位置 x、y(实际位置)
    • 宽度和高度 width、height
    • 颜色
    • 食物集合
    • 定位:绝对定位
  • 通过原型设置方法
    • render 随机渲染一个食物对象,并添加到stage上
  • 通过自调用函数,进行封装,并通过window暴露Food构造函数。

创建蛇对象

  • 创建 Snake 的构造函数,并设置属性
    • 宽度和高度 width、height
    • body 数组,蛇的头部和身体,第一个位置是蛇头
    • direction 蛇运动的方向
    • 定位:绝对定位
  • 通过原型设置方法
    • render 随机创建一个蛇对象,并添加到stage上
  • 通过自调用函数,进行封装。并通过window暴露Snake构造函数。

创建游戏对象

  • 创建 Game 的构造函数,并设置属性
    • food
    • snake
    • stage
  • 通过原型设置方法
    • start 开始游戏(绘制所有游戏对象,渲染食物对象和蛇对象)
  • 通过自调用函数,进行封装,通过 window 暴露 Game 构造函数

游戏逻辑

游戏逻辑1

  • 蛇的move方法
    • 在蛇对象 (snake.js) 中,在 Snake 的原型上新增 move 方法
    • 1.让蛇移动起来,把蛇身体的每一部分往前移动一下
    • 2.蛇头部分根据不同的方向决定 往哪里移动(+1)

游戏逻辑2

  • 让蛇自己动起来
    • 在 snake 中添加删除蛇的私有方法,在 render 中调用。
    • 在 game.js 中添加 runSnake 的私有方法,开启定时器调用蛇的 move 和 render 方法,让蛇动起来。
    • 判断蛇是否撞墙。
    • 在 game.js 中添加 bindKey 的私有方法,通过键盘控制蛇的移动方向,在 start 方法中调用 bindKey。

游戏逻辑3

  • 判断蛇是否吃到食物
    • 在 Snake 的 move 方法中判断在移动的过程中蛇是否吃到食物。
    • 如果蛇头和食物的位置重合代表吃到食物。
    • 食物的坐标是像素,蛇的坐标是几个宽度,需要进行转换
    • 吃到食物,往蛇节的最后加一节
    • 最后把现在的食物对象删除,并重新随机渲染一个食物对象
  • 升级为多个食物时,需要循环遍历查看是否重合,再随机渲染一个食物对象

其他处理

其他处理1

  • 将测试执行的代码,整合到main.js文件中,并设置其为自调用函数,使js脱离html。
  • 注意:引用js文件名的时候,要注意顺序,后引用的可以使用之前引用的文件,反之不行

其他处理2

  • 运行游戏后,在控制台network下可以看到HTTP向服务器请求的文件数
    • 如下图(其中有5次js请求文件)
  • 浏览器的性能优化中,包含一个减少发送http请求的条数,每多发送一个http请求,就要多在服务器里面请求一条数据,然后再去返回。
  • 解决方法:将所有的js文件整合到一个js文件中,命名为:index.js
    • 再次运行查看,如图(只请求了1次js文件)
    • 注意:在将js文件整合时,需要注意:每个js文件的先后顺序

其他处理3

  • js代码压缩优化,可以使传输时效率更快
    • 搜索:代码压缩
    • 压缩后文件命名:index.min.js
  • 在vscode中,查看选项卡 -> 自动切换换行

其他处理4

  • 自调用函数必须加分号。
    • 问题
    // 两种情况都是:控制台输出1,之后会报错
    // 第一种情况
    // 因为第一个执行完后,有一个返回值:默认为undefined
    // undefined(function (){}) 进行函数调用,就报错
    (function (){
          console.log("1");
      })()
      (function (){
          console.log("2");
      })()
    
      // 第二种情况
      // 如果函数作为事件或者函数表达式方式,
      // 在后面写的自调用函数也要注意
      var fun = function (){
          console.log("1");
      }
      (function (){
          console.log("2");
      })()
      // 其会看成 var fun = function (){}(function (){})
      // 即:var fun = function (){}()  undefined() -> 报错
    
    • 1.养成习惯,在函数末尾添加;
    (function (){
    })();
    
    • 2.在函数前添加; 表示前一段语句已经结束
    ;(function (){
    })()
    

其他处理5

  • 自调用函数的参数
    • 传入 window 对象:将来代码压缩的时候,可以把 function(window) 压缩成 function(w)。避免了跳出作用域去全局作用域查找window
    • 传入 undefined
    • 写的代码中会把 undefined 作为函数的参数(当前案例没有使用)。因为在有的老版本的浏览器中 undefined 可以被重新赋值,防止 undefined 被重新赋值。
      // IE8可以重新赋值
      undefined = "llll";
      console.log(undefined); // llll
    
      // 自调用函数参数
      (function (window,undefined){
      })(window,undefined);
    

其他处理6

  • 在遍历数组时,如果正向遍历,循环删除数组中的值,会出错
  • 解决方法,如下
// 从舞台中清除蛇
  for(var i = this.elements.length - 1; i >= 0; i--){
      stage.removeChild(this.elements[i]);
  }
  // 清空数组
  this.elements = [];

源码

index.html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪吃蛇游戏</title>
    <link rel="stylesheet" href="css/index.css">
</head>
<body>
    <div class="stage" id = "stage"></div>
    <!-- js文件书写的位置:后面可以用前面引用的文件内容 -->
    <!-- <script src="js/tools.js"></script>
    <script src="js/food.js"></script>
    <script src="js/snake.js"></script>
    <script src="js/game.js"></script>
    <script src="js/main.js"></script> -->
    <!-- 简化http向服务器请求文件数,将其他的整合到一个文件中 -->
    <script src="js/index.min.js"></script>
</body>
</html>
<!-- 疑问:为什么清除定时器之后,还会往前走一格 -->

index.css文件

/* 清除默认样式 */
* {
    margin: 0;
    padding: 0;
}
/* 给舞台添加样式 */
.stage {
    width: 800px;
    height: 600px;
    background-color: lightgray;
    position: relative;
}

index.js文件

// 整合所有的js文件
// =========================工具类=========================
// 制作一个工具对象,内部添加多种工具的方法
(function (window,undefined){
    var Tools = {
        getRandom: function (min,max){
            // 向上取整
            min = Math.ceil(min);
            // 向下取整
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
        },
        getColor : function (){
            // 颜色参数是rgb(r,g,b)
            var r = this.getRandom(0,255);
            var g = this.getRandom(0,255);
            var b = this.getRandom(0,255);
            // 返回的时候:进行拼接
            var rgb = "rgb(" + r + "," + g + "," + b  + ")";
            // 返回颜色值
            return rgb;
        }
    }
    window.Tools = Tools;
})(window,undefined);

// ===========================food食物=======================
(function (window,undefined){
    // 分析食物:位置x,y 宽高 颜色
    function Food(option){
        // 判断用户输入的是否正确
        // option = option || {};
        option = option instanceof Object ? option : {};
        // 传入的数据可能是类似数组等对象,所以需要进一步判断
        this.x = option.x || 0;
        this.y = option.y || 0;
        this.width = option.width || 20;
        this.height = option.height || 20;
        this.color = option.color || "blue";   
        // 如果后期难度加大,相当于一次出现三个小方块,就需要保存下
        this.elements = [];
    }
    
    // 定义全局变量
    // 将函数内部赋值后,不会改变的值,放在外面声明下更好
    var bs = "absolute";
    // 共享的方法:随机变换位置、颜色
    // 渲染一个元素到页面上,需要添加到原型对象的方法中
    Food.prototype.render = function(stage){
        // 创建一个新的div元素
        var ele = document.createElement("div");
        // 每次设置样式之前,都随机获取一个x和y的值
        this.x = Tools.getRandom(0,stage.clientWidth / this.width - 1) * this.width;
        this.y = Tools.getRandom(0,stage.clientHeight / this.height - 1) * this.height;
        // 添加对应的样式
        ele.style.left = this.x + "px";
        ele.style.top = this.y + "px";
        ele.style.width = this.width + "px";
        ele.style.height = this.height + "px";
        ele.style.backgroundColor = Tools.getColor();
        ele.style.position = bs;
        // 将新元素添加到父元素
        stage.appendChild(ele);
        // 将新元素添加到数组中,方便后期进行调用删除
        this.elements.push(ele);
    }
    
    // 当贪吃蛇碰到一个食物的时候,就应该让其消失
    // 即:从数组中删除
    Food.prototype.remove = function (stage,i){
        //从stage舞台上将其移除
        stage.removeChild(this.elements[i]);
        //从数组中将其移除
        this.elements.splice(i,1);
    }
    // 利用window对象暴露Food函数可以给外部使用
    // 相当于在外部找到了一个指针,指向它
        window.Food = Food;
    })(window,undefined);
// ===================蛇=====================
// 自调用函数
(function (window,undefined){
    var bs = "absolute";
    // 创建蛇对象
    function Snake(option){
        option = option instanceof Object ? option : {};
        // 每个蛇节的宽、高
        this.width = option.width || 20;
        this.height = option.height || 20;
        // 蛇身体
        this.body = [
            {x:3,y:2,color:"red"},
            {x:2,y:2,color:"blue"},
            {x:1,y:2,color:"blue"}
        ]; 
        this.position = bs;
        this.direction = "right";
        this.elements = [];
    }
    // 渲染出来蛇对象
    Snake.prototype.render = function (stage){
        // 优化写法:没必要定义变量,因为for循环,第一句原本就只执行一次
        // var len = this.body.length;
        for(var i = 0,len = this.body.length;i < len; i++){
            // 创建一个对象
            var ele = document.createElement("div");
            // 赋样式值
            ele.style.width = this.width + "px";
            ele.style.height = this.height + "px";
            ele.style.left = this.body[i].x * this.width + "px";
            ele.style.top = this.body[i].y * this.height + "px";
            ele.style.backgroundColor = this.body[i].color;
            ele.style.position = this.position;
            // 添加到舞台上
            stage.appendChild(ele);
            // 添加到数组的末尾
            this.elements.push(ele);
        }
    }
    // 蛇运动
    Snake.prototype.move = function (){
        // 蛇身循环
        for(var 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;
        }
        // 蛇头通过位置加减
        var head = this.body[0];
        // console.log(this.direction);
        switch(this.direction){
            case "right":
                head.x++;
                break;
            case "left":
                head.x--;
                break;
            case "top":
                head.y--;
                break;
            case "bottom":
                head.y++;
                break;
        }
    }
    // 移除之前渲染的蛇
    Snake.prototype.remove = function (stage){
        
        // 从舞台中清除蛇
        for(var i = this.elements.length - 1; i >= 0; i--){
            stage.removeChild(this.elements[i]);
        }
        // 清空数组
        this.elements = [];
    }
    // 通过window暴露属性
    window.Snake = Snake;
})(window,undefined);
// =====================游戏=======================
// 自调用函数
(function (window,undefined){
    // 全局变量:存储当前的game实例对象
    var that;
    function Game(stage){
        // 食物
        this.food = new Food();
        // 蛇
        this.snake = new Snake();
        // 舞台
        this.stage = stage;
        that = this;
    }

    // 游戏逻辑
        // 1.蛇往前走(定时器)
        // 2.走到边界,游戏结束
        // 3.吃到食物
        // 4.食物消失,蛇变长
    Game.prototype.start = function (){
        this.food.render(this.stage);
        this.food.render(this.stage);
        this.food.render(this.stage);
        this.snake.render(this.stage);
        // 1.蛇往前走(定时器)
        snakeRun();
        // 3.吃到食物
        bindKey();

    }
    // 通过按键控制蛇吃食物
    function bindKey() {
        // 给文档绑定键盘按下事件
        document.onkeydown = function (e){
            // console.log(e.keyCode);
            // 上:38 下:40
            // 右:39 左:37
            // var dir = that.snake.direction;
            switch(e.keyCode){
                case 38:
                    that.snake.direction = "top";
                    break;
                case 40:
                    that.snake.direction = "bottom";
                    break;
                case 39:
                    that.snake.direction = "right";
                    break;
                case 37:
                    that.snake.direction = "left";
                    break;
            }
        };
    }
    // 蛇运动
    function snakeRun(){
        var timer = setInterval(function (){
            // 1.蛇往前走(定时器)
            // this.snake.move();
            // 移除之前渲染的蛇
            // this.snake.remove(this.stage);
            // 渲染蛇
            // this.snake.render(this.stage);
            // 此时出现了问题:上一次渲染的蛇和这次渲染的重叠了
            // 正常来说,应该:这次渲染后,上次的就应该消失
            // 直接将代码粘贴过来执行,行不通,因为此时的this不再是game实例对象
            that.snake.move();
            that.snake.remove(that.stage);
            that.snake.render(that.stage);
            
            // 每次移动都需要判断下,是否到达边界
            var maxX = that.stage.offsetWidth / that.snake.width;
            var maxY = that.stage.offsetHeight / that.snake.height;
            // 待判断的数值
            var headX = that.snake.body[0].x;
            var headY = that.snake.body[0].y;
            // 每次移动一个新位置的时候,都需要判断下是否吃到了食物
            // 即:蛇头和食物的位置是否一致
            // var foodX = that.food.x;
            // var foodY = that.food.y;
            var hX = headX * that.snake.width;
            var hY = headY * that.snake.height;
            // 有三个食物,因此需要for循环遍历
            for(var i = 0; i < that.food.elements.length; i++){
                if(that.food.elements[i].offsetLeft == hX && that.food.elements[i].offsetTop == hY){
                    // 清除食物
                    // 问题:当食物的数量增加时,就不能固定传下标0
                    that.food.remove(that.stage,i);
                    // 再随机生成一个食物
                    that.food.render(that.stage);
                    // 添加一个新的蛇节
                    var last = that.snake.body[that.snake.body.length - 1];
                    that.snake.body.push({
                        x:last.x,
                        y:last.y,
                        color:last.color
                    });
                }
            }
            // 2.走到边界,游戏结束
            if(headX < 0 || headX >= maxX || headY < 0 || headY >= maxY){
                // 停止定时器
                clearInterval(timer);
                alert("Game over");
            }
        },150);
    }
    // 将构造函数通过window暴露
    window.Game = Game;
})(window,undefined);
// ==================main================
// 将执行代码放在单独的js页面中
(function (window,undefined){
    var stage = document.getElementById("stage");
    var game = new Game(stage);
    game.start();
})(window,undefined);
// 压缩文件会:1.去除注释、空格、换行 2.简化标识符