当前位置:   article > 正文

【JS】原生js实现极简版贪吃蛇,附全部代码_贪吃蛇js代码

贪吃蛇js代码

需求分析:

1.小蛇朝着某个方向不断运动 (头部运动 身体也动 每节身体运动的位置是下一节的位置)

2.上下左右能控制小蛇的运动方向

3.随机生成食物

4.碰到食物会增大

5.碰到四周或自己 游戏结束

实现思路:

最关键的就是利用Vue操作数据来改变视图的MVVM思想,我们设定一个数组,里面存放着小蛇每一个节点的全部信息,先改变数组内的数据,再根据数组数据进行dom操作。

1. 小蛇的渲染

设定一个数组,里面存放的是小蛇每节身体的数据,包括id和位置信息,根据这些信息渲染小蛇的dom节点(我这里是一个li作为一节身体),小蛇头部有一个移动方向的额外属性。

  1.     let snake_arr = [
  2. { id: 0, left: 100, top: 100, move_direction: 'r' },
  3. { id: 1, left: 80, top: 100 },
  4. { id: 2, left: 60, top: 100 }]
  5. // 1.根据数组渲染小蛇 为了让小蛇头部在右端,所以最后渲染
  6. function render_snake(flag = false) {
  7. if (flag) {
  8. // 当添加身体时,先把ul里面的li全部清空再进行渲染
  9. // 缺点是这种做法会让transition的效果失效
  10. ul_body.innerHTML = ''
  11. }
  12. // 从后往前 向ul里面添加li标签
  13. for (let i = snake_arr.length - 1; i >= 0; i--) {
  14. let li = document.createElement('li')
  15. // 第0个是小蛇的头部 多一个属性控制移动方向
  16. if (snake_arr[i].id == 0) {
  17. li.classList.add('cube', 'head')
  18. } else if (snake_arr[i].id == snake_arr.length - 1) {
  19. li.innerText = i
  20. li.classList.add('cube')
  21. }
  22. else {
  23. li.innerText = ''
  24. li.classList.add('cube')
  25. }
  26. li.style.left = snake_arr[i].left + 'px'
  27. li.style.top = snake_arr[i].top + 'px'
  28. ul_body.append(li)
  29. }
  30. }
  31.     render_snake()

2.小蛇的移动

其实是改变小蛇头部的一个属性而已,switchcase获取按键,并将该属性改为对应的值。然后根据小蛇头部位置进行dom操作来移动小蛇的头部。

而身体的移动其实是 上一节的身体位置和下一节身体的位置信息进行了交换,然后重新渲染小蛇身体,因为浏览器执行速度很快,所以看起来就像是小蛇在移动,其实是小蛇先消失,然后重新出现的。

  1. //根据上下左右改变小蛇头部的属性的代码略,就是先获取用户按的是上还是下,是左还是右,然后赋值给一个变量。不过需要注意:当小蛇在水平移动时,再按←或→不进行反应,上下也是同理,防止小蛇移动时原地掉头直接死亡了。
  2. // 先改变数组数据,然后重新渲染整条小蛇,渲染的函数可以复用刚才的函数
  3. function snake_move() {
  4. let d = snake_arr[0].move_direction
  5. /* lis: [尾 5 4 3 2 1 0 头]
  6. snake_arr: [头 0 1 2 3 4 5 尾]*/
  7. // 如果是操控dom元素的位置的话会比较难 所以直接改变存放小蛇数据的数组
  8. // 让每一节小蛇的身体都获取下一节的位置
  9. for (let i = snake_arr.length - 1; i >= 1; i--) {
  10. snake_arr[i].left = snake_arr[i - 1].left
  11. snake_arr[i].top = snake_arr[i - 1].top
  12. }
  13. // 头部单独处理 根据移动方向进行位置变化 这里取到20是因为小蛇每一节身体都是20*20px
  14. switch (d) {
  15. case 'l':
  16. snake_arr[0].left -= 20
  17. break
  18. case 'r':
  19. snake_arr[0].left += 20
  20. break
  21. case 'u':
  22. snake_arr[0].top -= 20
  23. break
  24. case 'd':
  25. snake_arr[0].top += 20
  26. break
  27. }
  28. // 数组变动之后,再重新进行渲染 这种方法的优点是代码量比较少 缺点是无法使用transition效果,但是使用过渡效果也会有bug,主要是拐弯时过渡效果会使得小蛇身体出现重影
  29. render_snake(true)
  30. }
  31. let auto_move = setInterval(snake_move, 100)

