如何用原生JS,快速写一个贪吃蛇小游戏。

·  阅读 3278
如何用原生JS,快速写一个贪吃蛇小游戏。

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

前言

贪吃蛇算是小游戏里面比较好写的,没有什么难点,基本上需要实现的功能,都能很顺利的用代码敲出来。

1655299027951.gif

1.绘制游戏区域和游戏元素

仍然是用16*16的二维数组来绘制,对这个数组进行遍历。第一层遍历的时候创建tr,第二层遍历的时候创建td。然后添加一些CSS样式,游戏区域就写好了。

let arr = [  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
]
//渲染游戏区域
const renderTable = () => {
  document.querySelector('table').innerHTML = ''
  arr.forEach(item => {
    //第一层遍历创建tr
    let tr = document.createElement('tr')
    item.forEach(item2 => {
      //第二层遍历创建td
      let td = document.createElement('td')
      tr.appendChild(td)
    })
    document.querySelector('table').appendChild(tr)
  })
}
renderTable()
复制代码

CSS&HTML

 <style>
    table {
      margin: 0px auto;
      border-collapse: collapse;
      border: 5px solid black;
    }
    td {
      border-radius: 50%;
      width: 40px;
      height: 40px;
      border: none
    }
    .bgc1 {
      background-color: black;
    }
    .bgc2 {
      background: url(./pngsucai_512252_f77802.png) no-repeat;
      background-size: 100%;
      z-index: 1;
    }
    .bgc3 {
      background: url(./92034446ddc7a91edec038f2d69415fd.jpeg)no-repeat;
      background-size: 100%;
      z-index: 9;
    }
    .defen {
      font-size: 30px;
      position: absolute;
      top: 40%;
      right: 20px;
    }
  </style>
  <body>
  <table></table>
  <div class="defen">
    键盘上下左右控制,Enter键暂停
    <br>
    <br>
    得分
    <br>
    <span>0</span>
  </div>
  <script src="./贪吃蛇2.0.js"></script>
</body>
复制代码

游戏元素的话一共有3个,蛇头,身体和苹果。就用3个构造函数来生成坐标,以及给对应坐标的那个对象里面添加不同的属性。用构造函数写既方便查找,也方便修改。然后写个渲染函数,格子里面对应的不同的属性,就渲染出不同的样式。

//渲染样式的函数
const renderColor = () => {
  arr.forEach((item, index) => {
    const trArr = document.querySelectorAll('tr')
    item.forEach((item2, index2) => {
      //头部渲染
      if (item2.head === 1) {
        trArr[index].querySelectorAll('td')[index2].classList.add('bgc3')
      }
      //身体渲染成黑色
      else if (item2.body === 1) {
        trArr[index].querySelectorAll('td')[index2].classList.remove('bgc3')
        trArr[index].querySelectorAll('td')[index2].classList.remove('bgc2')
        trArr[index].querySelectorAll('td')[index2].classList.add('bgc1')
      }
      //苹果渲染
      else if (item2.apple === 1) {
        trArr[index].querySelectorAll('td')[index2].classList.add('bgc2')
      }
      //如果是其他的值,就把这个样式清空
      else {
        trArr[index].querySelectorAll('td')[index2].className = ''
      }
    })
  })
}
//蛇头构造函数
function Head(x, y) {
  this.x = x
  this.y = y
  this.render = function (a) {
    arr[this.y][this.x].head = a
  }
}
//身体构造函数
function Body(x, y) {
  this.x = x
  this.y = y
  this.render = function (a) {
    arr[this.y][this.x].body = a
  }
}
//苹果构造函数
function Apple(x, y) {
  this.x = x
  this.y = y
  this.render = function (a) {
    arr[this.y][this.x].apple = a
  }
}
//依次渲染默认出现的头部,身体和苹果
let a = new Head(10, 10)
a.render(1)
//声明一个存放身体元素的数组,移动以及判断获胜都需要用到这个数组的功能
let bodyArr = []
let b = new Body(10, 11)
bodyArr.push(b)
b.render(1)
let c = new Apple(5, 5)
c.render(1)
renderColor()
复制代码

Snipaste_2022-06-15_20-59-49.jpg 图片是我在网上随便找的

2.移动功能

