# 建立太空遊戲第六部分:結束與重新開始 每個偉大的遊戲都需要明確的結束條件和流暢的重新開始機制。你已經建立了一個令人印象深刻的太空遊戲,擁有移動、戰鬥和得分功能——現在是時候添加最後的部分,讓它感覺更加完整。 目前你的遊戲可以無限運行,就像 NASA 在 1977 年發射的旅行者探測器一樣,幾十年後仍在太空中旅行。雖然這對於太空探索來說是可以接受的,但遊戲需要明確的結束點來創造令人滿意的體驗。 今天,我們將實現適當的勝負條件以及重新開始系統。在本課程結束時,你將擁有一個精緻的遊戲,玩家可以完成並重新遊玩,就像那些定義了遊戲媒介的經典街機遊戲一樣。 ## 課前測驗 [課前測驗](https://ff-quizzes.netlify.app/web/quiz/39) ## 理解遊戲結束條件 你的遊戲應該在什麼時候結束?這個基本問題自早期街機時代以來就影響了遊戲設計。吃豆人(Pac-Man)在你被鬼抓住或清除所有豆子時結束,而太空侵略者(Space Invaders)則在外星人到達底部或你摧毀它們時結束。 作為遊戲創作者,你可以定義勝利和失敗的條件。對於我們的太空遊戲,以下是一些經過驗證的方式,可以創造出引人入勝的遊戲體驗: - **摧毀 `N` 敵方飛船**:如果你將遊戲分成不同的關卡,通常需要摧毀 `N` 敵方飛船才能完成一個關卡。 - **你的飛船被摧毀**:有些遊戲中,如果你的飛船被摧毀,你就會輸掉遊戲。另一種常見的方法是引入生命值的概念。每次你的飛船被摧毀,生命值就會減少。一旦所有生命值耗盡,你就輸掉遊戲。 - **收集 `N` 分數**:另一種常見的結束條件是收集分數。如何獲得分數取決於你,但通常會將分數分配給各種活動,例如摧毀敵方飛船或收集敵方飛船被摧毀後掉落的物品。 - **完成一個關卡**:這可能涉及多種條件,例如摧毀 `X` 敵方飛船、收集 `Y` 分數,或者收集特定物品。 ## 實現遊戲重新開始功能 好的遊戲通過流暢的重新開始機制鼓勵玩家反覆遊玩。當玩家完成遊戲(或遭遇失敗)時,他們通常希望立即再次嘗試——無論是為了打破自己的得分記錄還是提高表現。 俄羅斯方塊(Tetris)完美地展示了這一點:當你的方塊到達頂部時,你可以立即開始新遊戲,而不需要導航複雜的菜單。我們將建立一個類似的重新開始系統,乾淨地重置遊戲狀態,讓玩家迅速回到遊戲中。 ✅ **反思**:想想你玩過的遊戲。它們在什麼條件下結束?如何提示你重新開始?什麼樣的重新開始體驗感覺流暢而不令人沮喪? ## 你將建立什麼 你將實現最終的功能,將你的項目轉變為完整的遊戲體驗。這些元素使精緻的遊戲與基本原型區分開來。 **今天我們要添加的內容:** 1. **勝利條件**:摧毀所有敵人並享受應得的慶祝! 2. **失敗條件**:生命值耗盡,面對失敗畫面 3. **重新開始機制**:按下 Enter 鍵立即重新開始——因為一局遊戲永遠不夠 4. **狀態管理**:每次都清空狀態——不會有上次遊戲留下的敵人或奇怪的故障 ## 開始準備 讓我們準備好你的開發環境。你應該已經準備好前幾課的太空遊戲文件。 **你的項目應該看起來像這樣:** ```bash -| assets -| enemyShip.png -| player.png -| laserRed.png -| life.png -| index.html -| app.js -| package.json ``` **啟動你的開發伺服器:** ```bash cd your-work npm start ``` **此命令:** - 在 `http://localhost:5000` 上運行本地伺服器 - 正確地提供你的文件 - 當你進行更改時自動刷新 在瀏覽器中打開 `http://localhost:5000`,確認你的遊戲正在運行。你應該能夠移動、射擊並與敵人互動。一旦確認,我們就可以進行實現了。 > 💡 **專業提示**:為了避免在 Visual Studio Code 中出現警告,請在文件的頂部聲明 `gameLoopId` 為 `let gameLoopId;`,而不是在 `window.onload` 函數內聲明。這符合現代 JavaScript 變數聲明的最佳實踐。 ## 實現步驟 ### 步驟 1:創建追蹤結束條件的函數 我們需要函數來監控遊戲應該何時結束。就像國際空間站上的傳感器不斷監控關鍵系統一樣,這些函數將持續檢查遊戲狀態。 ```javascript function isHeroDead() { return hero.life <= 0; } function isEnemiesDead() { const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead); return enemies.length === 0; } ``` **以下是內部發生的事情:** - **檢查**我們的英雄是否失去所有生命值(糟糕!) - **計算**還有多少敵人活著並且活躍 - **返回**當戰場上沒有敵人時的 `true` - **使用**簡單的 true/false 邏輯保持直觀 - **篩選**所有遊戲物件以找到倖存者 ### 步驟 2:更新事件處理器以檢查結束條件 現在我們將這些條件檢查與遊戲的事件系統連接起來。每次發生碰撞時,遊戲都會評估是否觸發結束條件。這為關鍵遊戲事件創造了即時反饋。 ```javascript eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { first.dead = true; second.dead = true; hero.incrementPoints(); if (isEnemiesDead()) { eventEmitter.emit(Messages.GAME_END_WIN); } }); eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => { enemy.dead = true; hero.decrementLife(); if (isHeroDead()) { eventEmitter.emit(Messages.GAME_END_LOSS); return; // loss before victory } if (isEnemiesDead()) { eventEmitter.emit(Messages.GAME_END_WIN); } }); eventEmitter.on(Messages.GAME_END_WIN, () => { endGame(true); }); eventEmitter.on(Messages.GAME_END_LOSS, () => { endGame(false); }); ``` **這裡發生了什麼:** - **雷射擊中敵人**:兩者消失,你獲得分數,並檢查是否獲勝 - **敵人擊中你**:你失去一條生命,並檢查你是否還活著 - **智能排序**:我們先檢查失敗(沒人想要同時贏和輸!) - **即時反應**:一旦發生重要事件,遊戲就會知道 ### 步驟 3:添加新的消息常量 你需要在 `Messages` 常量對象中添加新的消息類型。這些常量有助於保持一致性並防止事件系統中的拼寫錯誤。 ```javascript GAME_END_LOSS: "GAME_END_LOSS", GAME_END_WIN: "GAME_END_WIN", ``` **在上面,我們:** - **添加**了遊戲結束事件的常量以保持一致性 - **使用**描述性名稱清楚地指示事件目的 - **遵循**現有的消息類型命名規範 ### 步驟 4:實現重新開始控制 現在你將添加鍵盤控制,允許玩家重新開始遊戲。Enter 鍵是一個自然的選擇,因為它通常與確認操作和開始新遊戲相關聯。 **將 Enter 鍵檢測添加到現有的 keydown 事件監聽器:** ```javascript else if(evt.key === "Enter") { eventEmitter.emit(Messages.KEY_EVENT_ENTER); } ``` **添加新的消息常量:** ```javascript KEY_EVENT_ENTER: "KEY_EVENT_ENTER", ``` **你需要知道的:** - **擴展**現有的鍵盤事件處理系統 - **使用** Enter 鍵作為重新開始觸發器,提供直觀的用戶體驗 - **發送**自定義事件,遊戲的其他部分可以監聽 - **保持**與其他鍵盤控制相同的模式 ### 步驟 5:創建消息顯示系統 你的遊戲需要清楚地向玩家傳達結果。我們將創建一個消息系統,使用彩色文字顯示勝利和失敗狀態,類似於早期電腦系統的終端界面,其中綠色表示成功,紅色表示錯誤。 **創建 `displayMessage()` 函數:** ```javascript function displayMessage(message, color = "red") { ctx.font = "30px Arial"; ctx.fillStyle = color; ctx.textAlign = "center"; ctx.fillText(message, canvas.width / 2, canvas.height / 2); } ``` **逐步解析,以下是發生的事情:** - **設置**字體大小和字體家族,確保文字清晰可讀 - **應用**顏色參數,默認為 "red" 用於警告 - **水平和垂直居中**文字於畫布上 - **使用**現代 JavaScript 的默認參數提供靈活的顏色選項 - **利用**畫布 2D 上下文直接渲染文字 **創建 `endGame()` 函數:** ```javascript function endGame(win) { clearInterval(gameLoopId); // Set a delay to ensure any pending renders complete setTimeout(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); if (win) { displayMessage( "Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew", "green" ); } else { displayMessage( "You died !!! Press [Enter] to start a new game Captain Pew Pew" ); } }, 200) } ``` **此函數的作用:** - **凍結**所有物件——不再有移動的飛船或雷射 - **稍作停頓**(200 毫秒),讓最後一幀完成繪製 - **清空**屏幕並塗黑,營造戲劇效果 - **顯示**不同的消息給勝利者和失敗者 - **用顏色區分**消息——綠色代表好消息,紅色代表...嗯,不太好 - **告訴**玩家如何重新進入遊戲 ### 步驟 6:實現遊戲重置功能 重置系統需要完全清理當前的遊戲狀態並初始化新的遊戲會話。這確保玩家能夠在沒有任何前一局遊戲遺留數據的情況下重新開始。 **創建 `resetGame()` 函數:** ```javascript function resetGame() { if (gameLoopId) { clearInterval(gameLoopId); eventEmitter.clear(); initGame(); gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawPoints(); drawLife(); updateGameObjects(); drawGameObjects(ctx); }, 100); } } ``` **讓我們理解每個部分:** - **檢查**是否有遊戲循環正在運行,然後再重置 - **清除**現有的遊戲循環以停止所有當前遊戲活動 - **移除**所有事件監聽器以防止記憶體洩漏 - **重新初始化**遊戲狀態,創建新的物件和變數 - **啟動**新的遊戲循環,包含所有必要的遊戲功能 - **保持**相同的 100 毫秒間隔,確保遊戲性能一致 **將 Enter 鍵事件處理器添加到你的 `initGame()` 函數:** ```javascript eventEmitter.on(Messages.KEY_EVENT_ENTER, () => { resetGame(); }); ``` **將 `clear()` 方法添加到你的 EventEmitter 類:** ```javascript clear() { this.listeners = {}; } ``` **需要記住的要點:** - **連接** Enter 鍵按下與重置遊戲功能 - **在遊戲初始化期間**註冊此事件監聽器 - **提供**清除所有事件監聽器的方式,避免記憶體洩漏 - **重置**監聽器對象為空狀態,便於重新初始化 ## 恭喜!🎉 👽 💥 🚀 你已成功從零開始建立了一個完整的遊戲。就像 1970 年代創造第一批電子遊戲的程式員一樣,你已將代碼行轉化為具有完整遊戲機制和用戶反饋的互動體驗。🚀 💥 👽 **你已完成:** - **實現**完整的勝負條件並提供用戶反饋 - **創建**流暢的重新開始系統,支持連續遊玩 - **設計**清晰的視覺通信,傳達遊戲狀態 - **管理**複雜的遊戲狀態轉換和清理 - **組裝**所有組件,形成一個連貫且可玩的遊戲 ## GitHub Copilot Agent 挑戰 🚀 使用 Agent 模式完成以下挑戰: **描述:** 增強太空遊戲,實現具有逐漸增加難度和額外功能的關卡進程系統。 **提示:** 創建一個多關卡太空遊戲系統,每個關卡都有更多速度和生命值增加的敵方飛船。添加隨著每個關卡增加的得分倍數,並實現當敵人被摧毀時隨機出現的強化道具(如快速射擊或護盾)。包括關卡完成獎勵,並在屏幕上顯示當前關卡,與現有的得分和生命值一起。 了解更多關於 [Agent 模式](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode) 的信息。 ## 🚀 可選增強挑戰 **為你的遊戲添加音效**:通過實現音效來增強你的遊戲體驗!考慮添加以下音效: - **雷射射擊**:當玩家開火時 - **敵人摧毀**:當飛船被擊中時 - **英雄受傷**:當玩家受到攻擊時 - **勝利音樂**:當遊戲獲勝時 - **失敗音效**:當遊戲失敗時 **音效實現範例:** ```javascript // Create audio objects const laserSound = new Audio('assets/laser.wav'); const explosionSound = new Audio('assets/explosion.wav'); // Play sounds during game events function playLaserSound() { laserSound.currentTime = 0; // Reset to beginning laserSound.play(); } ``` **你需要知道的:** - **創建**不同音效的 Audio 對象 - **重置** `currentTime` 以允許快速觸發音效 - **處理**瀏覽器自動播放政策,通過用戶交互觸發音效 - **管理**音效音量和時間,提升遊戲體驗 > 💡 **學習資源**:探索這個 [音效沙盒](https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_audio_play),了解更多在 JavaScript 遊戲中實現音效的方法。 ## 課後測驗 [課後測驗](https://ff-quizzes.netlify.app/web/quiz/40) ## 回顧與自學 你的作業是創建一個新的樣本遊戲,因此探索一些有趣的遊戲,看看你可能會建立什麼類型的遊戲。 ## 作業 [建立樣本遊戲](assignment.md) --- **免責聲明**: 本文件已使用 AI 翻譯服務 [Co-op Translator](https://github.com/Azure/co-op-translator) 進行翻譯。雖然我們致力於提供準確的翻譯,但請注意,自動翻譯可能包含錯誤或不準確之處。原始文件的母語版本應被視為權威來源。對於關鍵信息,建議使用專業人工翻譯。我們對因使用此翻譯而引起的任何誤解或誤釋不承擔責任。