3. 随机生成食物

重点是生成的食物应该和小蛇可以完全重合,所以食物所在的位置和大小必须和小蛇的身体成倍数关系才行。

  1. // 随机在地图上生成食物
  2. function random_food() {
  3. // 0 - width/height 还得是20的倍数 这样20px * 20px的小蛇才能和食物完全重合
  4. let random_left = Math.floor(Math.random() * container.clientWidth / 20)
  5. let random_top = Math.floor(Math.random() * container.clientHeight / 20)
  6. let food = document.createElement('div')
  7. food.classList.add('food')
  8. food.style.left = random_left * 20 + 'px'
  9. food.style.top = random_top * 20 + 'px'
  10. container.append(food)
  11. }
  12. random_food()

效果如下:食物就像是在一个个的网格上面,不会出现错位的情况

4.吃到食物身体增加一节

需要使用定时器不断检测小蛇头部的位置,判断其是否与食物重合,重合的话才会执行下一步操作:即增加数组内一个元素,并将其渲染到dom节点中。

  1. // 吃到食物身体增加1节
  2. // 获取到指定的某个元素的相对位置
  3. function get_position(domEle) {
  4. return [domEle.offsetLeft, domEle.offsetTop]
  5. }
  6. // 1.吃食物的检测与后续操作
  7. function eat_food() {
  8. let test_head_position
  9. // 获得食物的位置 因为食物是不会动的,所以获取食物的位置只需要获取一次即可
  10. let food = document.querySelector('.food')
  11. let [fl, ft] = get_position(food)
  12. // 检测小蛇头部是否与食物的位置重合
  13. test_head_position = setInterval(() => {
  14. // console.log(test_head_position); //用log的频率来检测定时器是否被清除了
  15. // 小蛇是在不断移动的,所以需要不断重新获取小蛇的位置才行
  16. let head = document.querySelector('.snake_body .head')
  17. let [hl, ht] = get_position(head)
  18. if (hl == fl && ht == ft) {
  19. // 吃到了食物的时候再清除定时器
  20. clearInterval(test_head_position)
  21. // console.log('吃到了食物');
  22. // 删除被吃掉的食物
  23. food.parentElement.removeChild(food)
  24. // 随机生成一个新的食物
  25. random_food()
  26. // 执行这个函数 以便获取新食物的位置
  27. eat_food()
  28. // 增加n节身体长度
  29. add_body(1)
  30. }
  31. }, 100)
  32. }
  33.         // 2.增加1节身体
  34. function add_body(n = 1) {
  35. for (let i = 0; i < n; i++) {
  36. let length = snake_arr.length
  37. // 直接加在最后一节身体的上面 虽然当时重合在了一起 但是下次移动时,倒数第二个位置会变到倒数第三个cube的位置,倒数第一个位置相当于没变动,就露出来了
  38. snake_arr.push({ id: length, left: snake_arr[length - 1].left, top: snake_arr[length - 1].top })
  39. }
  40. render_snake(true)
  41. // console.log(snake_arr)
  42. }
'
运行

5.检测是否撞到了自己

设定一个定时器,不断执行下面函数,因为数组内存放的有小蛇每一节身体的位置数据,所以只需要遍历数组进行判断即可。

  1. // 检测是否撞到了自己
  2. function test_body() {
  3. // 通过小蛇存放的位置来判断头部是否和某一个身体部位重合
  4. console.log(snake_arr[0].move_direction);
  5. for (let i = snake_arr.length - 1; i > 0; i--) {
  6. if (snake_arr[0].left == snake_arr[i].left && snake_arr[0].top == snake_arr[i].top) {
  7. return true
  8. }
  9. }
  10. return false
  11. }
'
运行

额外注意:

