js贪吃蛇|旨在理解“面向对象思想”的js练习

目录

  • 效果展示:
  • 功能简介:
    • 1.通关方法:
    • 2.操作方法:
    • 3.小蛇运动规则:
  • 制作思路
  • 代码特点
  • 代码书写
    • 0.结构搭建
    • 1.游戏场景搭建:先绘制每次都不变的样式(index.html,index.css)
      • ①html
      • ②CSS
    • 2.创建蛇对象(snake.js)
      • ①在构造器里添加蛇对象的属性:
      • ②在原型链上添加蛇对象的方法:
      • 为什么要在原型链上添加方法?
    • 3.创建食物对象(food.js)
      • ①在构造器里添加食物对象的属性:
      • ②在原型链上添加食物对象的方法:
    • 4.设计游戏的玩法(index.html)
      • ①创建游戏对象的构造函数
      • ②在游戏对象的构造函数里实例化蛇对象和食物对象
      • ③添加游戏对象的各种属性
      • ④将游戏的各种方法放入构造函数内
        • 开始游戏的方法
        • 暂停游戏的方法
        • 结束游戏的方法
        • 吃到食物后加速的方法&运动方向改变的方法
        • 长按方向键快速移动的方法
        • 绘制map地图和地图中的格子
    • 4.玩家操作游戏的代码
  • 细节优化
    • 出现问题1:
    • 出现问题2:
    • 出现问题3:
  • 总体回顾
    • 面向对象特点:

效果展示:

屏幕录制 2024-04-26 012419

功能简介:

1.通关方法:

  1. 小蛇吃到一个小球得五分,且运动速度会加快(最快不超过100毫秒)。当分数达到100分时,通关游戏。
  2. 可在未达到100分时手动结束游戏。

2.操作方法:

  • 上下左右键操控方向;
  • 按空格键暂停;
  • 按回车键继续游戏;
  • 长按方向键加速运动。

3.小蛇运动规则:

可:

 可撞到自己身体;
 可长按方向键加速运动。

不可:

 向右走的时候不能直接向左走,也就是说不能直接回头;
 不可撞墙,会死亡。

制作思路

  1. 绘制地图(蛇移动的区域);
  2. 绘制小蛇,并实现蛇对象相关的方法;
  3. 绘制食物,使其在随机位置出现;
  4. 设置游戏的玩法(开始,结束,暂停,死亡,通关,得分机制,游戏内容,难度变化,各种情况的提示框等);
  5. 绑定各个按钮、键盘按键的事件监听,实现人机交互。

代码特点

  • 可在一个页面集中修改、自定义地图的宽高,蛇的初始长度,初始速度,加速百分比,吃到一个食物的得分,最大速度,便于管理。

实现方法
通过给构造器传入实参的形式把死的参数写活。

  1. 在index.html页面里集中对参数进行赋值,并在实例化的时候传入实参。
  2. 在构造函数里用“this.形参名=形参”接收实参的值。
  3. 使用对象的方法时,因为“this.形参名”已有实参的值,所以无需再传入参数,直接使用“this.形参名”。
function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
    //传入可自定义的小蛇初始速度speed,吃到食物后速度增加的百分比percent,
    //吃到一个食物的得分scoreAdd,最大速度maxSpeed,地图的宽和高mapWidth、mapHight

   //用“this.形参名=形参”接收,“this.形参名”有了传入的实参的值
    this.scoreAdd = scoreAdd
    this.speed = speed
    this.cunt = 0
    this.over = 0
    this.timer = 0 //定时器id
    this.percent = percent
    this.maxSpeed = maxSpeed
    this.mapWidth=mapWidth
    this.mapHight=mapHight

    //对象的方法无需再传入参数
    this.speedUp = function () {
        //直接使用this.percent
        this.speed = this.speed * (this.percent / 100)
        clearInterval(this.timer)
        this.start(this.speed, this.percent, this.scoreAdd, this.maxSpeed)
       }
})
  1. 在index.html页面里集中对参数进行赋值
     //可以修改参数的值的地方
      const bodyLength=4
      const mapWidth=800     /* 需要是20的整数倍 */
      const mapHight=500      
      const speed=400
      const percent=90
      const scoreAdd=5
      const maxSpeed=100
      //实例化时传入参数
      const game = new Game(".map", ".score span", ".over", ".over div >span",".time span",speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight)

代码书写

0.结构搭建

在这里插入图片描述

1.游戏场景搭建:先绘制每次都不变的样式(index.html,index.css)

①html

