Vue+TailWindcss實(shí)現(xiàn)一個簡單的闖關(guān)小游戲
游戲介紹

這是一款2d益智闖關(guān)游戲,玩家須躲避敵人與陷阱到達(dá)終點(diǎn) 擁有多個關(guān)卡

可進(jìn)行關(guān)卡的自定義并留存數(shù)據(jù)

實(shí)現(xiàn)技術(shù)
vue tailwindcss
本游特色
- 自定義關(guān)卡
- 敵人自動索敵
- 低技術(shù)力
- you win!
技術(shù)實(shí)現(xiàn)
初始化頁面
創(chuàng)建一個json文件,用來存放初始關(guān)卡的變量(只有一關(guān)。。。) 為方塊設(shè)定大小,初始化變量speed設(shè)置為176,棋盤的寬高就各位4個speed,方塊寬高就是1個speed,方塊移動一格就是speed * 1,兩格就是speed * 2
<!-- 棋盤 -->
<div :style="{ width: `${speed * 4}px`, height: `${speed * 4}px` }">
<!-- 每一個小方塊 -->
<div :style="{ width: `${speed}px`,height: `${speed}px`,}"></div>
</div>const speed = ref(176);
Level是一個json文件,里面放著第一關(guān)的各種變量,用來在沒有關(guān)卡的時候初始化一個關(guān)卡
level.json
[
{
"id": 1,// 第一關(guān)
"speed": 176,// 方塊大小
"top": 528,// 主角top值
"left": 0,// 主角left值
"enemy_top": 0,// 敵人top值
"enemy_left": 352,// 敵人left值
"enemy_top_2": 528,// 敵人2的top值
"enemy_left_2": 352,// 敵人2的left值
"obstacle_top": 176,// 障礙top值
"obstacle_left": 352,// 障礙left值
"trap_top": 352,// 陷阱top值
"trap_left": 176,// 陷阱left值
"spot_top": 0,// 終點(diǎn)top值
"spot_left": 528// 終點(diǎn)left值
}
]在加載頁面的時候判斷是否有數(shù)據(jù)如果沒有的話添加
import Level from "../../api/level.json";
let res = JSON.parse(localStorage.getItem("data"));
if (!res) {
localStorage.setItem("data", JSON.stringify(Level));
}小方塊設(shè)置
使用絕對定位,用transition-all讓方塊看起來有動畫效果
<div class="absolute transition-all"></div>
為小方塊設(shè)置特定的top和left,聲明變量然后設(shè)置給小方塊上
<!-- 終點(diǎn),我用的spot前綴 -->
<div :style="{ top: `${spot_top}px`,left: `${spot_left}px` }"></div>
<!-- 敵人,我用的enemy前綴(敵人2后綴直接-2) -->
<div :style="{ top: `${enemy_top}px`,left: `${enemy_left}px` }"></div>const Level = JSON.parse(localStorage.getItem("data"));
const spot_top = ref(Level[index].spot_top);
const spot_left = ref(Level[index].spot_left);
const enemy_top = ref(Level[index].enemy_top);
const enemy_left = ref(Level[index].enemy_left);主角移動
當(dāng)按下相應(yīng)按鍵后執(zhí)行相應(yīng)的函數(shù)
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "a":
if (is_run.value) {
moveProtagonistA();
}
break;
case "w":
if (is_run.value) {
moveProtagonistW();
}
break;
case "d":
if (is_run.value) {
moveProtagonistD();
}
break;
case "s":
if (is_run.value) {
moveProtagonistS();
}
break;
case "r":
againGame();// 重新開始
break;
}
});四個函數(shù)的意思分別是主角塊上下左右的移動,本質(zhì)其實(shí)都差不多,差別就在于每個的top和left是不同的,所以咱就挑一個詳細(xì)說明一下:
當(dāng)想讓主角向左移動時
const moveProtagonistA = () => {
// 自殺判斷
if (
left.value == enemy_left.value + speed.value &&
top.value == enemy_top.value
) {
left.value -= speed.value;
return false;
}
if (left.value == 0) {
// 邊界判斷
left.value = -20;
setTimeout(() => {
left.value = 0;
}, 100);
return false;
}
// 障礙判斷
obstacle = obstacle_left.value + speed.value;
if (top.value == obstacle_top.value && left.value == obstacle) {
left.value = obstacle - 20;
setTimeout(() => {
left.value = obstacle;
}, 100);
} else {
left.value -= speed.value;
freeFindEnemy(enemy_top, enemy_left);
freeFindEnemy(enemy_top_2, enemy_left_2);
}
};函數(shù)整體的內(nèi)容有點(diǎn)小多,咱們來分開解釋:
自殺判斷
因?yàn)樵谥鹘且苿訒r,敵人的自動索敵功能也會開啟,所以導(dǎo)致當(dāng)主角向敵人移動的時候因?yàn)閿橙俗詣铀鲾车脑驎c主角錯開,于是便誕生了這個邏輯,就是判斷如果主角的下一步有敵人的話,敵人原地不動,裝上敵人game over
// 自殺判斷
if (
left.value == enemy_left.value + speed.value &&
top.value == enemy_top.value
) {
left.value -= speed.value;
return false;// 如自殺成功則阻止下面的索敵判斷
}
邊界判斷
如果出界會被攔截并且給一個被攔截的效果提示,因?yàn)檫@個示例是想左移動的時候,所以判斷條件也是左邊
if (left.value == 0) {
// 這個效果可以讓方塊回彈一下
left.value = -20;
setTimeout(() => {
left.value = 0;
}, 100);
return false;// 如果碰到邊界則阻止像下面的索敵判斷
}
障礙判斷 && 索敵
如果關(guān)卡中存在障礙的話,當(dāng)主角觸碰到障礙的時候,會跟邊界判斷擁有一樣回彈效果來提示此路不通
如果主角移動沒有碰到障礙阻攔的話,則執(zhí)行正常移動的命令并且執(zhí)行自動索敵
obstacle = obstacle_left.value + speed.value;
if (top.value == obstacle_top.value && left.value == obstacle) {
// 跟上面一樣,回彈一下
left.value = obstacle - 20;
setTimeout(() => {
left.value = obstacle;
}, 100);
} else {
left.value -= speed.value;// 移動命令
freeFindEnemy(enemy_top, enemy_left);// 敵人1的索敵
freeFindEnemy(enemy_top_2, enemy_left_2);// 敵人2的索敵
}也許你已經(jīng)看到了(朵拉擺手),在索敵的最后使用的兩個函數(shù),這個函數(shù)就是自動索敵的邏輯,接下來繼續(xù)深入~
自動索敵
當(dāng)主角移動時敵人自動索敵
// 自動索敵
const freeFindEnemy = (Etop: any, Eleft: any) => {
let _top = top.value - Etop.value;
let _left = left.value - Eleft.value;
if (Math.abs(_top) > Math.abs(_left)) {
if (_top > 0) {
moveEnemyS(Etop, Eleft);
} else {
moveEnemyW(Etop, Eleft);
}
} else {
if (_left > 0) {
moveEnemyD(Etop, Eleft);
} else {
moveEnemyA(Etop, Eleft);
}
}
};這個里面出現(xiàn)的函數(shù)moveEnemy系列是敵人方塊的方向移動,邏輯就是判斷主角距離敵人的top和left來決定敵人方塊的走向,Etop與Eleft需要分別傳入的敵人的top和left值,判斷拿邊距離大就往哪邊行動,有大于、小于等于兩種情況
由自動索敵又延申出了--敵人移動
敵人移動
敵人移動也是擁有四個函數(shù),基本與主角移動沒有區(qū)別,但是敵人在碰到障礙的時候會選擇繞開,且敵人碰到陷阱的時候會被“吃掉”
拿敵人向下移動來舉例
const moveEnemyS = (Etop: any, Eleft: any) => {
// 陷阱判斷
if (trap_top.value == Etop.value && trap_left.value == Eleft.value) return;
// 障礙檢測判斷
obstacle = obstacle_top.value - speed.value;
if (Etop.value == obstacle && Eleft.value == obstacle_left.value) {
// 判斷如果碰到障礙
let _left = left.value - Eleft.value;
if (_left > 0) {
Eleft.value += speed.value;
} else {
Eleft.value -= speed.value;
}
} else {
Etop.value += speed.value;
}
};首先是陷阱的判斷,如果敵人的top和left與陷阱一致的話則判斷敵人掉進(jìn)了陷阱里,將終止敵人的所有移動
接下來是障礙,判斷如果敵人即將要走的方向有障礙擋著的話,就去判斷與主角的距離來向左或者向右避開
勝利與失敗
在勝利和失敗后肯定是要終止所有行動的,正好所有的行動也是由主角移動的函數(shù)來觸發(fā)的,所以先聲明一個變量用來控制游戲的進(jìn)行,然后通過按鍵在判斷這個變量,如果游戲正在進(jìn)行中則觸發(fā)移動函數(shù)函數(shù),如果游戲未開始或已失敗則跳過觸發(fā)事件,即無響應(yīng)
case "a":
// is_run即聲明的變量,在游戲失敗或未開始階段該變量為false
if (is_run.value) {
moveProtagonistA();
}
break;當(dāng)勝利條件符合(即主角碰到終點(diǎn))時,觸發(fā)win,即顯示win字樣并使is_run置為false