浏览器在监听键盘按压事件时,灵敏度是很高的,很可能出现同时或几乎同时按下两个按键时,小蛇移动出现原地掉头情况,然后直接死亡,玩家体验非常不好,这个问题我试验了很多次,但是都没有很好的解决,希望评论区有大神可以解决这个问题。以下是我尝试解决的方法:

  1. // 贪吃蛇要用keyup事件,否则 按住 ← 再按其他的按键,会同时执行这两个按键的事件
  2. window.addEventListener('keyup', (e) => {
  3. // console.log(e.key);
  4. // 1.使用防抖函数来 减少 快速按压两个按键时咬到自己的bug 的发生频率 (改成100ms几乎完全不会触发这个bug)
  5. // 在小蛇向下移动时,同时按下 → ↑ (先随便按一个 再同时按下相反的按键)就会触发这个bug,其他方向也有这个bug
  6. // (bug原因可能是因为在按下→键时,js代码已经改变了小蛇移动的方向 从d变成了r,但是100ms才移动一次,这时候
  7. // 小蛇还未向右移动,恰巧用户又按下了↑,导致snake_move执行时,小蛇移动方向变成了u,是向上移动的 就触发了这个bug)
  8. // debounce(e.key)
  9. // 重点是保证永远只有一个事件执行(防抖是只允许最后一个执行,节流是只允许第一个事件执行)
  10. // 2.如果不用防抖 小蛇虽然转向没延迟了 但是这个bug更容易触发了
  11. test_key(e.key)
  12. // 3.使用节流阀也行,但是其实本质是让节流阀来限制用户的操作频率 (如果在100ms之内按下了两次按键,只有第一次会被执行 第二次无效)
  13. // test_key(e.key)
  14. })

代码附录:

