iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Modern Web

30天打造純前端互動小遊戲網站系列 第 19

Day 19 | 音效 / 特效

  • 分享至 

  • xImage
  •  

Day 19
主題:音效 / 特效 — 成功或失敗時的動畫與音效
今日目標
在前幾天,我們已經完成了翻牌邏輯,卡片能夠生成、翻轉,並進行配對判斷。今天的任務就是為遊戲加入「聲音與視覺特效」,提升玩家的沉浸感。畢竟遊戲除了機制本身,即時的回饋也是影響體驗的重要元素。

學習與操作重點

  1. 音效(Audio)
    使用 HTML5 的 <audio> 標籤或 JavaScript 的 new Audio() 來播放音效。
    建立至少兩種音效:
    配對成功 → 輕快的提示音,營造成就感。
    配對失敗 → 簡單的錯誤音,提醒玩家。
    注意音效檔案大小,避免影響載入速度。

  2. 動畫 (CSS + JS)
    成功配對的卡片,可以加上閃爍或縮放效果,強調「恭喜找到一對!」。
    失敗時,可以加上抖動 (shake)效果,提醒玩家翻錯。
    利用 classList.add()setTimeout() 來動態套用 / 移除動畫 class。

  3. 細節控制
    避免同時播放太多音效,必要時使用 audio.currentTime = 0; 讓音效能立即重新播放。
    特效不要過於花俏,以免干擾遊戲的主要體驗。

程式範例
這裡示範如何在配對成功 / 失敗時,加入音效與特效:

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <title>Day 19 - 音效與特效</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background: #fef6e4;
    }
    .game-board {
      display: grid;
      grid-template-columns: repeat(4, 100px);
      grid-gap: 10px;
    }
    .card {
      width: 100px;
      height: 140px;
      perspective: 1000px;
      cursor: pointer;
    }
    .card-inner {
      width: 100%;
      height: 100%;
      position: relative;
      transform-style: preserve-3d;
      transition: transform 0.6s;
    }
    .card.flip .card-inner {
      transform: rotateY(180deg);
    }
    .front, .back {
      position: absolute;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 28px;
      font-weight: bold;
      border-radius: 10px;
      backface-visibility: hidden;
    }
    .front {
      background: #8ecae6;
      color: white;
    }
    .back {
      background: #ffb703;
      color: black;
      transform: rotateY(180deg);
    }

    /* 特效 */
    .match {
      animation: success 0.6s ease;
    }
    @keyframes success {
      0% { transform: scale(1); }
      50% { transform: scale(1.2); }
      100% { transform: scale(1); }
    }
    .shake {
      animation: shake 0.4s;
    }
    @keyframes shake {
      0% { transform: translateX(0); }
      25% { transform: translateX(-5px); }
      50% { transform: translateX(5px); }
      75% { transform: translateX(-5px); }
      100% { transform: translateX(0); }
    }
  </style>
</head>
<body>
  <div class="game-board" id="gameBoard"></div>

  <!-- 音效 -->
  <audio id="successSound" src="success.mp3"></audio>
  <audio id="failSound" src="fail.mp3"></audio>

  <script>
    const symbols = ["🍎", "🍌", "🍇", "🍓", "🍍", "🥝"];
    let cards = [...symbols, ...symbols];
    cards.sort(() => Math.random() - 0.5);

    const board = document.getElementById("gameBoard");
    const successSound = document.getElementById("successSound");
    const failSound = document.getElementById("failSound");

    let flippedCards = [];
    let lockBoard = false;

    // 建立卡片
    cards.forEach(symbol => {
      const card = document.createElement("div");
      card.classList.add("card");
      card.innerHTML = `
        <div class="card-inner">
          <div class="front">?</div>
          <div class="back">${symbol}</div>
        </div>
      `;

      card.addEventListener("click", () => {
        if (lockBoard || card.classList.contains("flip")) return;

        card.classList.add("flip");
        flippedCards.push(card);

        if (flippedCards.length === 2) {
          checkMatch();
        }
      });

      board.appendChild(card);
    });

    // 檢查配對
    function checkMatch() {
      lockBoard = true;
      const [card1, card2] = flippedCards;
      const symbol1 = card1.querySelector(".back").textContent;
      const symbol2 = card2.querySelector(".back").textContent;

      if (symbol1 === symbol2) {
        successSound.currentTime = 0;
        successSound.play();
        card1.classList.add("match");
        card2.classList.add("match");
        flippedCards = [];
        lockBoard = false;
      } else {
        failSound.currentTime = 0;
        failSound.play();
        card1.classList.add("shake");
        card2.classList.add("shake");
        setTimeout(() => {
          card1.classList.remove("flip", "shake");
          card2.classList.remove("flip", "shake");
          flippedCards = [];
          lockBoard = false;
        }, 1000);
      }
    }
  </script>
</body>
</html>

今日成果
學會如何在遊戲中加入音效,強化玩家回饋
完成配對成功 / 失敗的特效動畫
遊戲體驗更有「互動感」,不再只是單純的翻牌


上一篇
Day 18 | 基礎邏輯測試
系列文
30天打造純前端互動小遊戲網站19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言