iT邦幫忙

2021 iThome 鐵人賽

DAY 30
2
自我挑戰組

JS30 學習日記系列 第 30

Day 30 - Make a Whack A Mole Game with Vanilla JS

  • 分享至 

  • xImage
  •  

前言

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
下一篇
Day 31 - 遲來的鐵人賽心得
系列文
JS30 學習日記31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言