溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

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

發(fā)布時(shí)間:2022-04-11 15:38:25 來源:億速云 閱讀:121 作者:iii 欄目:開發(fā)技術(shù)

今天小編給大家分享一下Vue+TailWindcss怎么實(shí)現(xiàn)一個(gè)簡單的闖關(guān)小游戲的相關(guān)知識點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

游戲介紹

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

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

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

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

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

實(shí)現(xiàn)技術(shù)

vue tailwindcss

本游特色

  • 自定義關(guān)卡

  • 敵人自動(dòng)索敵

  • 低技術(shù)力

  • you win!

技術(shù)實(shí)現(xiàn)

初始化頁面

創(chuàng)建一個(gè)json文件,用來存放初始關(guān)卡的變量(只有一關(guān)。。。) 為方塊設(shè)定大小,初始化變量speed設(shè)置為176,棋盤的寬高就各位4個(gè)speed,方塊寬高就是1個(gè)speed,方塊移動(dòng)一格就是speed * 1,兩格就是speed * 2

<!-- 棋盤 -->
<div :>
    <!-- 每一個(gè)小方塊 -->
    <div :></div>
</div>
const speed = ref(176);

Level是一個(gè)json文件,里面放著第一關(guān)的各種變量,用來在沒有關(guān)卡的時(shí)候初始化一個(gè)關(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í)候判斷是否有數(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讓方塊看起來有動(dòng)畫效果

<div class="absolute transition-all"></div>

為小方塊設(shè)置特定的top和left,聲明變量然后設(shè)置給小方塊上

<!-- 終點(diǎn),我用的spot前綴 -->
<div :></div>
<!-- 敵人,我用的enemy前綴(敵人2后綴直接-2) -->
<div :></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)

當(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;
  }
});

四個(gè)函數(shù)的意思分別是主角塊上下左右的移動(dòng),本質(zhì)其實(shí)都差不多,差別就在于每個(gè)的top和left是不同的,所以咱就挑一個(gè)詳細(xì)說明一下:

當(dāng)想讓主角向左移動(dòng)時(shí)

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)樵谥鹘且苿?dòng)時(shí),敵人的自動(dòng)索敵功能也會開啟,所以導(dǎo)致當(dāng)主角向敵人移動(dòng)的時(shí)候因?yàn)閿橙俗詣?dòng)索敵的原因會與主角錯(cuò)開,于是便誕生了這個(gè)邏輯,就是判斷如果主角的下一步有敵人的話,敵人原地不動(dòng),裝上敵人game over

 // 自殺判斷
  if (
    left.value == enemy_left.value + speed.value &&
    top.value == enemy_top.value
  ) {
    left.value -= speed.value;
    return false;// 如自殺成功則阻止下面的索敵判斷
  }

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

邊界判斷

如果出界會被攔截并且給一個(gè)被攔截的效果提示,因?yàn)檫@個(gè)示例是想左移動(dòng)的時(shí)候,所以判斷條件也是左邊

if (left.value == 0) {
    // 這個(gè)效果可以讓方塊回彈一下
    left.value = -20;
    setTimeout(() => {
      left.value = 0;
    }, 100);
    return false;// 如果碰到邊界則阻止像下面的索敵判斷
  }

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

障礙判斷 && 索敵

如果關(guān)卡中存在障礙的話,當(dāng)主角觸碰到障礙的時(shí)候,會跟邊界判斷擁有一樣回彈效果來提示此路不通

如果主角移動(dòng)沒有碰到障礙阻攔的話,則執(zhí)行正常移動(dòng)的命令并且執(zhí)行自動(dòng)索敵

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;// 移動(dòng)命令
  freeFindEnemy(enemy_top, enemy_left);// 敵人1的索敵
  freeFindEnemy(enemy_top_2, enemy_left_2);// 敵人2的索敵
}

也許你已經(jīng)看到了(朵拉擺手),在索敵的最后使用的兩個(gè)函數(shù),這個(gè)函數(shù)就是自動(dòng)索敵的邏輯,接下來繼續(xù)深入~

自動(dòng)索敵

當(dāng)主角移動(dòng)時(shí)敵人自動(dòng)索敵

// 自動(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);
    }
  }
};

這個(gè)里面出現(xiàn)的函數(shù)moveEnemy系列是敵人方塊的方向移動(dòng),邏輯就是判斷主角距離敵人的top和left來決定敵人方塊的走向,Etop與Eleft需要分別傳入的敵人的top和left值,判斷拿邊距離大就往哪邊行動(dòng),有大于、小于等于兩種情況

由自動(dòng)索敵又延申出了--敵人移動(dòng)

敵人移動(dòng)