<div class="over">
        <span></span>
        <div>得分: <span>0</span></div>

    </div>
    <div class="btnbox">
        <div>贪吃蛇</div>
        <button id="start">开始</button>
        <button id="pause">暂停</button>
        <button id="game-over">结束</button>
        <div class="score">得分: <span>0</span></div>
        
    </div>
    <div class="tips">
        <p>tips:</p>
        <p>按空格暂停</p><p>按回车开始</p>
        <p>上下左右控制方向</p>
        <p>通关条件:</p>
        <p>得分达到100分</p>
    </div>
    <div class="map">
      //这里还要通过js绘制一千个格子作为map的背景,
      //并且实现管理者可以自行修改map地图的大小和格子数
    </div>

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

②CSS

* {
 margin: 0;
 padding: 0;
}

html,
body {
 height: 100%;
}

body {
 background-color: #157c93;
 
}

.btnbox {
 position: fixed;
 width: 150px;
 height: 400px;
 /* background-color: wheat; */
 display: flex;
 flex-direction: column;
 justify-content: space-evenly;
 align-items: center;

}

.btnbox div {
 color: #e1e779;
 font-size: 30px;
 text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;

}

button {
 width: 100px;
 height: 50px;
 font-size: 23px;
 background: #2f3640;
 color: #F3F0E7;
 box-shadow: 0 0 5px #2f3640;
 text-shadow: 0px 0px 4px #fff;
}
.btnbox .score{
 color:#fff;
 font-size: 28px;
 text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
}
.map {
 position: fixed;
 left: 0;
 top: 0;
 right: 0;
 bottom: 0;
 margin: auto;
 background-color: #3B5666;
 width: 800px;
 height: 500px;
 border: #EDECE8 8px solid;
 /* border-radius: 10px; */
 box-shadow: 0 0 8px #F3F0E7 ;
}

.map>div {
 width: 20px;
 height: 20px;
 position: absolute;
 border-radius: 50%;
 box-shadow: 0px 0px 10px #96dfe7;
}



