---
# System prepended metadata

title: ✨ 手勢控制無限跑者｜CodePen 工作坊講義

---

# AI 手勢跑酷遊戲：從零開始的 Teachable Machine 教學

嗨！各位同學，今天我們要來動手做一個超酷的 AI 互動專案！

在這個專案中，我們將會親手訓練一個 AI 模型，讓它能「看懂」你的手勢，然後用這些手勢來控制網頁上的小遊戲主角，做出跑、停、跳、攻擊等動作。

聽起來很複雜？別擔心！我們會一步一步來，從一個空白的網頁開始，慢慢地把 AI 的靈魂注入進去！

### 💻 專案三大核心
1.  **HTML**：網頁的**骨架**，決定網頁上有哪些東西（標題、遊戲區、角色...）。
2.  **CSS**：網頁的**外觀**，負責美化骨架，加上顏色、大小、動畫效果。
3.  **JavaScript**：網頁的**大腦**，負責所有的互動邏輯，也是我們連接 AI 模型的地方。

### 🚀 學習地圖
* **第一關：訓練你的 AI 大腦 (Teachable Machine)**
* **第二關：搭建遊戲的舞台 (HTML & CSS 基礎)**
* **第三關：讓主角動起來 (CSS 動畫)**
* **第四關：注入 AI 靈魂 (JavaScript 核心)**
* **第五關：手勢 x 動作 (最關鍵的連結！)**
* **終極挑戰：打造你的專屬遊戲！**

---

## 💎 第一關：訓練你的 AI 大腦

在開始寫程式之前，我們需要先訓練一個 AI 模型，讓它認識我們的「手勢」。