// 主角的topleft是否與終點(diǎn)的topleft重合
if (top.value == spot_top.value && left.value == spot_left.value) {
winShow.value = true;
is_run.value = false;
}當(dāng)失敗條件符合(即主角碰到敵人1或2或者陷阱)時,觸發(fā)lose,即顯示lose字樣并使is_run置為false

if (
(top.value == enemy_top.value && left.value == enemy_left.value) ||
(top.value == enemy_top_2.value && left.value == enemy_left_2.value) ||
(top.value == trap_top.value && left.value == trap_left.value)
) {
is_run.value = false;
loseShow.value = true;
return;
}最后一個return的作用是截?cái)啵?dāng)觸發(fā)了lose后就不再繼續(xù)執(zhí)行了(否則會接著執(zhí)行win)
編輯關(guān)卡
移入移出變色
16個黑塊,通過鼠標(biāo)移入移出判斷顏色
<div
v-for="(item, index) in blockList"
:key="index"
:style="{
width: `${speed}px`,
height: `${speed}px`,
background: item.background,
}"
@mousemove="editMove($event, item)"
@mouseleave="editLeave"
class="transition-all"
></div>
<!-- transition-all使樣式變換具有過渡效果 -->const editMove = (event, item) => {
// 如果該方塊已經(jīng)被選中則什么都不做
if (!item.is_confirm) {
for (let i in blockList.value) {
// 選中相應(yīng)的方塊進(jìn)行變色
if (blockList.value[i].id == item.id) {
blockList.value[i].background = "";
} else if (blockList.value[i].is_confirm) {
blockList.value[i].background = "";
} else {
blockList.value[i].background = "#000";
}
}
}
};
const editLeave = () => {
for (let i in blockList.value) {
// 如果該方塊已經(jīng)被選中則什么都不做
if (blockList.value[i].is_confirm) {
blockList.value[i].background = "";
} else {
// 選中相應(yīng)的方塊進(jìn)行變色
blockList.value[i].background = "#000";
}
}
};因?yàn)榉綁K被設(shè)置后是不能被改變顏色的,所以需要這兩個方法對已經(jīng)被設(shè)置的方塊進(jìn)行判斷
點(diǎn)擊設(shè)置
需先點(diǎn)擊左側(cè)圖例使顏色選中,再點(diǎn)擊方塊使其變色

