您的位置:首页 > Web前端 > Vue.js

详解vue.js实现贪吃蛇游戏

2019-04-20 17:29 2571 查看

贪吃蛇是一款经典的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>

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: