iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 30
0
Modern Web

一起挑戰 JavaScript 30 吧!系列 第 30

JS30 Day 30 - Whack A Mole

成品連結:Whack A Mole完成前程式碼完成後程式碼

今天要做的是打地鼠的遊戲!實作之前覺得很難,但在實作之後發現其實沒有想像中困難,一起來做做看吧!

待辦事項

與之前一樣,我們先列出要做的事情吧!
預計做出的成品要有下列功能:

  • 地鼠從六個洞隨機出現
  • 出現時間不固定,以增加難度
  • 打到地鼠後加分
  • 開始與結束遊戲機制

從功能我們可以列出下面的事項:

  • 地鼠能隨機產生的 function
  • 地數隨機出現時間的 function
  • 地鼠出現的 function(要持續隨機出現直到遊戲結束)
  • 開始、結束遊戲的 function
  • 監聽點擊事件並更新分數

列完了就開始吧!

地鼠能隨機產生的 function

這裡先解釋一下地數出現的方式,在 CSS 中有先寫好一個 up 的 class,當將它加到 div.hole 時,地數就會出現。

這裡要新增一個 function 並帶入一個引數 holes 也就是所有的地洞

function randomHole(holes) {
    // code here
}

接著要隨機產生 0~5 的數字當作是 holes 的 index,這樣我們就能選到隨機的一個地洞

function randomHole(holes) {
    const index = Math.floor(Math.random() * holes.length);
    const hole = holes[index];
    
    return hole;
}

這樣會有功能(可以試著在 console 執行看看!),但是由於是隨機產生數字,所以有可能連續 2 次、 3 次,甚至更多次連續選到同一個地洞。為了避免這種情形,我們要先記錄上一次選到的地洞,如果這次又選到了,就重新執行 randomHole 直到選到不一樣的動為止。

沒錯!你可能已經想到了,我們會使用迴圈(recursion)。

let lastHole;  // 記錄上次的地洞

function randomHole(holes) {
    const index = Math.floor(Math.random() * holes.length);  // 會是 0 ~ 5 的數字
    const hole = holes[index];
    
    if (lastHole === hole) {
        return randomHole(holes);
    }
    
    lastHole = hole;
    return hole;
}

使用迴圈的基本方法是會設定 if...else 做判斷,若不是預期的結果則重新呼叫自己(return 該 function);若是預期結果則 return 該值並跳出迴圈。

在我們的例子中,當 hole 與上次相同實則重新產生新的 hole,若與上次不一樣則 return 該值。

地數隨機出現時間的 function

這裡我們同樣要隨機產生時間,而 function 的引數會有兩個,maxmin,分別代表時間的最大值與最小值。

function randomTime(min, max) {
    const time = Math.round(Math.random() * (max - min) + min);
    return time;
}

上面這個寫法之前我們已經看過了,能夠幫我們得到 minmax 之間的數字;而 Math.round 幫我們四捨五入並取整數

地鼠出現的 function(要持續隨機出現直到遊戲結束)

地鼠已經可以隨機出現(但尚未渲染至畫面)、隨機出現的時間也得到了,我們來讓地鼠出現吧!

首先先使用剛剛建立的兩個 function randomHolerandomTime 取得地洞與出現的時間

function peep() {
    const time = randomTime(300, 1000);  // 最小 300 毫秒、最大 1000 毫秒
    const hole = randomHole(holes);
}

要讓地鼠出現,需要將 class up 加到 hole 上面

function peep() {
    const time = randomTime(300, 1000);  // 最小 300 毫秒、最大 1000 毫秒
    const hole = randomHole(holes);
    
    hole.classList.add('up');
}

但是這樣只會出現一次地鼠,為了要不停的出現直到遊戲時間結束,我們要再使用迴圈的方式,但是直接在 peep 中呼叫自己會造成無限迴圈(因為沒有跳出的條件)。我們希望停止執行 peep 的條件是當遊戲時間結束時,因此我們需要建立一個變數(也就是之前提到的 flag)來記錄時間是否已到。

let timeUp = false;  // 當遊戲時間結束時會改為 true

function peep() {
    const time = randomTime(300, 1000);  // 最小 300 毫秒、最大 1000 毫秒
    const hole = randomHole(holes);
    
    hole.classList.add('up');
    if (!timeUp) {
        peep();
    }   
}

這樣地數會出現了,但不會消失啊...這時 time 派上用場了,我們使用 setTimeout 來設定結束時從 hole 移除 up 這個 class

let timeUp = false;  // 當遊戲時間結束時會改為 true

function peep() {
    const time = randomTime(300, 1000);  // 最小 300 毫秒、最大 1000 毫秒
    const hole = randomHole(holes);
    
    hole.classList.add('up');
    
    setTimeout(() => {
        hole.classList.remove('up');
        if (!timeUp) {
            peep();   
        }
    }, time);
}

也就是當第一個地鼠出現時,在過了 time 的時間後移除 class up 並檢查遊戲是否已結束,若還沒則再度執行 peep

開始、結束遊戲的 function

在 HTML 的按鈕已經有綁定 click 事件,所以當按下按鈕後會開始遊戲,或是遊戲當中按下時會重置並重新開始。

在這個 function 中我們要執行 peep 並設定遊戲結束時間,並在結束時將 timeUp 設為 ture,以讓 peep 不再執行

function startGame() {
    scoreBoard.textContent = '0';  // 重設分數
    timeUp = false;
    peep();
    
    setTimeout(() => {
        timeUp = true;  // 當 timeUp === true 時,peep 會停止執行
    }, 10000)  // 設定 10 秒後遊戲結束
}

監聽點擊事件並更新分數

最後當使用者打到地鼠時,需要更新分數並從 hole 移除 class up

function scoreUpdate(e) {
    let score = Number(scoreBoard.textContent);  // 取得當前分數並轉型為 number
    if (!e.isTrusted) return;  // 確保使用者真的用滑鼠點擊

    score++;
    scoreBoard.textContent = score;
    this.classList.remove('up');
}

到這裡就完成啦!

後記

好不容易完成 30 天的挑戰了,想當初看到這 30 天的成品時從沒認為自己有能力完成;而是既操作時雖然有幾天還是需要看影片才能完成,但很高興自己能夠獨立完成大部分的天數。

這 30 天從作者(Wes Bos)學到了不少觀念像是寫程式的思考邏輯、如何將大問題拆解成小問題再各個擊破等等...。重點是 Wes 的 CSS 手法真的很厲害,有幾天我甚至覺得他的 CSS 比 JS 更值得學習啊(尤其是第 27 天)!

總之,非常謝謝你(妳)的觀看,希望在這 30 篇中多少有能夠幫忙到的部分。如有問題或想法也歡迎提出討論,謝謝!

Reference


上一篇
JS30 Day 29 - Countdown Timer
系列文
一起挑戰 JavaScript 30 吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言