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