圖例
<div
v-for="(item, index) in legendList"
:key="index"
class="flex mb-4 items-center text-xl"
@click="colorClick($event, item)"
>
<div class="legend_sign" :class="item.color"></div>
<div class="w-10"></div>
<div
class="transition-all p-2 rounded-lg"
:class="color == item.color ? color : ''"
>
{{ item.introduce }}
</div>
</div>const legendList = [
{
id: 0,
color: "bg-green-500",
introduce: "終點(diǎn)",
},
{
id: 1,
color: "bg-red-500",
introduce: "敵人",
},
{
id: 2,
color: "bg-blue-500",
introduce: "主角",
},
{
id: 3,
color: "bg-gray-500",
introduce: "障礙",
},
{
id: 4,
color: "bg-purple-500",
introduce: "陷阱",
},
];變色邏輯
<!-- 跟移入移出變色的div是同一個div -->
<!-- 重點(diǎn)看這句::class="item.color" -->
<div
v-for="(item, index) in blockList"
:key="index"
:style="{
width: `${speed}px`,
height: `${speed}px`,
background: item.background,
}"
:class="item.color"
@click="editClick($event, item)"
@mousemove="editMove($event, item)"
@mouseleave="editLeave"
class="transition-all"
></div>const editMove = (event, item) => {
if (!item.is_confirm) {
for (let i in blockList.value) {
if (blockList.value[i].id == item.id) {
// 重點(diǎn)在這兩句
blockList.value[i].background = "";
blockList.value[i].color = color.value;
} else if (blockList.value[i].is_confirm) {
blockList.value[i].background = "";
} else {
blockList.value[i].background = "#000";
}
}
}
};
const editClick = (event, item) => {
// json添加
switch (color.value) {
case "bg-green-500":
if (json.spot_top != 9999) {
tips.value = "終點(diǎn)只能有一個";
return;
}
json.spot_top = item.top;
json.spot_left = item.left;
break;
case "bg-red-500":
if (json.enemy_top != 9999) {
if (json.enemy_top_2 != 9999) {
tips.value = "敵人只能有兩個";
return;
}
json.enemy_top_2 = item.top;
json.enemy_left_2 = item.left;
break;
}
json.enemy_top = item.top;
json.enemy_left = item.left;
break;
case "bg-blue-500":
if (json.top != 9999) {
tips.value = "主角只能有一個";
return;
}
json.top = item.top;
json.left = item.left;
break;
case "bg-gray-500":
if (json.obstacle_top != 9999) {
tips.value = "障礙只能有一個";
return;
}
json.obstacle_top = item.top;
json.obstacle_left = item.left;
break;
case "bg-purple-500":
if (json.trap_top != 9999) {
tips.value = "陷阱只能有一個";
return;
}
json.trap_top = item.top;
json.trap_left = item.left;
break;
default:
tips.value = "請先選擇顏色~";
return;
}
// 狀態(tài)保留
for (let i in blockList.value) {
if (blockList.value[i].id == item.id) {
blockList.value[i].background = "";
blockList.value[i].color = color.value;
blockList.value[i].is_confirm = true;
} else if (blockList.value[i].is_confirm) {
blockList.value[i].background = "";
} else {
blockList.value[i].background = "#000";
}
}
};首先是通過點(diǎn)擊圖例來保存顏色,然后在鼠標(biāo)移入黑塊的時候不再是白色,而是選中的顏色,在點(diǎn)擊的時候能將顏色固定到黑塊上
因?yàn)閟tyle的優(yōu)先級要比class大(background比bg-red-500大),所以在懸浮時需要將背景顏色去掉:
blockList.value[i].background = ""; blockList.value[i].color = color.value;
在點(diǎn)擊的時候需要保留這個顏色,所以在點(diǎn)擊的時候要將本來的顏色改變,并且在懸浮上去后不會變色
blockList.value[i].background = ""; blockList.value[i].color = color.value; blockList.value[i].is_confirm = true;
is_confirm在上面已經(jīng)出現(xiàn)過一兩次,表示的是這個塊是否被設(shè)置,如果被設(shè)置了則不對它做任何操作
const editMove = (event, item) => {
if (!item.is_confirm) {
...
}
};保存關(guān)卡
對每個被設(shè)置的塊記住位置,在點(diǎn)擊保存關(guān)卡的時候?qū)⑺诺奖镜卮鎯铮@樣一個新的關(guān)卡就生成了
【gif保存關(guān)卡】
初始時將所有top left全都設(shè)置為9999,在點(diǎn)擊方塊的時候記錄方塊的top left和顏色來向一個數(shù)組中傳入數(shù)據(jù),并且對塊的數(shù)量做出限制,這里拿主角來舉例:
switch(color.value){
case "bg-blue-500":
if (json.top != 9999) {
tips.value = "主角只能有一個";
return;
}
// 將主角的top lef填入對應(yīng)的地方
json.top = item.top;
json.left = item.left;
break;
}在點(diǎn)擊保存關(guān)卡時將數(shù)組添加進(jìn)本地存儲
const Level = JSON.parse(localStorage.getItem("data"));
let json = {
id: Level.length + 1,
speed: 176,
top: 9999,
left: 9999,
enemy_top: 9999,
enemy_left: 9999,
enemy_top_2: 9999,
enemy_left_2: 9999,
obstacle_top: 9999,
obstacle_left: 9999,
trap_top: 9999,
trap_left: 9999,
spot_top: 9999,
spot_left: 9999,
};
...
const saveClick = () => {
Level.push(json);
localStorage.setItem("data", JSON.stringify(Level));
button_text.value = "保存成功";
router.push("/main");
};以上就是Vue+TailWindcss實(shí)現(xiàn)一個簡單的闖關(guān)小游戲的詳細(xì)內(nèi)容,更多關(guān)于Vue TailWindcss闖關(guān)游戲的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+webpack實(shí)現(xiàn)異步加載三種用法示例詳解
這篇文章主要介紹了vue+webpack實(shí)現(xiàn)異步加載的三種用法,文中給大家提到了vue+webpack實(shí)現(xiàn)異步組件加載的代碼,非常不錯,具有參考借鑒價(jià)值,需要的朋友參考下吧2018-04-04