div.body {
 background: repeating-radial-gradient(#F3F0E7, #93d5dc);
}

div.head {
 left: 20px;

 background: repeating-radial-gradient(#93d5dc, #6bc1ca);
}
div.food {
 
 background: repeating-radial-gradient(#4293d5,#2775b6);
 animation: footAnimation 1s linear infinite;
}
@keyframes footAnimation {
 0%{
     box-shadow: 0 0 10px #8db8dc;
 }
 50%{
    box-shadow: 0 0 80px #206399;

 }
 100%{
     box-shadow:  0 0 10px #8db8dc;


 }
}
.over{
 display: none;
 width: 400px;
 height: 300px;
 position: fixed;
 left: 0;
 right: 0;
 top: 0;
 bottom: 0;
 margin: auto;/* 这几行能实现居中 */
 background-color: #EDECE8;
 color: #e1e779;
 font-size: 70px;
 z-index: 100;
 line-height: 220px;
 text-align: center;
 text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
 box-shadow: 0 0 10px #F3F0E7 ;
 border-radius: 8px;
}   
.over div{
 position: absolute;
 left: 50%;
 transform: translate(-50%,-10%);
 bottom: 0;
 line-height: 150px;
 color: #e1e779;
 text-shadow: 1px 1px 3px #000,0px 0px 4px #fff;
 font-size:35px;


}
.li{
 float: left;
 list-style: none;
 width: 20px;
 height: 20px;
 box-sizing: border-box;
 border: 1px rgba(5,27,32,.5) solid;
}
.tips{
 position: absolute;
 bottom: 10%;
 left: .3%;
 display: flex;
 flex-direction: column;
 align-items: center;
 width: 142px;
 color:#e1e779;
 border: 2px #e1e779 dashed;
 border-radius: 8% 0%;
}

2.创建蛇对象(snake.js)

①在构造器里添加蛇对象的属性:

function Snake(select,bodyLength) {
//传入一个地图元素,并捕获
  this.map = document.querySelector(select)
  //蛇运动的方向
  this.direction = "right"
  //蛇的数组(把蛇的头和身体都会存储到数组当中,头从数组的第0位开始)
  this.snakeList = []
  this.head = null
  this.pos = {}//坐标
  //接收传入的参数
  this.bodyLength=bodyLength
  //调用创建蛇的方法
  this.createSnake()
}

②在原型链上添加蛇对象的方法:

包含:1. 创建蛇头的方法
2. 创建蛇的方法
3. 蛇移动的方法
4. 判断是否吃到食物的方法
5. 判断是否撞墙的方法

Snake.prototype.createHead = function () {
    this.head = this.snakeList[0]
    //创建的新的小球的坐标
    this.pos = {
        x: 0,
        y: 0
    }

    if (this.head) {
        //如果有蛇头那么创建新的小球放到原先蛇头的后面坐标位置上,作为新的蛇头,之前的蛇头变为身体
        switch (this.direction) {
            case "left":
                this.pos.x = this.head.offsetLeft - 20;
                this.pos.y = this.head.offsetTop;
                break;
            case "right":
                this.pos.x = this.head.offsetLeft + 20;
                this.pos.y = this.head.offsetTop;
                break;
            case "top":
                this.pos.x = this.head.offsetLeft;
                this.pos.y = this.head.offsetTop - 20;
                break;
            case "bottom":
                this.pos.x = this.head.offsetLeft;
                this.pos.y = this.head.offsetTop + 20;
                break;
            default:
                break;
        }
        //把上一个创建的球的样式从蛇头改为蛇的身体样式
        this.head.className = "body"
    }
    const div = document.createElement("div") //创建蛇头
    this.map.appendChild(div)
    div.className = "head"
    this.snakeList.unshift(div)
    div.style.left = this.pos.x + "px"
    div.style.top = this.pos.y + "px"
}
//新增蛇的方法
Snake.prototype.createSnake = function () {
    for (let i = 0; i < this.bodyLength; i++) {
        this.createHead()
    }
}
//蛇移动的方法
Snake.prototype.move = function () {
    const body = this.snakeList.pop() //从数组中移除并返回最后一个元素
    body.remove() //从页面删除
    this.createHead()
}
//判断蛇有没有吃到食物
Snake.prototype.isEat = function (foodX, foodY) {
    const head = this.snakeList[0]
    const headX = head.offsetLeft
    const headY = head.offsetTop
    if (foodX === headX && foodY === headY) {
        return true
    }
    return false
}

//是否撞墙
Snake.prototype.isDie = function () {
    const head = this.snakeList[0]
    const headX = head.offsetLeft
    const headY = head.offsetTop
    if (headX < 0 || headY < 0 || headX >= this.map.clientWidth || headY >= this.map.clientHeight) {
        return true
    }
    return false
}

为什么要在原型链上添加方法?

  • 将方法写在原型链上,可以使得所有由Snake 构造函数创建的实例都可使用写在函数原型上的方法(如:move方法),而不需要每个实例都有自己的 move 方法。
  • 同时这样能使创建对象的时候不创建方法,而是调用的时候才创建方法。

3.创建食物对象(food.js)

①在构造器里添加食物对象的属性:

function Food(select)  {
        //***小栗子 */
        /* this指的是实例化的对象,指向的是那个实例*/
        //传入一个地图元素,并捕获
        this.map = document.querySelector(select)
        //创建食物元素
        this.food = document.createElement("div")
        //这样子就能把食物元素放到地图元素中
        this.map.appendChild(this.food)
        //定义样式
        this.food.className = "food"

      
        this.x = 0
        this.y = 0
        this.foodPos()
    }

②在原型链上添加食物对象的方法:

随机在某一位置生成食物的方法:

Food.prototype.foodPos=function() {
        //1.将长和宽划分为...份//为什么要除以20再乘以20是为了让他出现在格子上而不是两个格子中间
        const w = this.map.clientWidth / 20
        const h = this.map.clientHeight / 20
        //2.随机生成数字
        let n1 = Math.floor(Math.random() * w) //相当于总的份数乘一个百分比,就可以得到0-w范围内的数字
        let n2 = Math.floor(Math.random() * h)
        //3.将坐标转化为长度(因为食物的位置是要具体的px)
        this.x = n1 * 20
        this.y = n2 * 20
        //4定位
        this.food.style.left = this.x + "px"
        this.food.style.top = this.y + "px"
    }

4.设计游戏的玩法(index.html)

因为游戏只有一个,所以游戏对象的方法没有写在原型链上

①创建游戏对象的构造函数

function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
//创建游戏对象的构造函数
}

②在游戏对象的构造函数里实例化蛇对象和食物对象

目的:这样就可以通过food实例和snake实例使用对象里的属性和方法。

function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
this.food = new Food(select)
this.snake = new Snake(select,bodyLength)
})

③添加游戏对象的各种属性

