详解vue.js实现贪吃蛇游戏
贪吃蛇是一款经典的h5小游戏,对于许多入门js的同学来说这是一个很好的练手项目,可以锻炼思维并且更熟悉js与视图层的交互。网上普遍的做法都是用原生js实现的,很少有用框架来实现的。所以我试着用vue.js做了出来,在难度和代码量上确实比我之前用原生js做的小了点,下面讲解我的每一步思路和步骤:
Step 1
首先先创建棋盘
先创建一个大的容器,然后每行20格,共20行填上小格子。这里稍微计算一下高宽,用两层for循环实现。因为之后要频繁对dom结点操作,所以用ref钩住,调用方式为
this.$refs.grid[index]
html
<div class="board"> <div v-for="(i,index) in 20" :key="index"> <div class="commonGrid" v-for="(j,index) in 20" ref="grid" :key="index"> </div> </div> </div>
.board { width: 300px; height: 300px; flex-flow: row; border: 1px solid black; } .commonGrid { float: left; width: 14px; height: 14px; border: 0.5px solid rgb(214, 125, 125); } .snakeGrid { background-color: red; } .eggGrid { background-color: yellow; }
Step2
好了,有了棋盘,我们要把蛇和蛋放进去了吧。思路是定义一个数组,每个对象包含一个格子的行、列,然后再把蛇渲染到棋盘上。还需要分辨一下头尾。下标为0的对象是头,最后一个为尾,一进入页面就默认生成一条蛇,蛋也是如此。
在data里定义全局变量 snake: [],egg:{}
created() { //初始化蛇身,下标第一位是头,最后一位是尾 for (let i = 4; i > 0; i--) this.snake = [ ...this.snake, { row: 2, column: i + 1 } ]; }, mounted() { this.mountSnake() this.createEgg() }
在methods里定义渲染蛇和蛋的函数
mountSnake() { //forEach循环,改变对应格子颜色 this.snake.forEach(body => { // 由于refs是一维数组,所以要转换一下:行数*20 + 列数就是对应的格子 this.$refs.grid[body.row * 20 + body.column].setAttribute( "class", "commonGrid snakeGrid" ); }); }, createEgg(){ let randrow = null let randcolumn = null while(true) { randrow = Math.floor(Math.random()*20) //生成0-19的随机数 randcolumn = Math.floor(Math.random()*20) // 不能生长在蛇的身上 if(this.notInSnake(randrow, randcolumn)) break } this.egg = { row: randrow, column: randcolumn } this.$refs.grid[randrow * 20 + randcolumn].setAttribute("class", "commonGrid eggGrid") }, notInSnake(row,column) { let flag = false this.snake.forEach(item => { if(item.row != row && item.column != column) { flag = true } }) return flag },
可以看到棋盘上躺着一条蛇啦和一个蛋啦
Step3
第三步是最难的一步。要让蛇动起来,首先要有方向,然后设定定时器,每次循环判断蛇的方向,然后往该方向前移一格。而改变方向需要一个间隔很短的定时器,以判断玩家是否按键盘改变方向了。
data
data() { return { pauseButton: false, snake: [], dir: 'right', // 初始化方向 moveTimer: null, dirTimer: null, }; },
methods
timer会返回一个计时器对象的id值,可以用来暂停时清除定时器。
start() { // 点击'开始游戏'的事件处理 this.pauseButton = true; // 暂停 this.moveTimer = this.setTimer() this.dirTimer = this.changeDir() },
这里判断方向需要一些逻辑思维,在浏览器中按f12查看每个格子的信息。可以发现行数是从上往下递增,列数是从左到右递增的。
所以每次判断 当方向往右时,创建一个新对象,行数为头部的行数,列数为头部的列数+1,放入snake[0],再取出尾部。以此类推。详见代码
setTimer() { return setInterval(() => { let newRow = null; let newColumn = null switch (this.dir) { case "right": newRow = this.snake[0].row; newColumn = this.snake[0].column + 1; this.snake.unshift({ row: newRow, column: newColumn }); break; case "up": newRow = this.snake[0].row - 1; newColumn = this.snake[0].column; this.snake.unshift({ row: newRow, column: newColumn }) break; case "left": newRow = this.snake[0].row; newColumn = this.snake[0].column - 1; this.snake.unshift({ row: newRow, column: newColumn }) break; case "down": newRow = this.snake[0].row + 1; newColumn = this.snake[0].column; this.snake.unshift({ row: newRow, column: newColumn }) break } if(!this.judgeEgg()) //判断是否吃到蛋 { const delItem = this.snake.pop() this.$refs.grid[delItem.row * 20 + delItem.column].setAttribute( "class", "commonGrid" ) } this.judgeFail() this.mountSnake() }, 200) }, changeDir() { return setInterval(() => { document.onkeydown = event => { const e = event || window.event || arguments.callee.caller.arguments[0]; if (e && e.keyCode == 37 && this.dir != 'right') { // 按 左 this.dir = "left" } else if (e && e.keyCode == 38 && this.dir != 'down') { // 按 上键 this.dir = "up" } else if (e && e.keyCode == 39 && this.dir != 'left') { // 按 右键 this.dir = "right" } else if (e && e.keyCode == 40 && this.dir != 'up') { //按 下键 this.dir = "down" } this.mountSnake() }; }, 10); },
可以看到,蛇已经会动了
Step4
接下来要实现吃蛋功能。在setTimer()可以看到,if(!this.judgeEgg())语句就是判断蛇的头部是否碰到蛋了,如果碰到了,尾巴就不用减去了,这样就实现了长度+1,并且要重新生成一个蛋
judgeEgg() { let flag = false this.snake.forEach( body => { if(body.row === this.egg.row && body.column === this.egg.column) { flag = true this.createEgg() this.score ++ } }) return flag },
Step5
离成功之差一步了!就是要判断失败条件,也就是撞到自己或者墙壁就算输。
judgeFail() { // 判断是否吃到自己,只需判断新加入的那一块格子是否与身体重叠 for (let i = 1; i < this.snake.length ; i++){ if(this.snake[0].row === this.snake[i].row && this.snake[0].column === this.snake[i].column) { this.fail() return } } // 判断是否撞到墙,只需判断新加入的那一块格子是否越界 if (this.snake[0].row > 19 || this.snake[0].row < 0 || this.snake[0].column > 19 || this.snake[0].column < 0) this.fail() }, fail() { this.pauseButton = false clearInterval(this.moveTimer) clearInterval(this.dirTimer) this.failFlag = true },
finally
按照前五步的思路走,就可以实现贪吃蛇小游戏了!
最后就是把样式优化一下,增加点功能 如计算得分、开始/暂停、失败弹框、调整速度等。
总的来说难度中等,考验了对js和vue的掌握程度。涉及到对es6、计时器、对象、生命周期等知识的理解和运用。还是非常不错的练手项目。
下面我把所有代码贴出来
<template> <div class="container"> <div class="info" v-if="!failFlag"> <button @click="start" v-if="!pauseButton">开始游戏</button> <button @click="pause" v-else>暂停</button> <span>长度: {{score}}</span> </div> <div class="board" v-if="!failFlag"> <div v-for="(i,index) in 20" :key="index"> <div class="commonGrid" v-for="(j,index) in 20" ref="grid" :key="index"></div> </div> </div> <div v-else class="failBox"> <img src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2872222233,2453972241&fm=26&gp=0.jpg'/> <button @click="again">再来一次</button> </div> </div> </template> <script> import { Message } from "element-ui"; import { setInterval, clearInterval } from "timers"; export default { data() { return { pauseButton: false, //二维数组 snake: [], egg: { row: Number, column: Number }, score: 4, //蛇前进的方向 dir: String, moveTimer: null, dirTimer: null, failFlag: false }; }, created() { //初始化蛇身,下标第一位是头,最后一位是尾 for (let i = 4; i > 0; i--) this.snake = [ ...this.snake, { row: 2, column: i + 1 } ]; this.dir = "right" }, mounted() { this.mountSnake() this.createEgg() }, methods: { mountSnake() { this.snake.forEach(body => { this.$refs.grid[body.row * 20 + body.column].setAttribute( "class", "commonGrid snakeGrid" ); }); }, start() { this.pauseButton = true; this.moveTimer = this.setTimer() this.dirTimer = this.changeDir() }, setTimer() { return setInterval(() => { let newRow = null; let newColumn = null switch (this.dir) { case "right": newRow = this.snake[0].row; newColumn = this.snake[0].column + 1; this.snake.unshift({ row: newRow, column: newColumn }); break; case "up": newRow = this.snake[0].row - 1; newColumn = this.snake[0].column; this.snake.unshift({ row: newRow, column: newColumn }) break; case "left": newRow = this.snake[0].row; newColumn = this.snake[0].column - 1; this.snake.unshift({ row: newRow, column: newColumn }) break; case "down": newRow = this.snake[0].row + 1; newColumn = this.snake[0].column; this.snake.unshift({ row: newRow, column: newColumn }) break } if(!this.judgeEgg()) //判断是否吃到蛋 { const delItem = this.snake.pop() this.$refs.grid[delItem.row * 20 + delItem.column].setAttribute( "class", "commonGrid" ) } this.judgeFail() this.mountSnake() }, 200) }, changeDir() { return setInterval(() => { document.onkeydown = event => { const e = event || window.event || arguments.callee.caller.arguments[0]; if (e && e.keyCode == 37 && this.dir != 'right') { // 按 左 this.dir = "left" } else if (e && e.keyCode == 38 && this.dir != 'down') { // 按 上键 this.dir = "up" } else if (e && e.keyCode == 39 && this.dir != 'left') { // 按 右键 this.dir = "right" } else if (e && e.keyCode == 40 && this.dir != 'up') { //按 下键 this.dir = "down" } this.mountSnake() }; }, 10); }, judgeEgg() { let flag = false this.snake.forEach( body => { if(body.row === this.egg.row && body.column === this.egg.column) { flag = true this.createEgg() this.score ++ } }) return flag },judgeFail() { // 判断是否吃到自己,只需判断新加入的那一块格子是否与身体重叠 for (let i = 1; i < this.snake.length ; i++){ if(this.snake[0].row === this.snake[i].row && this.snake[0].column === this.snake[i].column) { this.fail() return } } // 判断是否撞到墙,只需判断新加入的那一块格子是否越界 if (this.snake[0].row > 19 || this.snake[0].row < 0 || this.snake[0].column > 19 || this.snake[0].column < 0) this.fail() }, notInSnake(row,column) { let flag = false this.snake.forEach(item => { if(item.row != row && item.column != column) { flag = true } }) return flag }, createEgg(){ let randrow = null let randcolumn = null while(true) { randrow = Math.floor(Math.random()*20) //生成0-19的随机数 randcolumn = Math.floor(Math.random()*20) if(this.notInSnake(randrow, randcolumn)) break } this.egg = { row: randrow, column: randcolumn } console.log(this.egg) this.$refs.grid[randrow * 20 + randcolumn].setAttribute("class", "commonGrid eggGrid") }, pause() { this.pauseButton = false clearInterval(this.moveTimer) clearInterval(this.dirTimer) this.moveTimer = null this.dirTimer = null }, fail() { this.pauseButton = false clearInterval(this.moveTimer) clearInterval(this.dirTimer) this.failFlag = true }, again() { location.reload() } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { height: 80vh; overflow: hidden; display: flex; align-items: center; justify-content: center; flex-direction: column; } .board { width: 300px; height: 300px; flex-direction: row; border: 1px solid black; } button { cursor: pointer; width: 80px; height: 30px; margin: 10px 20px; } .commonGrid { float: left; width: 14px; height: 14px; border: 0.5px solid rgb(214, 125, 125); } .snakeGrid { background-color: red; } .eggGrid { background-color: yellow; } .failBox { display: flex; flex-direction: column; justify-items: center; align-items: center; } </style>
- 纯原生js实现贪吃蛇游戏
- Vue.js分页组件实现:diVuePagination的使用详解
- Js实现贪吃蛇游戏
- 详解Vue基于 Nuxt.js 实现服务端渲染(SSR)
- 用JS实现的贪吃蛇游戏
- 如何在Vue.js中实现标签页组件详解
- 原生js实现的贪吃蛇网页版游戏完整实例
- 慕课网——JS实现贪吃蛇游戏——01(蛇出洞喽~)
- Vue.js双向绑定实现原理详解
- 【贪吃蛇—Java程序员写Android游戏】系列 3. 用J2ME实现Android的Snake Sample详解
- JS实现的贪吃蛇游戏完整实例
- 详解Vue.js iview实现树形权限表(可扩展表)
- webpack+vue.js实现组件化详解
- canvas+js实现贪吃蛇游戏
- 详解用vue.js和laravel实现微信授权登陆
- jQuery+vue.js实现的九宫格拼图游戏完整实例【附源码下载】
- 用JS实现简易贪吃蛇游戏
- 详解利用 Vue.js 实现前后端分离的RBAC角色权限管理
- 【贪吃蛇—Java程序员写Android游戏】系列 3. 用J2ME实现Android的Snake Sample详解
- Vue.js实现可配置的登录表单代码详解