移动功能要分两个步骤来写。一个是蛇头的移动,一个是身体的移动。贪吃蛇的身体它不是一个整理,是不能写成一块的。蛇头动的时候,身体它得扭来扭去,这才像个蛇。

蛇头移动很简单,上下左右键对应着蛇头X和Y两个值的加减。X和Y超出范围,代码就会报错。就可以直接用try catch来判断边界。报错了就说明出界了,直接走catch的游戏结束。

02jpg.jpg

这个游戏唯一麻烦一点的地方来了,怎么让蛇身体能扭起来。相通一个逻辑,这个问题就迎刃而解了。

蛇身体怎么移动,是身体里的每个元素都往前移动一格吗,显然不是。仔细观察你会发现,蛇移动时,身体的中间部分其实是不动的。动的只有最前端和最末端的两格。也就是说蛇移动时,其实就是把身体最末端的格子移动到了身体最前端,其他的都不需要动。前面声明的身体元素数组就是这个时候用的。把身体的最后一个元素移动到头部,同时数组里的最后一个元素也要移动到最前面去。

03.jpg

//上下左右移动函数
const up = () => {
  //用try catch来判断是否出界
  try { //把移动的函数写在try里面,如果出界了就会报错,然后走catch里的代码
    //移动的时候先清除当前格子的样式
    a.render(0)
    a.y -= 1
    //然后渲染新样式
    a.render(1)
  } catch {
    clearInterval(timer)
    alert('游戏结束!')
    location.reload()
  }
  //调用吃苹果函数
  eat()
  //让数组中的最后一个元素,移动到头部刚才所在的位置
  bodyArr[bodyArr.length - 1].render(0)
  //这个a.x,a.y+1就是头部移动前的坐标
  bodyArr[bodyArr.length - 1].x = a.x
  bodyArr[bodyArr.length - 1].y = a.y + 1
  bodyArr[bodyArr.length - 1].render(1)
  //把身体数组里的最后一个元素移到最开头
  bodyArr.unshift(bodyArr.pop())
  renderColor()
  //调用判断游戏结束函数
  end()
}
const down = () => {
  try {
    a.render(0)
    a.y += 1
    a.render(1)
  }
  catch {
    clearInterval(timer)
    alert('游戏结束!')
    location.reload()
  }
  eat()
  //让数组中的最后一个元素,移动到头部刚才所在的位置
  bodyArr[bodyArr.length - 1].render(0)
  bodyArr[bodyArr.length - 1].x = a.x
  bodyArr[bodyArr.length - 1].y = a.y - 1
  bodyArr[bodyArr.length - 1].render(1)
  bodyArr.unshift(bodyArr.pop())
  renderColor()
  end()
}
const right = () => {
  try {
    a.render(0)
    a.x += 1
    a.render(1)
  }
  catch {
    clearInterval(timer)
    alert('游戏结束!')
    location.reload()
  }
  eat()
  //让数组中的最后一个元素,移动到头部刚才所在的位置
  bodyArr[bodyArr.length - 1].render(0)
  bodyArr[bodyArr.length - 1].x = a.x - 1
  bodyArr[bodyArr.length - 1].y = a.y
  bodyArr[bodyArr.length - 1].render(1)
  bodyArr.unshift(bodyArr.pop())
  renderColor()
  end()
}
const left = () => {
  try {
    a.render(0)
    a.x -= 1
    a.render(1)
  }
  catch {
    clearInterval(timer)
    alert('游戏结束!')
    location.reload()
  }
  eat()
  //让数组中的最后一个元素,移动到头部刚才所在的位置
  bodyArr[bodyArr.length - 1].render(0)
  bodyArr[bodyArr.length - 1].x = a.x + 1
  bodyArr[bodyArr.length - 1].y = a.y
  bodyArr[bodyArr.length - 1].render(1)
  bodyArr.unshift(bodyArr.pop())
  renderColor()
  end()
}
复制代码

3.写键盘事件

写键盘事件的时候要加一个判断,让这个蛇只能够相对它自身左右移动。不能掉头,也不能向前冲,向前冲很容易就撞到墙了。

