avatar

目錄
Project - Memory Card Game

Memory Card Game

Demo

前言

打開 YouTube 首頁時看到這個推薦影片 (Memory Card Game - JavaScript Tutorial),教導如何使用 Vanilla JavaScript 撰寫記憶卡牌遊戲。

照著影片練習過一次後,我在第二次的撰寫加入了一些想法,例如計時功能、破關之後的恭喜訊息,還有重新開始遊戲等等。

簡介

記憶遊戲,點擊卡片進行配對。

使用工具或技術

  • HTML
    • data 屬性
  • CSS
    • 3D 屬性
  • JavaScript
    • ES6 語法

過程

詳細操作可以看影片,這裡只節錄影片中的主要步驟與一些用到的觀念技巧,主要為自己隨後加上的功能與做法。

影片中的建立步驟

  • 建立資料夾與檔案
  • 撰寫 HTML 並引入 CSS 和 JavaScript 檔案
  • 重置 CSS
  • 調整元素的樣式
  • 抓取所有卡片到 JavaScript 並為它們註冊點擊事件,加上翻面效果
  • 到 CSS 設定 3D 翻面效果
  • 使用 JavaScript 儲存卡片以配對
  • 用 data 屬性儲存卡片的花色,並且進行配對
  • 重構程式碼
  • 處理細節問題:配對失敗的卡片翻回來之前,鎖定畫面
  • 處理細節問題:避免重複點擊卡片被判斷為配對成功
  • 使用立即函式洗牌

影片中用到的觀念與技巧

  • Position
    • 使用 Position: absolute 時,會往上層查找有被定位的元素,以該元素為定位基準。
  • Flexbox
    • 在外層元素加上 display: flex ,外層元素和內層元素會有這些預設屬性:
      • 外層元素 (flex-container)
        • flex-direction: row
        • flex-wrap: nowrap
        • justify-content: flex-start
        • align-items: stretch
      • 內層元素 (flex-item)
        • flex: 1
  • calc
    • 使用 calc() 時,裡面的運算符號前後都要留一格,不能緊跟著前後的數字。
    • 例如:width: calc(25% - 10px)
  • 3D Effect
    • perspective 在外層元素加上景深,
    • transform-style 設定卡片位在 3D 空間
    • backface-visibility 隱藏元素的背面
    • transform: rotateY(180deg) 實現卡片翻面
  • 使用 forEach()addEventListener() 為每張卡片監聽事件
  • 使用 classList.add()classList.remove()classList.toggle()去調整 HTML 元素的 class
  • 借助變數 hasFlipedCard 的值為 true/false 的判斷讓卡片兩兩一組
  • 當配對成功時,用 removeEventListener() 移除事件的監聽
  • data 屬性
    • 使用 data-* 屬性在 HTML 儲存卡片的花色
    • 並在 JavaScript 用 dataset 取得花色
  • 程式碼重構
    • 將程式碼以更小功能拆解
    • 用三元運算式來簡化 if/else
    • 用 return 來簡化 if/else
  • 借助變數 lockBoard 鎖定畫面
  • 使用 Math.random() 產生亂數,並用 Math.floor() 取得最小整數。
  • 使用 CSS flex 的 order 屬性為卡片排序。

增加計時功能

照著影片練習過一次之後,有了一些想法,覺得可以加上去。例如計時功能,並在破關時顯示恭喜訊息等等。

我的想法是,網頁載入後,一旦點擊卡片所在範圍 (.memory-game),就開始計時。由於這個動作只需要在第一次點擊時執行,因此啟動之後先移除這個監聽事件,以免每次點擊卡片都觸發這個動作。

然後,用 new Date() 取得點擊當下的時間作為起始時間,每秒執行一次 countTime() 以計算時間並且更新到畫面。

javascript
1
2
3
4
5
function startTimer() {
cardArea.removeEventListener('click', startTimer)
startTime = new Date()
counting = setInterval(countTime, 1000)
}

接下來,countTime() 會每秒取得一次目前的時間,並與起始時間相減,這樣就能知道經過多少時間。再將時間以分和秒個別處理,最後呈現到 HTML 畫面。

javascript
1
2
3
4
5
6
7
8
function countTime() {
let currentTime = new Date()
let spendMilliseconds = currentTime - startTime
let spendMinutes = Math.floor(spendMilliseconds / 1000 / 60).toString().padStart(2, 0)
let spendSeconds = Math.floor((spendMilliseconds / 1000) - (spendMinutes * 60)).toString().padStart(2, 0)
spendTimes = spendMinutes + ":" + spendSeconds
showTimeText.innerHTML = spendTimes
}

最後,當所有的卡片都配對成功,也就是畫面中沒有未翻開的卡片時,就停止計時,並顯示恭喜等訊息。

為此,先取得所有卡片的數量,紀錄在變數 unflipCardNumbers 裡,表示未翻開的卡片數量。只要畫面上有卡片配對成功,就執行 countUnflipCards ,把 unflipCardNumbers 減二。等到 unflipCardNumbers 等於 0 的時候,就執行 stopTime 把計時暫停。

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let unflipCardNumbers = cards.length

function disableCards() {
firstCard.removeEventListener('click', flipCard)
secondCard.removeEventListener('click', flipCard)
resetBoard()
countUnflipCards()
}

function countUnflipCards() {
unflipCardNumbers -= 2
if (unflipCardNumbers == 0) stopTime()
}

function stopTime() {
clearInterval(counting)
spendTimeText.innerHTML = spendTimes
messageText.classList.remove('none')
}

實際執行的結果如下:

顯示訊息與重新開始

當畫面上所有的牌都配對成功之後,除了停止計時,也會移除我加在訊息視窗的 none ,並且顯示恭喜訊息、花費的時間,還有重新開始遊戲的按鈕。

如果按下重新開始遊戲的按鈕,會將 none 加回到訊息視窗,以關閉訊息視窗,並且重新整理網頁來重新開始遊戲。

javascript
1
2
3
4
5
6
7
8
9
10
11
12
function stopTime() {
clearInterval(counting)
spendTimeText.innerHTML = spendTimes
messageText.classList.remove('none')
}

function startNewGame() {
messageText.classList.add('none')
window.location.reload()
}

newGameButton.addEventListener('click', startNewGame)

後記

這個影片讓我學到很多,例如 3D 效果的使用,透過 CSS flex 的 order 讓元素排序,還有一些 JavaScript 的撰寫思路。

最後我也將自己對於計時、顯示訊息的想法加到這個遊戲,並將卡片和背景顏色改成了漸層色。雖然總覺得破關後道恭喜的訊息視窗簡陋了點,不過目前還沒其他更好的想法,就先這樣吧。

參考資料

文章作者: Jane Lin
文章鏈接: http://yoursite.com/2020/02/13/1581541645/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 JL's Coding Notes

評論