function Game(select, score, gameover, finalScore, time,speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight) {
   //实例化
   this.food = new Food(select) 
   this.snake = new Snake(select,bodyLength)
   //捕获元素
   this.map = document.querySelector(select)
   this.gameover = document.querySelector(gameover)
   this.score = document.querySelector(score)
   this.time = document.querySelector(time)
   this.finalScore = document.querySelector(finalScore)
   this.startBtn = document.querySelector("#start")
   this.ending = document.querySelector(".over span")
   this.map = document.querySelector(".map")
   this.ul = document.createElement("ul")  
   this.cunt = 0
   this.over = 0
   this.timer = 0 //定时器id
   //接收传入的参数
   this.scoreAdd = scoreAdd
   this.speed = speed
   this.percent = percent
   this.maxSpeed = maxSpeed
   this.mapWidth=mapWidth
   this.mapHight=mapHight
   })

④将游戏的各种方法放入构造函数内

包含:

  1. 开始游戏的方法
  2. 暂停游戏的方法
  3. 结束游戏的方法
  4. 吃到食物后加速的方法
  5. 运动方向改变的方法
  6. 长按方向键快速移动的方法
  7. 绘制map地图和地图中的格子
开始游戏的方法
this.start = function () {
        //(细节优化)禁用开始、结束按钮
        this.startBtn.disabled = true
        //(细节优化)添加方向键的事件监听,使得可以通过方向键操控运动方向
        document.addEventListener("keydown", keyDownDirection)
        //(细节优化)如果出现游戏结束的提示框,则重新加载页面
        if (this.gameover.style.display === "block") {
            window.location.reload() //***重新加载了会停止执行代码,后面的代码不会执行
        }
        this.timer = setInterval(() => {
            this.snake.move()
            if (this.snake.isEat(this.food.x, this.food.y)) {
                //吃到食物要变长,调用增加蛇头的方法id
                this.snake.createHead()
                this.food.foodPos() //更新食物位置
               //更改得分情况
                this.cunt += this.scoreAdd
                this.score.innerText = this.cunt
                if (this.speed >= maxSpeed) {
                    this.speedUp()
                }
    
            }
            //得分满100游戏结束
            if (this.cunt === 100) {
                this.gameOver()
            }
            if (this.snake.isDie()) {
                clearInterval(this.timer)
                this.gameOver()
            }
        }, `${ this.speed}`);
    
        
    }
暂停游戏的方法
this.pause = function () {
     clearInterval(this.timer)
    //(细节优化)不禁用开始、结束按钮
     this.startBtn.disabled = false
     //(细节优化)添加方向键的事件监听,使得可以通过方向键操控运动方向
     document.removeEventListener("keydown", keyDownDirection)
 }
结束游戏的方法
this.gameOver = function () {
      //出现提示框
     this.gameover.style.display = "block"
     clearInterval(this.timer)
     //写入得分
     this.finalScore.innerText = this.cunt
     //(细节优化)不禁用开始、结束按钮
     this.startBtn.disabled = false
     console.log(this.speed)
     document.removeEventListener("keydown", keyDownDirection)
     //三种不同的情况显示不同的文字,分别为通关,死亡,玩家主动点击结束按钮(默认)
     //1(默认)
     this.ending.innerText = "游戏结束"
     //2
     if (this.cunt === 100) {
         this.ending.innerText = "恭喜通关"
     }
     //3
     if (this.snake.isDie()) {
         this.ending.innerText = "游戏失败"
     }
 }
吃到食物后加速的方法&运动方向改变的方法
this.speedUp = function () {
       this.speed = this.speed * (this.percent / 100)
       clearInterval(this.timer)
       this.start(this.speed, this.percent, this.scoreAdd, this.maxSpeed)
   }
   this.change = function (type) {
       this.snake.direction = type
   }
长按方向键快速移动的方法

思路:

  • 一开始的时候一直在想改变定时器时间的事情,后面发现可以通过在按方向键的时候调用一个移动的函数。
  • 这个函数里必须包括吃到小球后得分,变长,死亡,游戏通关等游戏的基本规则的代码,否则小蛇吃到小球后不得分,也不变长,撞墙也不会死亡。

这样也能够使快速交替按左键和上键的时候,小蛇运动更灵活,不会像原本一样受定时器的限制。(因为定时器有时间500毫秒,如果快速按两次时,间隔时间小于500,则会忽略前一次。但修改后不会,只要按方向就一定会改变,小蛇运动更灵活,体验感会更好。)

    this.quicklyMove = function () {
        this.snake.move()
        if (this.snake.isEat(this.food.x, this.food.y)) {
            //吃到食物要变长,调用增加蛇头的方法id
            this.snake.createHead()
            this.food.foodPos() //更新食物位置
            this.cunt += this.scoreAdd
            this.score.innerText = this.cunt
            if (this.speed >= this.maxSpeed) {
                this.speedUp()
            }
    
        }
        if (this.cunt === 100) {
            this.gameOver()
        }

        if (this.snake.isDie()) {
            clearInterval(this.timer)
            this.gameOver()
        }
    }