因为是极简版,所以没有素材图片,直接复制粘贴即可运行

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>贪吃蛇-极简版</title>
  8. <style>
  9. * {
  10. margin: 0;
  11. padding: 0;
  12. }
  13. ul {
  14. list-style: none;
  15. }
  16. .container {
  17. width: 1000px;
  18. height: 800px;
  19. margin: 10px auto;
  20. background-color: antiquewhite;
  21. position: relative;
  22. }
  23. .food {
  24. width: 20px;
  25. height: 20px;
  26. background-color: lightcoral;
  27. position: absolute;
  28. }
  29. /* .snake_body {
  30. position: relative;
  31. } */
  32. .snake_body .cube {
  33. position: absolute;
  34. width: 20px;
  35. height: 20px;
  36. background-color: #bff;
  37. }
  38. .cube.head {
  39. background-color: rgb(4, 147, 250);
  40. }
  41. </style>
  42. </head>
  43. <body>
  44. <div class="container">
  45. <ul class="snake_body">
  46. <!-- <li class="cube"></li>
  47. <li class="cube"></li>
  48. <li class="cube head"></li> -->
  49. </ul>
  50. </div>
  51. <script>
  52. const container = document.querySelector('.container')
  53. const ul_body = document.querySelector('.snake_body')
  54. /*需求:
  55. 1.小蛇朝着某个方向不断运动 (头部运动 身体也动 每节身体运动的位置是下一节的位置)
  56. 2.上下左右能控制小蛇的运动方向
  57. 3.随机生成食物
  58. 4.碰到食物会增大
  59. 5.碰到四周或自己 游戏结束
  60. 6.采用vue 操作数据的思想 */
  61. let snake_arr = [
  62. { id: 0, left: 100, top: 100, move_direction: 'r' },
  63. { id: 1, left: 80, top: 100 },
  64. { id: 2, left: 60, top: 100 }]
  65. render_snake()
  66. // 1.根据数组渲染小蛇 为了让小蛇头部在右,所以最后渲染
  67. function render_snake(flag = false) {
  68. if (flag) {
  69. // 当添加身体时,先把ul里面的li全部清空再进行渲染
  70. // 这种做法会让transition的效果失效
  71. ul_body.innerHTML = ''
  72. }
  73. // 从后往前 向ul里面添加li标签
  74. for (let i = snake_arr.length - 1; i >= 0; i--) {
  75. let li = document.createElement('li')
  76. // 第0个是小蛇的头部 多一个属性控制移动方向
  77. if (snake_arr[i].id == 0) {
  78. li.classList.add('cube', 'head')
  79. } else if (snake_arr[i].id == snake_arr.length - 1) {
  80. li.innerText = i
  81. li.classList.add('cube')
  82. }
  83. else {
  84. li.innerText = ''
  85. li.classList.add('cube')
  86. }
  87. li.style.left = snake_arr[i].left + 'px'
  88. li.style.top = snake_arr[i].top + 'px'
  89. ul_body.append(li)
  90. }
  91. }
  92. // 2.增加1节身体
  93. function add_body(n = 1) {
  94. for (let i = 0; i < n; i++) {
  95. let length = snake_arr.length
  96. // 直接加在最后一节身体的上面 虽然当时重合在了一起 但是下次移动时,倒数第二个位置会变到倒数第三个cube的位置,倒数第一个位置相当于没变动,就露出来了
  97. snake_arr.push({ id: length, left: snake_arr[length - 1].left, top: snake_arr[length - 1].top })
  98. }
  99. render_snake(true)
  100. // console.log(snake_arr)
  101. }
  102. // 3. 根据头部的方向移动
  103. function snake_move() {
  104. let d = snake_arr[0].move_direction
  105. /* lis: [尾 5 4 3 2 1 0 头]
  106. snake_arr: [头 0 1 2 3 4 5 尾]*/
  107. // 如果是操控dom元素的位置的话会比较难 所以直接改变存放小蛇数据的数组
  108. // 让每一节小蛇的身体都获取下一节的位置
  109. for (let i = snake_arr.length - 1; i >= 1; i--) {
  110. snake_arr[i].left = snake_arr[i - 1].left
  111. snake_arr[i].top = snake_arr[i - 1].top
  112. }
  113. // 头部单独处理 根据移动方向进行位置变化 这里取到20是因为小蛇每一节身体都是20*20px
  114. switch (d) {
  115. case 'l':
  116. snake_arr[0].left -= 20
  117. break
  118. case 'r':
  119. snake_arr[0].left += 20
  120. break
  121. case 'u':
  122. snake_arr[0].top -= 20
  123. break
  124. case 'd':
  125. snake_arr[0].top += 20
  126. break
  127. }
  128. // 数组变动之后,再重新进行渲染 这种方法的优点是代码量比较少 缺点是无法使用transition效果,但是使用过渡效果也会有bug,主要是拐弯时过渡效果会使得小蛇身体出现重影
  129. render_snake(true)
  130. }
  131. let auto_move = setInterval(snake_move, 100)
  132. // 4.按键改变方向 其实是改变数组中head的属性值,然后移动时是看属性值进行移动的
  133. // 节流阀的做法:定义lock
  134. let lock = false
  135. // 判断一下是否和正在移动的方向相反 如果是的话不对用户的操作作出反应
  136. function test_key(key) {
  137. if (!lock) {
  138. lock = true
  139. // console.log('我执行了');
  140. let d = snake_arr[0].move_direction
  141. // 判断一下按压的key是水平的还是垂直的
  142. let key_type = 'vertical'
  143. if (key == 'ArrowLeft' || key == 'ArrowRight') {
  144. key_type = 'level'
  145. } else {
  146. key_type = 'vertical'
  147. }
  148. // 如果用户想让小蛇水平移动 且此时小蛇已经是水平移动状态了 那么就不做操作
  149. if (key_type == 'level' && (d == 'r' || d == 'l')) {
  150. // console.log('小蛇正在水平移动');
  151. } else if (key_type == 'vertical' && (d == 'u' || d == 'd')) {
  152. // console.log('小蛇正在垂直移动');
  153. } else {
  154. key_change_direction(key)
  155. }
  156. setTimeout(() => {
  157. lock = false
  158. }, 50)
  159. }
  160. }
  161. // 根据按键改变小蛇的头部方向
  162. function key_change_direction(key) {
  163. switch (key) {
  164. case 'ArrowLeft':
  165. snake_arr[0].move_direction = 'l'
  166. break
  167. case 'ArrowRight':
  168. snake_arr[0].move_direction = 'r'
  169. break
  170. case 'ArrowUp':
  171. snake_arr[0].move_direction = 'u'
  172. break
  173. case 'ArrowDown':
  174. snake_arr[0].move_direction = 'd'
  175. break
  176. default:
  177. console.log(key);
  178. }
  179. }
  180. let timer = null
  181. function debounce(key) {
  182. if (timer) {
  183. // console.log('已经有了一个定时器:' + timer);
  184. clearTimeout(timer)
  185. }
  186. timer = setTimeout(() => {
  187. test_key(key)
  188. }, 50)
  189. }
  190. // 贪吃蛇要用keyup事件,否则 按住 ← 再按其他的按键,会同时执行这两个按键的事件
  191. window.addEventListener('keyup', (e) => {
  192. // console.log(e.key);
  193. // 1.使用防抖函数来 减少 快速按压两个按键时咬到自己的bug 的发生频率 (改成100ms几乎完全不会触发这个bug)
  194. // 如果把211行代码改成0,在 小蛇向下移动时,同时按下 → ↑ (先随便按一个 再同时按下相反的按键)就会触发这个bug,其他方向也有这个bug
  195. // (bug原因可能是因为在按下→键时,js代码已经改变了小蛇移动的方向 从d变成了r,但是100ms才移动一次,这时候
  196. // 小蛇还未向右移动,恰巧用户又按下了↑,导致snake_move执行时,小蛇移动方向变成了u,是向上移动的 就触发了这个bug)
  197. // debounce(e.key)
  198. // 重点是保证永远只有一个事件执行(防抖是只允许最后一个执行,节流是只允许第一个事件执行)
  199. // 2.如果不用防抖 小蛇虽然转向没延迟了 但是这个bug更容易触发了
  200. test_key(e.key)
  201. // 3.使用节流阀也行,但是其实本质是让节流阀来限制用户的操作频率 (如果在100ms之内按下了两次按键,只有第一次会被执行 第二次无效)
  202. // test_key(e.key)
  203. })
  204. // 5.随机在地图上生成食物
  205. function random_food() {
  206. // 0 - width/height 还得是20的倍数 这样20px * 20px的小蛇才能和食物完全重合
  207. let random_left = Math.floor(Math.random() * container.clientWidth / 20)
  208. let random_top = Math.floor(Math.random() * container.clientHeight / 20)
  209. let food = document.createElement('div')
  210. food.classList.add('food')
  211. food.style.left = random_left * 20 + 'px'
  212. food.style.top = random_top * 20 + 'px'
  213. container.append(food)
  214. }
  215. random_food()
  216. // 6.吃到食物身体增加1节
  217. // 获取到指定元素的相对位置
  218. function get_position(domEle) {
  219. return [domEle.offsetLeft, domEle.offsetTop]
  220. }
  221. // 吃食物的检测与后续操作
  222. function eat_food() {
  223. let test_head_position
  224. // 获得食物的位置 因为食物是不会动的,所以获取食物的位置只需要获取一次即可
  225. let food = document.querySelector('.food')
  226. let [fl, ft] = get_position(food)
  227. // 检测小蛇头部是否与食物的位置重合
  228. test_head_position = setInterval(() => {
  229. // console.log(test_head_position); //用log的频率来检测定时器是否被清除了
  230. // 小蛇是在不断移动的,所以需要不断重新获取小蛇的位置才行
  231. let head = document.querySelector('.snake_body .head')
  232. let [hl, ht] = get_position(head)
  233. if (hl == fl && ht == ft) {
  234. // 吃到了食物的时候再清除定时器
  235. clearInterval(test_head_position)
  236. // console.log('吃到了食物');
  237. // 删除被吃掉的食物
  238. food.parentElement.removeChild(food)
  239. // 随机生成一个新的食物
  240. random_food()
  241. // 执行这个函数 以便获取新食物的位置
  242. eat_food()
  243. // 增加n节身体长度
  244. add_body(1)
  245. }
  246. }, 100)
  247. }
  248. eat_food()
  249. // 7.增加失败条件
  250. let container_w = container.offsetWidth
  251. let container_h = container.offsetHeight
  252. let test_failue = setInterval(() => {
  253. // 跑出圈外判负
  254. if (snake_arr[0].left >= container_w || snake_arr[0].left < 0 || snake_arr[0].top >= container_h || snake_arr[0].top < 0) {
  255. // location.reload()
  256. // alert('碰壁了!')
  257. // 不清除定时器的话会一直alert很多遍 但是如果把reload放到alert上面就不用清除定时器了
  258. clearInterval(test_failue)
  259. clearInterval(auto_move)
  260. }
  261. // 碰到自己的身体也判负
  262. else if (test_body()) {
  263. // location.reload()
  264. // alert('咬着自己了!')
  265. clearInterval(test_failue)
  266. clearInterval(auto_move)
  267. }
  268. // 胜利条件 没有写
  269. // else if(){
  270. // }
  271. }, 100)
  272. // 检测是否撞到了自己
  273. function test_body() {
  274. // let cubes = document.querySelectorAll('.snake_body .cube')
  275. // let head = document.querySelector('.snake_body .head')
  276. // let [hl, ht] = get_position(head)
  277. // for (let i = 0; i < cubes.length - 1; i++) {
  278. // let [bl, bt] = get_position(cubes[i])
  279. // if (hl == bl && ht == bt) {
  280. // return true
  281. // }
  282. // }
  283. // 没有问题
  284. // return false
  285. // 通过小蛇存放的位置来判断头部是否和某一个身体部位重合
  286. console.log(snake_arr[0].move_direction);
  287. for (let i = snake_arr.length - 1; i > 0; i--) {
  288. if (snake_arr[0].left == snake_arr[i].left && snake_arr[0].top == snake_arr[i].top) {
  289. return true
  290. }
  291. }
  292. return false
  293. }
  294. </script>
  295. </body>
  296. </html>
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/1001433
推荐阅读
相关标签
  

闽ICP备14008679号