基于Vue實現(xiàn)簡單的貪食蛇游戲
貪食蛇是一個非常經(jīng)典的游戲, 在游戲中, 玩家操控一條細長的直線(俗稱蛇或蟲), 它會不停前進, 玩家只能操控蛇的頭部朝向(上下左右), 一路拾起觸碰到之物(或稱作“豆”), 并要避免觸碰到自身或者邊界. 每次貪吃蛇吃掉一件食物, 它的身體便增長一些.
本項目使用的技術(shù)棧和標題一樣非常的簡單, 只有用到 vue, 主要實現(xiàn)使用的是 HTML + CSS 動畫
代碼實現(xiàn)可以參考: CodeSandbox
實現(xiàn)游戲棋盤
在游戲描述中有提到, 玩家操縱的蛇要避免觸碰到自身或者邊界. 這就需要我們實現(xiàn)一個有邊界的游戲棋盤.
在 html 中, 我們可以使用 css 的 width、border 和 height 屬性來實現(xiàn)一個簡單的、具有邊界的容器:
在 App.vue 中的實現(xiàn)(功能節(jié)選)
<template>
<div class="game-box"></div>
</template>
<style>
body {
display: flex;
width: 100vw;
height: 100vh;
margin: 0;
}
.game-box {
position: relative;
width: 500px;
height: 500px;
border: 1px solid #ddd;
margin: auto;
}
</style>其中 position: relative; 是為了之后的 position: absolute 元素能夠在游戲棋盤中的顯示正確的位置.
實現(xiàn)蛇與豆的實體
展示豆的方式可以使用一個 div 元素, 使用 position: absolute 與 left、top 屬性來實現(xiàn)豆的位置:
在 App.vue 中的實現(xiàn)(功能節(jié)選)
<template>
<div class="game-box">
<div class="snake-food" :style="{ top: foodPos.y + 'px', left: foodPos.x + 'px' }" />
</div>
</template>
<script>
export default {
data() {
return {
foodPos: {},
};
},
};
</script>
<style>
.snake-foot {
position: absolute;
/* 保證初始位置不可見 */
top: -9999px;
left: -9999px;
width: 10px;
height: 10px;
/* 你也可以與眾不同 */
background-color: rgb(207, 38, 38);
z-index: 2;
}
</style>實現(xiàn)蛇就需要稍稍拆解一下需求. 我們知道蛇在吃了豆之后, 就會增長一些. 這看起來就像是一條單向的鏈表, 在蛇吃到豆之后便插入一條. 而且插入數(shù)據(jù)的部分只有在其尾部, 并不需要鏈表的便捷插入特性, 所以我們可以使用一個保存位置信息的數(shù)組來實現(xiàn)蛇的身體. 并且獨立出蛇的頭部來引導蛇的移動. 在這里我們保留了指向尾部的引用, 以便在蛇吃到豆之后, 可以快速的將新的蛇尾插入到最后:
在 App.vue 中的實現(xiàn)(功能節(jié)選)
<template>
<div class="game-box">
<div ref="snake" class="snake">
<!-- 蛇的頭部用來引導蛇的移動 -->
<div :style="{ top: headerPos.y + 'px', left: headerPos.x + 'px' }" ref="snakeHeader" class="snake-header" />
<!-- 蛇的身體, 使用連續(xù)的數(shù)組實現(xiàn) -->
<div
:key="uuid"
:uid="uuid"
v-for="{ pos: { y, x }, uuid } in snakeBodyList"
:style="{ top: y + 'px', left: x + 'px' }"
class="snake-body"
/>
</div>
</div>
</template>
<script>
// 蛇身的大小單位
const defaultUnit = 10;
function updatePos(pos, direction) {
// 規(guī)避引用
const newPos = { ...pos };
switch (direction) {
case directionKeys.up:
newPos.y -= defaultUnit;
break;
case directionKeys.down:
newPos.y += defaultUnit;
break;
case directionKeys.left:
newPos.x -= defaultUnit;
break;
case directionKeys.right:
newPos.x += defaultUnit;
break;
default:
throw new Error('NotFind');
}
return newPos;
}
export default {
data() {
return {
// 蛇身自增的 uuid
id: 0,
// 蛇的頭部位置
headerPos: {},
// 保存尾部的位置信息
lastPos: {},
// 保存蛇的身體位置信息
snakeBodyList: [],
};
},
methods: {
init() {
// 初始化數(shù)據(jù)
const initData = { x: 250, y: 250 };
this.direction = directionKeys.left;
this.lastPos = { ...initData, direction: this.direction };
this.headerPos = { ...initData };
this.snakeBodyList = Array(defaultUnit).fill(0).map(this.createBody);
},
createBody() {
const { x, y } = this.lastPos;
// 判斷是否屬于同水平方向
const isLower = this.direction === directionKeys.up || this.direction === directionKeys.left;
const pos = {
// 同水平方向剛好差 2 的數(shù)值, 40 - 38 = 2, 39 - 37 = 2
...updatePos({ x, y }, isLower ? this.direction + 2 : this.direction - 2),
};
// 保存尾部的位置信息
this.lastPos = pos;
return {
uuid: this.id++,
pos,
};
},
},
};
</script>當我們需要添加新的蛇身時, 只需要調(diào)用 createBody 方法, 并將其添加至蛇的身體數(shù)組尾部即可:
// 使用push方法添加蛇身至身體數(shù)組尾部 this.snakeBodyList.push(this.createBody());
實現(xiàn)蛇的移動方向(輸入控制)
我們知道, 用戶在鍵入一個按鍵時, 如果我們有監(jiān)聽 keydown 事件, 瀏覽器會觸發(fā)回調(diào)函數(shù)并提供一個KeyboardEvent 對象. 當我們要使用鍵盤來控制蛇的移動方向時, 就可以使用該事件對象的 keyCode 屬性來獲取鍵盤按鍵的編碼.
其中 keyCode 屬性的值可以參考 鍵盤編碼.
實現(xiàn)這個功能我們可以在全局對象 window 上添加一個 keydown 事件監(jiān)聽函數(shù), 并將鍵盤按鍵的編碼保存在實例中, 考慮到用戶可能會輸入多個鍵盤按鍵, 所以我們需要檢查是否為方向鍵, 并且跳過同一個水平方向上的輸入:
在 App.vue 中的實現(xiàn)(功能節(jié)選)
<script>
// 方向鍵的鍵盤按鍵的編碼
const directionKeys = {
up: 38,
down: 40,
left: 37,
right: 39,
};
// 檢查是否在水平方向上
function checkIsLevel(direction) {
return direction === directionKeys.right || direction === directionKeys.left;
}
export default {
data () {
return {
// 當前的方向鍵的編碼
direction: undefined,
// 最終輸入的方向鍵的編碼
lastInputDirection: undefined,
}
}
mounted() {
window.addEventListener('keydown', this.onKeydown);
},
methods: {
onKeydown(e) {
if (
// 檢查是否為方向鍵
![38, 40, 37, 39].includes(keyCode) ||
// 檢查是否在同一個水平方向上
checkIsLevel(keyCode) === checkIsLevel(this.direction)
) {
return;
}
// 保存輸入的方向
this.lastInputDirection = keyCode;
},
},
};
</script>碰撞檢測
游戲要求玩家避免觸碰到自身或者邊界, 我們自然而然的就需要去檢測它們是否發(fā)生了碰撞.
檢測與自身碰撞的方法是, 判斷蛇頭的位置是否與蛇身體的位置相同:
// 檢測是否發(fā)生碰撞
function isRepeat(list, pos) {
return list.some(({ pos: itemPos }) => pos.x === itemPos.x && pos.y === itemPos.y);
}
// 使用的地方傳入蛇身體數(shù)組和蛇頭的位置
isRepeat(snakeBodyList, headerPos);而檢測與邊界碰撞的方法是, 判斷蛇頭的位置是否超出了游戲區(qū)域:
const MAX_X = 500;
const MAX_Y = 500;
// 檢測是否超出邊界
function isCrossedLine(x, y) {
// 因為是使用position, 我們的位置計算需要考慮到 { x: 0, y: 0 } 的位置不為邊界
return x >= MAX_X || x < 0 || y >= MAX_Y || y < 0;
}當蛇頭的位置將要超出了游戲區(qū)域或者與蛇身體的位置相同時, 游戲結(jié)束:
const next = updatePos(this.headerPos, this.direction);
if (isCrossedLine(next.x, next.y) || isRepeat(this.snakeBodyList, next)) {
alert('你輸了');
return;
}實現(xiàn)渲染動畫
為了寫出渲染動畫, 我們需要嘗試理解蛇的運動方式.
當玩家輸入操作的時候, 蛇會根據(jù)用戶輸入的方向進行移動, 在這個過程中蛇頭的位置會發(fā)生變化, 而蛇身體的位置也會隨之發(fā)生變化. 仔細觀察可以發(fā)現(xiàn), 其實不斷變化的每個蛇身就是將它的位置替換成上一個蛇身的位置:
let head = this.headerPos;
const snakeBodyList = this.snakeBodyList;
for (const body of snakeBodyList) {
const nextPos = body.pos;
body.pos = head;
head = nextPos;
}除了這種逐步更新的方式也可以使用更簡單的直接更新數(shù)組的方式, 比如:
這樣會使 uuid 無法更新, vue 不會重新渲染 DOM, 導致 transition 無法生效
// 移除蛇尾
const snakeBodyList = this.snakeBodyList.slice(0, this.snakeBodyList.length - 1);
// 添加當前的蛇頭至蛇身的最前方
snakeBodyList.unshift({ pos: this.headerPos, uuid: this.id++ });而當蛇頭觸碰到豆的時候, 豆會被消除并且延長蛇身:
if (isRepeat(snakeBodyList, this.foodPos)) {
snakeBodyList.push(this.createBody());
}有了檢測邏輯, 我們再將動畫添加上. 因為蛇是一步一步的移動, 所以可以使用 setTimeout 來實現(xiàn)動畫:
render 函數(shù)最終會掛載在 vue 實例上
function render() {
const next = updatePos(this.headerPos, this.lastInputDirection);
if (isCrossedLine(next.x, next.y) || isRepeat(this.snakeBodyList, next)) {
clearTimeout(this._timer);
alert('你輸了');
return;
}
const snakeBodyList = this.snakeBodyList.slice(0, this.snakeBodyList.length - 1);
snakeBodyList.unshift({ pos: this.headerPos, uuid: this.id++ });
this.headerPos = next;
this.lastPos = snakeBodyList[snakeBodyList.length - 1].pos;
if (isRepeat(snakeBodyList, this.foodPos)) {
snakeBodyList.push(this.createBody());
}
this.snakeBodyList = snakeBodyList;
this.direction = this.lastInputDirection;
this._timer = setTimeout(() => this.render(), 100);
}最后的潤色
我們添加一下生成豆的方法, 并且保證它的位置不會出現(xiàn)在游戲區(qū)域的邊界或者蛇身體的位置上:
genFoot 函數(shù)最終會掛載在 vue 實例上
// 生成隨機數(shù)
function genRandom(max, start) {
return start + (((Math.random() * (max - start)) / start) >>> 0) * start;
}
// 隨機生成豆的位置
function genFoot() {
const x = genRandom(MAX_X, defaultUnit);
const y = genRandom(MAX_Y, defaultUnit);
// 如果出現(xiàn)在游戲區(qū)域的邊界或者蛇身體的位置上則重新生成
if (isRepeat(this.snakeBodyList, { x, y }) || isCrossedLine(x, y)) {
this.genFoot();
} else {
this.foodPos = { x, y };
}
}
// 添加到render方法中
function render() {
// ...
if (isRepeat(snakeBodyList, this.foodPos)) {
snakeBodyList.push(this.createBody());
this.genFoot();
}
// ...
}再添加一下開始與結(jié)束游戲, 以及一些展示當前蛇的信息的地方:
在 App.vue 中的實現(xiàn)(功能節(jié)選)
<template>
<div class="game-box">
<div class="tools">
<button @click="playGame">
{{ isPlaying ? '停止' : isLose ? '重新開始' : '開始' }}
</button>
<div class="info-bar">
<p>?? 的長度: {{ snakeBodyList.length }}</p>
</div>
<p class="count">得分: {{ count }}</p>
</div>
</div>
</template>
<script>
export default {
data: () => ({
// 游戲狀態(tài)
isPlaying: false,
// 是否失敗
isLose: false,
// 蛇的步行速度
speed: 100,
}),
methods: {
playGame() {
if (this.isPlaying) {
clearTimeout(this._timer);
} else {
this.isLose = false;
this.init();
this.genFoot();
this.render();
}
this.isPlaying = !this.isPlaying;
},
},
};
</script>這樣我們就使用 vue 實現(xiàn)了一個簡單的貪吃蛇游戲了.
效果圖

以上就是基于Vue實現(xiàn)簡單的貪食蛇游戲的詳細內(nèi)容,更多關(guān)于Vue貪食蛇游戲的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在vue+element ui框架里實現(xiàn)lodash的debounce防抖
今天小編就為大家分享一篇在vue+element ui框架里實現(xiàn)lodash的debounce防抖,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11