绘制map地图和地图中的格子
this.createCell = function () {
     const cellNumber=(this.mapWidth/20)*(this.mapHight/20)
     this.map.style.width=this.mapWidth+"px"
     this.map.style.hight=this.mapHight+"px"
     for (let i = 0; i < cellNumber; i++) {
         let li = document.createElement("li")
         li.className = "li"
         this.ul.appendChild(li)
     }
     this.map.appendChild(this.ul)
 }

4.玩家操作游戏的代码

  • 写在index.html页面中
  • 在这里面实例化游戏对象
<script>
       //管理者可在这里统一修改参数
       const bodyLength=4
       const mapWidth=800   /* 需要是20的整数倍 */
       const mapHight=500      
       const speed=400
       const percent=90
       const scoreAdd=5
       const maxSpeed=100
       
       const game = new Game(".map", ".score span", ".over", ".over div span",".time span",speed,percent,scoreAdd,maxSpeed,mapWidth,mapHight)
       const start = document.querySelector("#start")
       const pause = document.querySelector("#pause")
       const gameOver = document.querySelector("#game-over")
     
       //调用绘制地图和格子的函数
       game.createCell()
       //按钮的事件监听
       start.onclick = function () {
           game.start()//传入初始速度,时间变快的百分比
           this.blur()//(细节优化:点击按钮后按钮失去焦点)
                   //(如果不写,按空格会再点击一次按钮)
       }
       pause.onclick = function () {
           game.pause()
           this.blur()
       }
       gameOver.onclick = function () {
           game.gameOver()
           this.blur()
       }

       //方向键的事件监听
       //键盘监听左37上38右39下40
       function keyDownDirection(e) {
           switch (e.keyCode) { //获取键对应的数字
               case 37:
                   if (game.snake.direction !== "right") {
                       game.change("left")
                       game.quicklyMove()
                   }
                   break;
               case 38:
                   if (game.snake.direction !== "bottom") { //不能直接用snake.direction,因为这个页面没有实例化snake实例化了game,
                       //需要通过game引入,全部都在game里面实例化了
                       game.change("top")
                       game.quicklyMove()
                   }

                   break;
               case 39:
                   if (game.snake.direction !== "left") {
                       game.change("right")
                       game.quicklyMove()
                   }

                   break;
               case 40:
                   if (game.snake.direction !== "top") {
                       game.change("bottom")
                       game.quicklyMove()
                   }
                   break;
               default:
                   break;
           }
       }
      
       //开始、结束键的事件监听
       function keyDownStartPause(e) {
           switch (e.keyCode) { //获取键对应的数字
               case 13:
                   if (!game.startBtn.disabled) {

                       game.start()
                   }
                   break;
               case 32:
                   game.pause()
                   break;
               default:
                   break;
           }
       }
       //(细节优化)在这里绑定开始结束键的事件监听,却不绑定方向键的事件监听,(而是在游戏开始的函数里才绑定),是为了防止游戏为开始时按动方向键,开始后会直接往那个方向走
        document.addEventListener("keydown", keyDownStartPause)
   </script>

细节优化

本文前面写的代码为已优化的代码,在优化的地方均已添加//细节优化的注释,可配合查看,方便理解。

出现问题1:

点击开始之后,若再次点击开始,会出现定时器叠加的问题。

  • 解决方法
  1. 在开始方法里解除开始按钮的事件监听;
  2. 在暂停和结束方法里,加上开始按钮的事件监听。

出现问题2:

暂停游戏后,若再按方向键,开始后会直接往那个方向运动。

  • 解决方法
  1. 在暂停游戏的方法里解除方向键的事件监听;
  2. 在暂停和结束方法里,加上方向键的事件监听。

出现问题3:

页面加载完成但是没有点击开始前,若按动方向键,点击开始后小蛇会直接朝该方向运动。

  • 解决方法
    不在人机交互页面(也就是index HTML页面,添加方向键的事件监听),而是在开始函数里添加。

总体回顾

面向对象特点:

1.可以通过类(类和构造器是一个东西)将一个对象的属性和方法进行封装。
2.在一个页面中实例化一个对象,就可以通过这个对象调用他的方法。
3.在对象的页面里再实例化,就可以通过第一个对象调用第二个对象再调用它的方法。(如game.snake.direction !== "top")【本文的代码是在HTML页面实例化游戏类,游戏类中实例化蛇和食物。】

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值