我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛
前言
之前用原生的JavaScrip
写过一次贪吃蛇,逻辑稍显复杂,后面用JQuery
做过一个优化,但是整体的效果不是很好,这里我们用vue2
来粗略地实现。
思路
要让一条蛇在画板上跑,因此首先我们要新建一个画板元素div
,先暂时固定宽高,然后给定一个背景色,样式后面再来优化。
然后我们再来放一条蛇,新增一个list
的数据,然后初始化它的坐标为(0, 0)、(0, 1)、(0, 2)、(0, 3)、(0, 4)
,其中x
表示每一个元素位于多少行,y
表示位于多少列。
this.list = new Array(length).fill(0).map((el, index) => ({ x: 0, y: index }))
复制代码
然后我们要v-if
渲染到画板上,同时配上位置函数,将每个元素都位置都放好。
getPosotion({ x, y }) {
return {
top: `${x * 20}px`,
left: `${y * 20}px`,
}
},
复制代码
现在我们来让这条蛇元素动起来,很简单,列表中的数据,后一个的坐标来覆盖前一个的坐标即可,而最后一个元素,也就是蛇头,我们固定让它的y
自增1
就可以了。
const list = [...this.list]
const len = list.length
const head = list[len - 1]
list.forEach((item, index) => {
if (index < len - 1) {
const next = list[index + 1]
;[item.x, item.y] = [next.x, next.y]
}
})
head.y += 1
复制代码
现在它就动起来了。
现在来想一个问题呢,如果定时器一直跑,蛇就跑出画板了,同时我们还不能控制方向,因此我们需要监听键盘的按下事件,同时,我们需要一个current
值来保存当前的方向。
addEventListener() {
document.addEventListener('keydown', ({ keyCode }) => {
this.current = keyCode
})
},
复制代码
然后在移动的函数中,根据当前的方向值,来让蛇上下左右移动。
const keyCodes = {
LEFT: 37,
TOP: 38,
RIGHT: 39,
BOTTOM: 40,
}
switch (this.current) {
case keyCodes.RIGHT:
head.y += 1
break
case keyCodes.LEFT:
head.y -= 1
break
case keyCodes.BOTTOM:
head.x += 1
break
case keyCodes.TOP:
head.x -= 1
break
}
this.list = list
复制代码
现在,我们的大蛇可以上下左右移动了。
然后再来考虑碰撞问题,无非就是四个边界,还有就是自身也可能发生碰撞,这里直接上代码。边界碰撞只需要判断头部元素的坐标值和边界之间的关系,而自身碰撞,需要头部坐标与除了头部以外的剩余蛇身体部分的坐标做比较,如果有一个相等,some
再好不过,那么就是碰撞了自身,游戏将结束。
isImpact() {
const len = this.list.length
const { x: headX, y: headY } = this.list[len - 1]
const { cols, rows } = this.getRowsCols()
if (this.list.slice(0, len - 1).some(({ x, y }) => x === headX && y === headY)) {
return true
}
if (headY >= cols) {
return true
}
if (headX >= rows) {
return true
}
if (headX < 0) {
return true
}
if (headY < 0) {
return true
}
return false
},
复制代码
这里就是自己撞自己的情况。
以上情况都解决之后呢,我们再来考虑生成苹果的情况,蛇嘛,让吃此苹果就行了。逻辑上也很简单,根据画布的情况,随机生成一个苹果,然后我们要判断每一个方向时,只要苹果的坐标和蛇头的坐标满足条件与否。举个栗子,假设蛇往右移动,那么就是current
为RIGHT
的keycode
时,且苹果的x
和蛇头的x
相同,而蛇头的y + 1
是等于苹果的y
的话,表明蛇可以吃苹果,然后我们往list
中push
一个空对象就可以了。为什么是空对象呢,因为下一次移动时,空对象将被上一个对象覆盖。
canEat() {
const len = this.list.length
const { x: headX, y: headY } = this.list[len - 1]
const { x, y } = this.apple
if (this.current === keyCodes.RIGHT) {
if (headY + 1 === y && headX === x) {
return true
}
}
if (this.current === keyCodes.LEFT) {
if (headY - 1 === y && headX === x) {
return true
}
}
if (this.current === keyCodes.TOP) {
if (headX - 1 === x && headY === y) {
return true
}
}
if (this.current === keyCodes.BOTTOM) {
if (headX + 1 === x && headY === y) {
return true
}
}
return false
},
复制代码
一个简单的贪吃蛇就完成了,你可以添加一些得分,或者暂停按钮,或者其它的样式,这里是完整的代码。
<template>
<div id="app">
<div v-for="(item, index) in list" :key="index" class="item" :style="getPosotion(item)"></div>
<button @click="pause">暂停</button>
<div v-if="show" class="dialog">
<p>你已经GG了!!!</p>
<p class="start" @click="handleRePlay">重新开始</p>
</div>
<div v-if="showApple" class="apple" :style="getPosotion(apple)"></div>
</div>
</template>
<script>
const keyCodes = {
LEFT: 37,
TOP: 38,
RIGHT: 39,
BOTTOM: 40,
}
const length = 5
const speed = 300
export default {
name: 'App',
data() {
return {
list: [],
current: null,
timer: null,
show: false,
showApple: false,
apple: {
x: -1,
y: -1,
},
}
},
mounted() {
this.init()
this.addEventListener()
},
beforeDestroy() {
clearInterval(this.timer)
},
methods: {
handleRePlay() {
this.show = false
this.init()
},
init() {
this.current = keyCodes.RIGHT
this.start()
},
start() {
this.list = new Array(length).fill(0).map((el, index) => ({ x: 0, y: index }))
clearInterval(this.timer)
this.timer = setInterval(() => {
this.move()
}, speed)
},
move() {
const list = [...this.list]
const len = list.length
const head = list[len - 1]
list.forEach((item, index) => {
if (index < len - 1) {
const next = list[index + 1]
;[item.x, item.y] = [next.x, next.y]
}
})
switch (this.current) {
case keyCodes.RIGHT:
head.y += 1
break
case keyCodes.LEFT:
head.y -= 1
break
case keyCodes.BOTTOM:
head.x += 1
break
case keyCodes.TOP:
head.x -= 1
break
}
this.list = list
if (!this.showApple) {
this.showApple = true
this.createApple()
}
if (this.canEat()) {
this.list.unshift({})
this.showApple = false
}
if (this.isImpact()) {
this.pause()
this.show = true
this.showApple = false
}
},
canEat() {
const len = this.list.length
const { x: headX, y: headY } = this.list[len - 1]
const { x, y } = this.apple
if (this.current === keyCodes.RIGHT) {
if (headY + 1 === y && headX === x) {
return true
}
}
if (this.current === keyCodes.LEFT) {
if (headY - 1 === y && headX === x) {
return true
}
}
if (this.current === keyCodes.TOP) {
if (headX - 1 === x && headY === y) {
return true
}
}
if (this.current === keyCodes.BOTTOM) {
if (headX + 1 === x && headY === y) {
return true
}
}
return false
},
createApple() {
const { cols, rows } = this.getRowsCols()
const x = getRandomIntInclusive(0, rows - 1)
const y = getRandomIntInclusive(0, cols - 1)
this.apple = {
x,
y,
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
},
getRowsCols() {
const { width: appWidth, height: appHeight } = document.querySelector('#app').getBoundingClientRect()
const item = document.querySelector('.item')
const { width, height } = item.getBoundingClientRect()
return {
cols: ~~(appHeight / height),
rows: ~~(appWidth / width),
}
},
isImpact() {
const len = this.list.length
const { x: headX, y: headY } = this.list[len - 1]
const { cols, rows } = this.getRowsCols()
if (this.list.slice(0, len - 1).some(({ x, y }) => x === headX && y === headY)) {
return true
}
if (headY >= cols) {
return true
}
if (headX >= rows) {
return true
}
if (headX < 0) {
return true
}
if (headY < 0) {
return true
}
return false
},
pause() {
clearInterval(this.timer)
},
addEventListener() {
document.addEventListener('keydown', ({ keyCode }) => {
if (this.canBack(keyCode)) {
this.current = keyCode
}
})
},
canBack(keyCode) {
const len = this.list.length
const prev = this.list[len - 2]
const head = this.list[len - 1]
if (prev.y === head.y) {
// 向下移动时,不能往上移动
if (this.current === keyCodes.BOTTOM && keyCode === keyCodes.TOP) {
return false
}
// 向上移动时,不能往下移动
if (this.current === keyCodes.TOP && keyCode === keyCodes.BOTTOM) {
return false
}
}
if (prev.x === head.x) {
// 向左移动时,不能往右移动
if (this.current === keyCodes.LEFT && keyCode === keyCodes.RIGHT) {
return false
}
// 向右移动时,不能往左移动
if (this.current === keyCodes.RIGHT && keyCode === keyCodes.LEFT) {
return false
}
}
return true
},
getPosotion({ x, y }) {
return {
top: `${x * 20}px`,
left: `${y * 20}px`,
}
},
},
}
</script>
<style lang="scss">
#app {
margin-top: 60px;
width: 500px;
height: 500px;
background: #00cc99;
border-radius: 5px;
margin: 0 auto;
position: relative;
// overflow: hidden;
}
.item,
.apple {
position: absolute;
width: 20px;
height: 20px;
background-color: red;
border-right: 1px solid #fff;
box-sizing: border-box;
border-radius: 5px;
}
.apple {
background-color: pink;
}
button {
transform: translateX(-100%);
}
.dialog {
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
.start {
background-color: green;
font-size: 16px;
padding: 5px;
cursor: pointer;
border-radius: 5px;
}
}
</style>
复制代码
最后的效果。