基于Vue實(shí)現(xiàn)簡單的貪食蛇游戲
貪食蛇是一個(gè)非常經(jīng)典的游戲, 在游戲中, 玩家操控一條細(xì)長的直線(俗稱蛇或蟲), 它會(huì)不停前進(jìn), 玩家只能操控蛇的頭部朝向(上下左右), 一路拾起觸碰到之物(或稱作“豆”), 并要避免觸碰到自身或者邊界. 每次貪吃蛇吃掉一件食物, 它的身體便增長一些.
本項(xiàng)目使用的技術(shù)棧和標(biāo)題一樣非常的簡單, 只有用到 vue, 主要實(shí)現(xiàn)使用的是 HTML + CSS 動(dòng)畫
代碼實(shí)現(xiàn)可以參考: CodeSandbox
實(shí)現(xiàn)游戲棋盤
在游戲描述中有提到, 玩家操縱的蛇要避免觸碰到自身或者邊界. 這就需要我們實(shí)現(xiàn)一個(gè)有邊界的游戲棋盤.
在 html 中, 我們可以使用 css 的 width、border 和 height 屬性來實(shí)現(xiàn)一個(gè)簡單的、具有邊界的容器:
在 App.vue 中的實(shí)現(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
元素能夠在游戲棋盤中的顯示正確的位置.
實(shí)現(xiàn)蛇與豆的實(shí)體
展示豆的方式可以使用一個(gè) div 元素, 使用 position: absolute 與 left、top 屬性來實(shí)現(xiàn)豆的位置:
在 App.vue 中的實(shí)現(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>
實(shí)現(xiàn)蛇就需要稍稍拆解一下需求. 我們知道蛇在吃了豆之后, 就會(huì)增長一些. 這看起來就像是一條單向的鏈表, 在蛇吃到豆之后便插入一條. 而且插入數(shù)據(jù)的部分只有在其尾部, 并不需要鏈表的便捷插入特性, 所以我們可以使用一個(gè)保存位置信息的數(shù)組來實(shí)現(xiàn)蛇的身體. 并且獨(dú)立出蛇的頭部來引導(dǎo)蛇的移動(dòng). 在這里我們保留了指向尾部的引用, 以便在蛇吃到豆之后, 可以快速的將新的蛇尾插入到最后:
在 App.vue 中的實(shí)現(xiàn)(功能節(jié)選)
<template> <div class="game-box"> <div ref="snake" class="snake"> <!-- 蛇的頭部用來引導(dǎo)蛇的移動(dòng) --> <div :style="{ top: headerPos.y + 'px', left: headerPos.x + 'px' }" ref="snakeHeader" class="snake-header" /> <!-- 蛇的身體, 使用連續(xù)的數(shù)組實(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>
當(dāng)我們需要添加新的蛇身時(shí), 只需要調(diào)用 createBody
方法, 并將其添加至蛇的身體數(shù)組尾部即可:
// 使用push方法添加蛇身至身體數(shù)組尾部 this.snakeBodyList.push(this.createBody());
實(shí)現(xiàn)蛇的移動(dòng)方向(輸入控制)
我們知道, 用戶在鍵入一個(gè)按鍵時(shí), 如果我們有監(jiān)聽 keydown
事件, 瀏覽器會(huì)觸發(fā)回調(diào)函數(shù)并提供一個(gè)KeyboardEvent 對象. 當(dāng)我們要使用鍵盤來控制蛇的移動(dòng)方向時(shí), 就可以使用該事件對象的 keyCode
屬性來獲取鍵盤按鍵的編碼.
其中 keyCode
屬性的值可以參考 鍵盤編碼.
實(shí)現(xiàn)這個(gè)功能我們可以在全局對象 window 上添加一個(gè) keydown
事件監(jiān)聽函數(shù), 并將鍵盤按鍵的編碼保存在實(shí)例中, 考慮到用戶可能會(huì)輸入多個(gè)鍵盤按鍵, 所以我們需要檢查是否為方向鍵, 并且跳過同一個(gè)水平方向上的輸入:
在 App.vue 中的實(shí)現(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 { // 當(dāng)前的方向鍵的編碼 direction: undefined, // 最終輸入的方向鍵的編碼 lastInputDirection: undefined, } } mounted() { window.addEventListener('keydown', this.onKeydown); }, methods: { onKeydown(e) { if ( // 檢查是否為方向鍵 ![38, 40, 37, 39].includes(keyCode) || // 檢查是否在同一個(gè)水平方向上 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) { // 因?yàn)槭鞘褂胮osition, 我們的位置計(jì)算需要考慮到 { x: 0, y: 0 } 的位置不為邊界 return x >= MAX_X || x < 0 || y >= MAX_Y || y < 0; }
當(dāng)蛇頭的位置將要超出了游戲區(qū)域或者與蛇身體的位置相同時(shí), 游戲結(jié)束:
const next = updatePos(this.headerPos, this.direction); if (isCrossedLine(next.x, next.y) || isRepeat(this.snakeBodyList, next)) { alert('你輸了'); return; }
實(shí)現(xiàn)渲染動(dòng)畫
為了寫出渲染動(dòng)畫, 我們需要嘗試?yán)斫馍叩倪\(yùn)動(dòng)方式.
當(dāng)玩家輸入操作的時(shí)候, 蛇會(huì)根據(jù)用戶輸入的方向進(jìn)行移動(dòng), 在這個(gè)過程中蛇頭的位置會(huì)發(fā)生變化, 而蛇身體的位置也會(huì)隨之發(fā)生變化. 仔細(xì)觀察可以發(fā)現(xiàn), 其實(shí)不斷變化的每個(gè)蛇身就是將它的位置替換成上一個(gè)蛇身的位置:
let head = this.headerPos; const snakeBodyList = this.snakeBodyList; for (const body of snakeBodyList) { const nextPos = body.pos; body.pos = head; head = nextPos; }
除了這種逐步更新的方式也可以使用更簡單的直接更新數(shù)組的方式, 比如:
這樣會(huì)使 uuid 無法更新, vue 不會(huì)重新渲染 DOM, 導(dǎo)致 transition 無法生效
// 移除蛇尾 const snakeBodyList = this.snakeBodyList.slice(0, this.snakeBodyList.length - 1); // 添加當(dāng)前的蛇頭至蛇身的最前方 snakeBodyList.unshift({ pos: this.headerPos, uuid: this.id++ });
而當(dāng)蛇頭觸碰到豆的時(shí)候, 豆會(huì)被消除并且延長蛇身:
if (isRepeat(snakeBodyList, this.foodPos)) { snakeBodyList.push(this.createBody()); }
有了檢測邏輯, 我們再將動(dòng)畫添加上. 因?yàn)樯呤且徊揭徊降囊苿?dòng), 所以可以使用 setTimeout 來實(shí)現(xiàn)動(dòng)畫:
render 函數(shù)最終會(huì)掛載在 vue 實(shí)例上
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); }
最后的潤色
我們添加一下生成豆的方法, 并且保證它的位置不會(huì)出現(xiàn)在游戲區(qū)域的邊界或者蛇身體的位置上:
genFoot 函數(shù)最終會(huì)掛載在 vue 實(shí)例上
// 生成隨機(jī)數(shù) function genRandom(max, start) { return start + (((Math.random() * (max - start)) / start) >>> 0) * start; } // 隨機(jī)生成豆的位置 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é)束游戲, 以及一些展示當(dāng)前蛇的信息的地方:
在 App.vue 中的實(shí)現(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 實(shí)現(xiàn)了一個(gè)簡單的貪吃蛇游戲了.
效果圖
以上就是基于Vue實(shí)現(xiàn)簡單的貪食蛇游戲的詳細(xì)內(nèi)容,更多關(guān)于Vue貪食蛇游戲的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue 組件修改根實(shí)例的數(shù)據(jù)的方法
這篇文章主要介紹了Vue 組件修改根實(shí)例的數(shù)據(jù)的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Vue動(dòng)態(tài)綁定Class的幾種常用方式
在vue框架開發(fā)中,有時(shí)候我們需要對元素的樣式進(jìn)行動(dòng)態(tài)控制,比如tab按鈕的切換,下面這篇文章主要給大家介紹了關(guān)于Vue動(dòng)態(tài)綁定Class的幾種常用方式,需要的朋友可以參考下2023-03-03vue實(shí)現(xiàn)點(diǎn)擊當(dāng)前標(biāo)簽高亮效果【推薦】
這篇文章主要介紹了vue實(shí)現(xiàn)點(diǎn)擊當(dāng)前標(biāo)簽高亮效果的思路詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-06-06在vue+element ui框架里實(shí)現(xiàn)lodash的debounce防抖
今天小編就為大家分享一篇在vue+element ui框架里實(shí)現(xiàn)lodash的debounce防抖,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11