let num = 1
document.addEventListener('keydown', function (e) {
  //flag是暂停功能的变量
  if (flag) {
    if (e.key === 'ArrowDown') {
      //蛇头只能够向左或者向右移动,否则冲太快容易死。也不可以调头。
      if (num == 2 || num == 4) {
        down()
        num = 3
      }
    } else if (e.key === 'ArrowRight') {
      if (num == 1 || num == 3) {
        right()
        num = 2
      }
    } else if (e.key === 'ArrowLeft') {
      if (num == 1 || num == 3) {
        left()
        num = 4
      }
    } else if (e.key === 'ArrowUp') {
      if (num == 2 || num == 4) {
        up()
        num = 1
      }
    }
  }
})
复制代码

4.吃苹果功能

吃苹果功能分为3个步骤

1.判断头部和苹果有没有重合,重合的话,就让这个苹果消失,让分数+1。

2.生成随机坐标来渲染苹果,判断一下这个坐标上是否与蛇身体重合了,重合的话就要重新生成坐标。

3.生成一个新的身体实例,并且把这个新实例添加到身体数组中。

//得分
let fen = 0
//吃苹果
const eat = () => {
  //如果头部和苹果重合了
  if (arr[a.y][a.x].apple == 1) {
    //清除这个苹果
    arr[a.y][a.x].apple = 0
    //分数加1
    fen++
    //调用判断游戏胜利函数
    win()
    //渲染分数
    document.querySelector('.defen span').innerHTML = fen
    //用循环来生成新苹果,满足条件就退出循环
    while (true) {
      const x = Math.floor(Math.random() * 16)
      const y = Math.floor(Math.random() * 16)
      //判断苹果不能出现在身体上
      if (!arr[y][x].head && !arr[y][x].body) {
        arr[y][x].apple = 1
        break
      }
    }
    //生成新的身体实例
    let b = new Body(bodyArr[bodyArr.length - 1].x, bodyArr[bodyArr.length - 1].y)
    b.render(1)
    bodyArr.push(b)
  }
}
复制代码

5.头部碰到身体游戏失败的功能

和吃苹果的逻辑一样,就判断头部和身体是不是重合的。

//碰到身体游戏失败判定
const end = () => {
  //如果头部和身体重合了
  if (arr[a.y][a.x].body == 1) {
    clearInterval(timer)
    alert('游戏结束!')
    location.reload(true)
  }
}
复制代码

6.自动移动的功能

自动移动可以通过间歇函数来实现,然后不能单纯的在间歇函数的回调里面写上下左右的某一个,要判断一下蛇当前的移动方向是什么。然后来一个分数越高速度越快的功能。

//自动向前移动功能
let timer
//封装一个向前移动的函数
function move() {
  if (num == 1) {
    up()
  } else if (num == 2) {
    right()
  } else if (num == 3) {
    down()
  } else {
    left()
  }
}
//写自动移动的间歇函数
time()
function time() {
  //来个分越高速度越快的功能
  if (fen <= 20) {
    timer = setInterval(function () {
      move()
    }, 250)
  } else if (fen > 20 && fen <= 40) {
    clearInterval(timer)
    timer = setInterval(function () {
      move()
    }, 200)
  } else {
    clearInterval(timer)
    timer = setInterval(function () {
      move()
    }, 150)
  }
}
复制代码

7.暂停功能和判断游戏胜利

这两个比较简单,就一起说了。暂停功能就是让定时器停止,并且让flag变量变为false。这样就不能再去控制蛇了。这个游戏一共有256个格子,胜利的条件就是身体有255个元素,因为要减去一个头部。

//暂停功能
let flag = true
document.addEventListener('keydown', function (e) {
  if (flag) {
    if (e.key === 'Enter') {
      clearInterval(timer)
      flag = !flag
    }
  } else {
    if (e.key === 'Enter') {
      time()
      flag = true
    }
  }
})
//胜利条件,身体数组的元素等于255个就获胜
const win = () => {
  if (bodyArr.length == 255) {
    clearInterval(timer)
    alert('游戏胜利!')
    location.reload()
  }
}
复制代码

写在最后

游戏到这里就写完了,代码虽然看起来多,但是并没有什么难的地方,想到就能写。唯一麻烦一点的就是那个身体的移动,相通了就很简单了。码字不易,希望能留下你的赞。

04.jpg

05.jpg

分类:
前端
收藏成功!
已添加到「」, 点击更改