1.  **前往 Teachable Machine**
    打開瀏覽器，進入 [Teachable Machine](https://teachablemachine.withgoogle.com/) 網站，點選「**開始使用**」。

2.  **選擇專案類型**
    選擇「**圖片專案**」，並點擊「**標準圖片模型**」。

3.  **建立分類 (Classes)**
    我們需要讓 AI 認識幾個基本手勢。請建立至少 4 個分類，並用你的「網路攝影機」為每個分類拍攝 **100 張以上** 的照片。
    * **拳頭 (Attack)**: 握緊拳頭的樣子。
    * **手掌 (Stop)**: 張開手掌，像喊停一樣。
    * **讚 (Jump)**: 比出讚的手勢。
    * **背景 (Idle/Run)**: **非常重要！**這個分類請不要做任何手勢，單純拍你的臉或背景。這能讓 AI 知道在「沒有特定手勢」時該做什麼。

4.  **訓練模型**
    點擊「**訓練模型**」，耐心等待訓練完成。訓練期間請不要切換分頁。

5.  **匯出模型**
    訓練完畢後，在右邊的預覽區塊測試一下模型是否能準確辨識你的手勢。如果沒問題，點擊「**匯出模型**」。
    
    在新視窗中，點擊「**上傳我的模型**」。上傳完畢後，會出現一段「**可分享的連結**」。**把它複製下來，我們馬上就要用了！**

---

## 🧱 第二關：搭建遊戲的舞台 (HTML & CSS 基礎)

AI 大腦有了，現在來蓋一個空的遊戲舞台吧！

打開你的程式碼編輯器（例如 [CodePen](https://codepen.io/pen/)），然後在 HTML 和 CSS 區塊貼上以下程式碼。

### HTML (網頁骨架)
```html
<!DOCTYPE html>
<html>
<head>
  <title>我的手勢跑酷遊戲</title>
</head>
<body>

  <h1>我的手勢跑酷遊戲 ✨</h1>
  
  <div id="game">
    <div class="ground"></div>
    
    <div id="runner" class="runner">
      <svg viewBox="0 0 120 120" class="avatar">
        <circle cx="60" cy="28" r="12" fill="var(--accent)"></circle>
        <rect x="52" y="40" width="16" height="28" rx="6" fill="white" stroke="var(--accent)" stroke-width="2"></rect>
        <rect x="52" y="68" width="8" height="22" fill="white" stroke="var(--accent)" stroke-width="2"></rect>
        <rect x="60" y="68" width="8" height="22" fill="white" stroke="var(--accent)" stroke-width="2"></rect>
        <rect x="40" y="42" width="10" height="4" fill="white" stroke="var(--accent)" stroke-width="2"></rect>
        <rect x="70" y="42" width="10" height="4" fill="white" stroke="var(--accent)" stroke-width="2"></rect>
      </svg>
      <div class="effect"></div>
    </div>
  </div>

  <div id="webcam-container"></div>
  <div id="label-container"></div>

</body>
</html>
```
> **小知識**:
> - `<div>`: 像一個透明的箱子，用來裝東西和排版。
> - `id="..."`: 元素的**身分證**，在整個網頁中是獨一無二的，方便 JS 找到它。
> - `<svg>`: 一種用程式碼畫出來的向量圖，我們的預設小人就是用它畫的。

### CSS (網頁外觀)

```css
/* :root 讓我們可以設定全域變數，方便統一管理顏色、速度等 */
:root {
  --accent: #ff6b6b; /* 主題色 */
  --ground: #7ad66a; /* 草地顏色 */
}

body {
  /* 讓所有東西水平置中 */
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: sans-serif;
}

#game {
  position: relative; /* 讓裡面的東西可以用絕對位置定位 */
  width: 600px;
  height: 300px;
  background: #cfe9ff; /* 天空藍 */
  border-radius: 18px;
  border: 2px solid #ccc;
  overflow: hidden; /* 超出框框的東西會被藏起來 */
  margin: 20px 0;
}

.ground {
  position: absolute;
  bottom: 0; /* 貼在底部 */
  left: 0;
  right: 0;
  height: 40px;
  background: var(--ground);
}

.runner {
  position: absolute;
  left: 100px;
  bottom: 40px; /* 站在地板上 */
  width: 90px;
  height: 90px;
}

.runner .avatar {
  width: 100%;
  height: 100%;
}
```
> **小知識**:
> - `#game`: `id` 是 game 的元素，用 `#` 來選取。
> - `.runner`: `class` 是 runner 的元素，用 `.` 來選取。
> - `position: absolute`: 讓元素可以相對於它的上層容器 (`#game`) 自由移動。

**完成後，你應該會看到一個藍色的天空、綠色的草地，還有一個靜止的小人。**

---

## 🏃 第三關：讓主角動起來 (CSS 動畫)

靜止的遊戲太無聊了！我們來用 CSS 的 `animation` 和 `@keyframes` 讓主角跑、跳、攻擊。

**把下面的 CSS 程式碼，加到你剛剛的 CSS 區塊的最下方。**

```css
/* ========== 狀態與動畫 ========== */

/* 遊戲狀態：透過在 #game 加上不同的 class 來控制 */
.state-run .runner {
  animation: bob 0.45s ease-in-out infinite;
}
.state-jump .runner {
  animation: jump 0.6s ease-out;
}
.state-attack .runner .effect {
  position: absolute;
  right: -10px;
  top: 20px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: radial-gradient(#fde047, #f97316 60%, transparent 70%);
  animation: boom 0.45s ease-out;
}
.state-stop .runner {
  /* 停止時可以加點特效，例如變灰 */
  filter: grayscale(0.5);
}

/* 定義動畫細節 */
/* 跑步時的上下擺動 */
@keyframes bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

/* 跳躍 */
@keyframes jump {
  0% { transform: translateY(0); }
  50% { transform: translateY(-100px); } /* 跳躍高度 */
  100% { transform: translateY(0); }
}

/* 攻擊光圈 */
@keyframes boom {
  from { transform: scale(0.4); opacity: 0.9; }
  to   { transform: scale(2.6); opacity: 0; }
}
```

> **試試看！**
> 在 HTML 編輯器中，找到 `<div id="game">` 這一行，把它改成 `<div id="game" class="state-run">`。
> 是不是看到主角開始上下擺動，像在跑步了？
> 再試試看改成 `class="state-jump"` 或 `class="state-attack"`，看看會發生什麼事！
>
> 待會我們就要用 JavaScript 來自動切換這些 class！

---

## 🧠 第四關：注入 AI 靈魂 (JavaScript 核心)

重頭戲來了！我們要寫 JavaScript，讓網頁載入 Teachable Machine 模型，並啟動攝影機。

**在 Html 區塊貼上以下程式碼。**

請把這兩行 `<script>` 標籤加到你的 HTML 檔案的 `<head>` 裡面
```html
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.20.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
```

**在 JavaScript 區塊貼上以下程式碼。**

```javascript
// 1. 把你的模型連結貼在這裡！
const URL = "【把你第一關複製的模型連結貼上來】";

let model, webcam, labelContainer, maxPredictions;

// 找到網頁上的重要元素
const game = document.getElementById("game");
const webcamContainer = document.getElementById("webcam-container");
labelContainer = document.getElementById("label-container");

// 建立一個啟動按鈕
const startButton = document.createElement("button");
startButton.innerText = "啟動 AI 攝影機";
document.body.appendChild(startButton);

// 按下按鈕後，執行 init 函式
startButton.addEventListener("click", init);

// 主要的初始化函式
async function init() {
  // 載入模型和相關資料
  const modelURL = URL + "model.json";
  const metadataURL = URL + "metadata.json";
  model = await tmImage.load(modelURL, metadataURL);
  maxPredictions = model.getTotalClasses();

  // 設定攝影機
  const flip = true; // 是否水平翻轉
  webcam = new tmImage.Webcam(200, 200, flip);
  await webcam.setup(); // 請求攝影機權限
  await webcam.play();
  
  // 將攝影機畫面加到網頁上
  webcamContainer.appendChild(webcam.canvas);
  
  // 隱藏按鈕，開始無限循環預測
  startButton.style.display = "none";
  window.requestAnimationFrame(loop);
}

// 無限循環，不斷進行預測
async function loop() {
  webcam.update(); // 更新攝影機畫面
  await predict(); // 進行預測
  window.requestAnimationFrame(loop); // 繼續下一次循環
}

// 預測函式
async function predict() {
  // 從攝影機畫面進行預測
  const prediction = await model.predict(webcam.canvas);
  
  // 把所有預測結果顯示出來
  let predictionText = "";
  for (let i = 0; i < maxPredictions; i++) {
    const classPrediction =
      prediction[i].className + ": " + prediction[i].probability.toFixed(2);
    predictionText += `<div>${classPrediction}</div>`;
  }
  labelContainer.innerHTML = predictionText;
}

```

**執行看看！** 點擊「啟動 AI 攝影機」按鈕，你應該會看到攝影機畫面出現，底下還會即時顯示 AI 對你每個手勢的辨識機率！

---

## ✨ 第五關：手勢 x 動作 (最關鍵的連結！)

我們已經能看到 AI 的預測結果了，現在剩下最後一步：**把預測結果轉換成遊戲主角的動作！**

我們要修改剛剛的 `predict` 函式，並新增一個 `setAction` 函式。

**找到剛剛的 `predict` 函式，用下面的新版本把它整個換掉。**

```javascript
// 遊戲目前最後的動作狀態
let lastAction = "idle"; 

// 預測函式 (更新版)
async function predict() {
  const prediction = await model.predict(webcam.canvas);

  // 1. 找出機率最高的預測是哪一個
  let highestProb = 0;
  let topPrediction = null;
  for (let i = 0; i < maxPredictions; i++) {
    if (prediction[i].probability > highestProb) {
      highestProb = prediction[i].probability;
      topPrediction = prediction[i];
    }
  }

  // 2. 顯示最高的預測結果
  labelContainer.innerHTML = `偵測到: ${topPrediction.className} (${(highestProb * 100).toFixed(1)}%)`;

  // 3. 根據手勢名稱，決定要執行哪個動作
  let currentAction = "run"; // 預設是跑步
  if (highestProb > 0.85) { // 只有在機率高於 85% 時才相信
      switch (topPrediction.className) {
          case "拳頭": // 這裡的文字要跟你 Teachable Machine 的分類名稱一模一樣！
              currentAction = "attack";
              break;
          case "手掌":
              currentAction = "stop";
              break;
          case "讚":
              currentAction = "jump";
              break;
          default:
              currentAction = "run";
      }
  }
  
  // 4. 執行動作
  applyAction(currentAction);
}

// 執行動作的函式
function applyAction(action) {
  // 如果是跳躍或攻擊，它們是短暫的動作，做完要變回跑步
  if (action === "jump") {
    setAction("jump");
    setTimeout(() => setAction("run"), 600); // 0.6秒後變回跑步
    return;
  }
  if (action === "attack") {
    setAction("attack");
    setTimeout(() => setAction("run"), 450); // 0.45秒後變回跑步
    return;
  }
  
  // 如果是跑步或停止，就直接設定
  setAction(action);
}

// 設定遊戲狀態的核心函式
function setAction(action) {
  if (lastAction === action) return; // 如果動作一樣，就不用重做
  
  // 移除所有舊的 state- class
  game.classList.remove("state-idle", "state-run", "state-stop", "state-jump", "state-attack");
  // 加上新的 state- class
  game.classList.add(`state-${action}`);
  
  lastAction = action;
}
```

> **注意！**
> `switch (topPrediction.className)` 裡面的 `"拳頭"`, `"手掌"`, `"OK"` **必須**跟你在 Teachable Machine 裡設定的「分類名稱」一模一樣！如果你的分類叫 `attack`，這裡就要改成 `case "attack":`。

**恭喜你！** 重新執行你的專案，現在你應該可以用手勢來控制遊戲主角了！

* 沒有手勢 -> **跑**
* 手掌 -> **停**
* 讚 -> **跳**
* 拳頭 -> **攻擊**

---

## 🏆 終極挑戰：打造你的專屬遊戲！

你已經完成了核心功能，現在是發揮創意的時候了！一個偉大的遊戲開發者，都是從不斷調整和實驗開始的。讓我們捲起袖子，直接修改程式碼，把這個遊戲變成你獨一無二的作品！

### 挑戰 A：成為遊戲美術設計師 (修改 CSS)

遊戲的氛圍和風格，通常是由顏色決定的。我們把所有重要的顏色都放在 CSS 的最上方，方便你修改。

1.  **找到 `:root` 區塊**
    在你的 CSS 程式碼最頂端，會看到這樣的區塊：
    ```css
    :root {
      --accent: #ff6b6b; /* 主題色 (角色) */
      --ground: #7ad66a; /* 草地顏色 */
    }
    ```
2.  **修改顏色**
    試著把顏色代碼換掉！例如，想做一個「夜晚沙漠」主題：
    ```css
    :root {
      --accent: #9b5de5; /* 換成神秘的紫色 */
      --ground: #ffd166; /* 換成沙地的黃色 */
    }
    ```
    > **小提示**：你可以上網搜尋「Color Picker」，就會有很多工具可以幫你找到喜歡的顏色代碼（例如 `#3a86ff`）。

### 挑戰 B：調整遊戲物理引擎 (修改 CSS 動畫)

覺得角色跳得不夠高？攻擊特效太慢？這些都可以在 CSS 的 `@keyframes` 裡調整。

1.  **調整跳躍高度**
    在 CSS 裡找到 `@keyframes jump`：
    ```css
    @keyframes jump {
      0%   { transform: translateY(0); }
      50%  { transform: translateY(-100px); } /* 就是這個數字！ */
      100% { transform: translateY(0); }
    }
    ```
    把 `-100px` 的數字改大一點（例如 `-150px`）會跳得更高，改小一點會跳得比較低。

2.  **調整動畫速度**
    在 CSS 裡找到 `.state-jump` 的設定：
    ```css
    .state-jump .runner {
      animation: jump 0.6s ease-out; /* 就是這個數字！ */
    }
    ```
    把 `0.6s` (0.6秒) 的數字改小（例如 `0.4s`）會讓跳躍動作更快，改大則會變慢。你可以用同樣的方式去調整 `.state-attack` 的攻擊動畫速度！

### 挑戰 C：創造你自己的英雄 (上傳圖片功能！)

這是我們唯一需要新增功能的地方！讓玩家可以上傳自己的圖片當作主角。

**第 1 步：在 HTML 中加入上傳按鈕**

很簡單，我們只需要一行程式碼。把它加到 `<h1>` 標題的 바로 밑에。

```html
  <h1>我的手勢跑酷遊戲 ✨</h1>
  <p>試著上傳一張圖片，打造你自己的英雄！</p>
  <input type="file" id="charUploader" accept="image/*">
```

**第 2 步：在 JavaScript 中加入魔法**

這段程式碼會「聽」用戶有沒有上傳檔案，如果有的話，就把遊戲主角換掉。把這整段程式碼，**複製貼到你的 JavaScript 檔案的最下方**。

```javascript

// === 角色上傳功能 (加在 JS 檔案最下方) ===

// 1. 找到我們剛剛在 HTML 裡建立的上傳按鈕
const uploader = document.getElementById('charUploader');

// 2. 監聽這個按鈕，當使用者選擇了檔案時，就執行裡面的程式
uploader.addEventListener('change', function(event) {
  
  // 檢查是否有檔案，並取得第一個檔案
  const file = event.target.files[0];
  if (!file) {
    return; // 如果沒有檔案，就什麼都不做
  }
  
  // 建立一個檔案讀取器，這東西能幫我們把圖片轉成電腦看得懂的格式
  const reader = new FileReader();
  
  // 4. 當讀取完成後，執行這個 function
  reader.onload = function(e) {
    // e.target.result 就是讀取完的圖片資料
    const imageDataUrl = e.target.result;
    
    // 5. 找到遊戲主角所在的 div
    const runnerDiv = document.getElementById('runner');
    
    // 6. 移除舊的角色 (不管是 SVG 還是之前的圖片)
    const oldAvatar = runnerDiv.querySelector('.avatar');
    if (oldAvatar) {
      oldAvatar.remove();
    }
    
    // 7. 建立一個新的圖片元素 <img>
    const newAvatar = document.createElement('img');
    newAvatar.src = imageDataUrl; // 把圖片資料放進去
    newAvatar.className = 'avatar'; // 給它一樣的 class 名稱，才能套用樣式
    
    // 8. 把新的圖片角色，加到 runnerDiv 的最前面
    runnerDiv.prepend(newAvatar);
  };
  
  // 3. 啟動讀取器，開始讀取檔案
  reader.readAsDataURL(file);
});

```

**第 3 步：用 CSS 調整圖片大小**

上傳的圖片可能會太大或太小，我們加一小段 CSS 來確保它的大小是正確的。把這段程式碼加到你的 CSS 檔案裡。

```CSS

/* 專門用來設定上傳圖片的樣式 */
#runner img.avatar {
  width: 100%;
  height: 100%;
  object-fit: contain; /* 確保圖片等比例縮放，不會變形 */
}

```