# 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; /* 確保圖片等比例縮放,不會變形 */
}
```