敵人移動(dòng)也是擁有四個(gè)函數(shù),基本與主角移動(dòng)沒有區(qū)別,但是敵人在碰到障礙的時(shí)候會選擇繞開,且敵人碰到陷阱的時(shí)候會被“吃掉”

拿敵人向下移動(dòng)來舉例

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)了陷阱里,將終止敵人的所有移動(dòng)

接下來是障礙,判斷如果敵人即將要走的方向有障礙擋著的話,就去判斷與主角的距離來向左或者向右避開

勝利與失敗

在勝利和失敗后肯定是要終止所有行動(dòng)的,正好所有的行動(dòng)也是由主角移動(dòng)的函數(shù)來觸發(fā)的,所以先聲明一個(gè)變量用來控制游戲的進(jìn)行,然后通過按鍵在判斷這個(gè)變量,如果游戲正在進(jìn)行中則觸發(fā)移動(dòng)函數(shù)函數(shù),如果游戲未開始或已失敗則跳過觸發(fā)事件,即無響應(yīng)

case "a":
  // is_run即聲明的變量,在游戲失敗或未開始階段該變量為false
  if (is_run.value) {
    moveProtagonistA();
  }
  break;

當(dāng)勝利條件符合(即主角碰到終點(diǎn))時(shí),觸發(fā)win,即顯示win字樣并使is_run置為false

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

// 主角的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或者陷阱)時(shí),觸發(fā)lose,即顯示lose字樣并使is_run置為false

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

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;
  }

最后一個(gè)return的作用是截?cái)?,?dāng)觸發(fā)了lose后就不再繼續(xù)執(zhí)行了(否則會接著執(zhí)行win)

編輯關(guān)卡

移入移出變色

16個(gè)黑塊,通過鼠標(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è)置后是不能被改變顏色的,所以需要這兩個(gè)方法對已經(jīng)被設(shè)置的方塊進(jìn)行判斷

點(diǎn)擊設(shè)置

需先點(diǎn)擊左側(cè)圖例使顏色選中,再點(diǎn)擊方塊使其變色

Vue+TailWindcss怎么實(shí)現(xiàn)一個(gè)簡單的闖關(guā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是同一個(gè)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)只能有一個(gè)";
        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 = "敵人只能有兩個(gè)";
          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 = "主角只能有一個(gè)";
        return;
      }
      json.top = item.top;
      json.left = item.left;
      break;
    case "bg-gray-500":
      if (json.obstacle_top != 9999) {
        tips.value = "障礙只能有一個(gè)";
        return;
      }
      json.obstacle_top = item.top;
      json.obstacle_left = item.left;
      break;
    case "bg-purple-500":
      if (json.trap_top != 9999) {
        tips.value = "陷阱只能有一個(gè)";
        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)移入黑塊的時(shí)候不再是白色,而是選中的顏色,在點(diǎn)擊的時(shí)候能將顏色固定到黑塊上

因?yàn)閟tyle的優(yōu)先級要比class大(background比bg-red-500大),所以在懸浮時(shí)需要將背景顏色去掉:

blockList.value[i].background = "";
blockList.value[i].color = color.value;

在點(diǎn)擊的時(shí)候需要保留這個(gè)顏色,所以在點(diǎn)擊的時(shí)候要將本來的顏色改變,并且在懸浮上去后不會變色

blockList.value[i].background = "";
blockList.value[i].color = color.value;
blockList.value[i].is_confirm = true;

is_confirm在上面已經(jīng)出現(xiàn)過一兩次,表示的是這個(gè)塊是否被設(shè)置,如果被設(shè)置了則不對它做任何操作

const editMove = (event, item) => {
  if (!item.is_confirm) {
    ...
  }
};

保存關(guān)卡

對每個(gè)被設(shè)置的塊記住位置,在點(diǎn)擊保存關(guān)卡的時(shí)候?qū)⑺诺奖镜卮鎯?,這樣一個(gè)新的關(guān)卡就生成了

【gif保存關(guān)卡】

初始時(shí)將所有top left全都設(shè)置為9999,在點(diǎn)擊方塊的時(shí)候記錄方塊的top left和顏色來向一個(gè)數(shù)組中傳入數(shù)據(jù),并且對塊的數(shù)量做出限制,這里拿主角來舉例:

switch(color.value){
  case "bg-blue-500":
      if (json.top != 9999) {
        tips.value = "主角只能有一個(gè)";
        return;
      }
      // 將主角的top lef填入對應(yīng)的地方
      json.top = item.top;
      json.left = item.left;
      break;
}

在點(diǎn)擊保存關(guān)卡時(shí)將數(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)一個(gè)簡單的闖關(guān)小游戲”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI