iT邦幫忙

2021 iThome 鐵人賽

DAY 30
1
自我挑戰組

JS30 學習日記系列 第 30

Day 30 - [打地鼠] Make a Whack A Mole Game with Vanilla JS

前言

JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No FrameworksNo CompilersNo LibrariesNo Boilerplate 在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。

另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。


本日目標

最後一天要實作的內容是"網頁版打地鼠"。(下面是實際網頁效果的gif圖)


解析程式碼

HTML 部分

<h1>是頁面的標題,旁邊的<span class="score">是打地鼠遊戲的記分板。

<button>在滑鼠點擊時,會呼叫startGame()方法,開始打地鼠遊戲。

<div class="game">是打地鼠遊戲的主體,內部有<dvi class="hole">共計六個洞,每個洞(.hole)都有一隻初始被隱藏的地鼠(<div class="mole">)。

<h1>Whack-a-mole! <span class="score">0</span></h1>
<button onClick="startGame()">Start!</button>

<div class="game">
    <div class="hole hole1">
      <div class="mole"></div>
    </div>
    <div class="hole hole2">
      <div class="mole"></div>
    </div>
    <div class="hole hole3">
      <div class="mole"></div>
    </div>
    <div class="hole hole4">
      <div class="mole"></div>
    </div>
    <div class="hole hole5">
      <div class="mole"></div>
    </div>
    <div class="hole hole6">
      <div class="mole"></div>
    </div>
</div>

CSS 部分

初始每一隻地鼠(.mole)都是採用絕對定位(position:absolute),然後將top指定為100%,把地鼠(.mole)隱藏起來。

在遊戲過程中,我們會為地洞(.hole)添加.up這個 CSS class 選擇器,讓地鼠探頭出來給我們打 XD。

.mole {
  /*上略...*/
  position: absolute;
  top: 100%;
  /*下略...*/
}

.hole.up .mole {
  top: 0;
}

JS 部分

宣告常數holes取得所有的地洞(.hole),資料型態是 NodeList。

宣告常數scoreBoard取得頁面中顯示的分數(.score)。

宣告常數mole取得所有的地鼠(.mole),資料型態是 NodeList。

const holes = document.querySelectorAll('.hole'); //NodeList
const scoreBoard = document.querySelector('.score');
const moles = document.querySelectorAll('.mole');

撰寫ranTime()幫我們決定地鼠出現的持續時間並給定參數minmax作為出現持續時間範圍的最小、最大值。

下面用Math.random()隨機產生一個介於0~1的數字,然後把它乘上(max-min)再加上min,最後用Math.round()四捨五入得到隨機的出現持續時間。

function randTime(min,max){
    return Math.round(Math.random() * (max-min) + min);
}

randomHole()幫我們隨機選擇地鼠出現的洞,為避免接連選到兩次一樣的洞,所以另外宣告變數lastHole,來幫我們記住上一次出現的洞。

在方法裡,首先要傳入所有洞穴(holes,NodeList),接著一樣用Math.random()隨機產生0~1的數字並乘上holes的長度後,呼叫Math.floor()無條件捨去小數點,取得一個隨機的index放入常數idx中。

然後,宣告的常數hole就可以用這個idx,隨機取得holes中的一個洞穴。

為避免選到和上次一樣的洞穴,利用條件判斷hole是不是跟lastHole相同,如果相同就遞迴呼叫randomHole(),直至選到不同的洞為止。

在方法的最後,把這次的結果放到lastHole中,之後回傳被隨機選到的hole

let lastHole;

function randomHole(holes){
    const idx = Math.floor(Math.random() * holes.length);
    const hole = holes[idx]
    if(hole === lastHole){
      console.log('You got the same hole.');
      return randomHole(holes);
    }
    lastHole = hole;
    return hole;
}

宣告變數timeup作為遊戲是否已經結束的flag

peep()是讓地鼠從洞穴探頭出來的關鍵!!!

peep()的最一開始,宣告常數time並取得由randTime(200,1000)隨機產生的地鼠持續出現時間,接著宣告常數hole取得由randomHole(holes)隨機挑出的地洞。

隨機挑出的hole會被添加.up這個 class,目的是讓躲在洞中的地鼠探出頭來。

如果超過地鼠持續出現的時間,地鼠就應該要重新回到洞中。所以這邊使用setTimeout(),在經過地鼠出現持續時間time後,移除hole上的.up,讓地鼠順利回家。在遊戲未結束(timeup = false)且前一隻地鼠已經回家的狀態下,我們會重新呼叫peep(),讓下一隻地鼠探頭出來。

let timeup = false;

function peep(){
    const time = randTime(200,1000);
    const hole = randomHole(holes);
    hole.classList.add('up');
    setTimeout(()=>{
      hole.classList.remove('up');
      if(!timeup) peep();
    },time);
}

宣告變數score給定初始值0,用來幫我們算分數。

startGame()可以讓我們在點擊頁面上的button後,立即開始打地鼠遊戲。

在方法的一開始,先把記分板(scoreBoard)歸零,然後把代表結束遊戲與否的timeup設成flase,同時也把計算的分數(score)歸零。

上面都做完後,呼叫peep()讓地鼠開始探頭出來給我們打,接著利用setTimeout()訂定打地鼠遊戲的時間限制,這邊設定遊戲時間為15秒(15000毫秒),15秒後把timeup設為true並提示使用者遊戲結束。

let score = 0;

function startGame(){
    scoreBoard.textContent = 0;
    timeup = false;
    score = 0;
    peep();
    setTimeout(()=>{
      timeup = true;
      alert("時間到,遊戲結束!!!");
    } , 15000);
}

最後一部分要來處理的是"當地鼠被點擊到,頁面上的得分要加1,然後地鼠要縮回洞中"。

我們可以為每一隻地鼠(mole)註冊click event listenerbonk()作為event handler

bonk()裡,首先判斷點擊是不是"人為"的,如果是使用者點擊觸發,則e.isTrusted會回傳true,而如果是用像是script之類的去觸發click event,則會回傳false並直接停止往下執行方法。

接著,在每一次點擊成功後,把分數(score)加1並移除加到地鼠(mole)上的.up,讓地鼠回到洞中,之後更新頁面上的得分(scoreBoard)就完成了。

function bonk(e){
    if(!e.isTrusted) return; //cheater
    score++;
    this.classList.remove('up');
    scoreBoard.textContent = score;
}

moles.forEach(mole => mole.addEventListener('click',bonk));
補充資料:

Math.random()
Math.round()
Math.floor()
setTimeout()
Element.classList
Node.textContent
Event.isTrusted

範例網頁請點此

完整程式碼請點此

30天的漫長挑戰終於結束啦~ /images/emoticon/emoticon29.gif (ps. 有空應該會再發一篇參賽心得吧! XD)


上一篇
Day 29 - Vanilla JS Countdown Timer
系列文
JS30 學習日記30

尚未有邦友留言

立即登入留言