You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/zh-HK/6-space-game/6-end-condition/README.md

24 KiB

建立太空遊戲第6部分結束與重新開始

journey
    title 你的遊戲完成旅程
    section 結束條件
      定義勝負狀態: 3: Student
      實作條件檢查: 4: Student
      處理狀態轉換: 4: Student
    section 玩家體驗
      設計反饋系統: 4: Student
      建立重新開始機制: 5: Student
      美化用戶介面: 5: Student
    section 系統整合
      管理遊戲生命週期: 5: Student
      處理記憶體清理: 5: Student
      建立完整體驗: 5: Student

每一款偉大的遊戲都需要明確的結束條件和流暢的重新開始機制。你已經建立了一款令人印象深刻的太空遊戲,有移動、戰鬥和計分——現在是時候加入最後的元素,讓它感覺完整了。

你的遊戲目前無限運行就像1977年NASA發射的旅行者探測器——數十年後仍在太空中旅行。雖然這對太空探索來說沒問題但遊戲需要明確的終點來創造令人滿意的體驗。

今天,我們將實現正確的勝利/失敗條件和重新開始系統。課程結束時,你會擁有一款經過精緻打磨,玩家可以完成並重玩,就像定義了媒介的經典街機遊戲一樣的遊戲。

mindmap
  root((遊戲完成))
    End Conditions
      Victory States
      Defeat Conditions
      Progress Tracking
      State Validation
    Player Feedback
      Visual Messages
      Color Psychology
      Clear Communication
      Emotional Response
    State Management
      Game Loop Control
      Memory Cleanup
      Object Lifecycle
      Event Handling
    Restart Systems
      Input Handling
      State Reset
      Fresh Initialization
      User Experience
    Polish Elements
      Message Display
      Smooth Transitions
      Error Prevention
      Accessibility

課前小測

課前小測

理解遊戲結束條件

你的遊戲應該何時結束?這個基本問題自早期街機時代以來一直塑造著遊戲設計。吃豆人當你被鬼抓住或吃完所有豆子時結束,而太空侵略者當外星人到達底部或你消滅所有外星人時結束。

作為遊戲創作者,你定義勝利和失敗條件。對於我們的太空遊戲,以下是經證明能創造吸引人玩法的做法:

flowchart TD
    A["🎮 遊戲開始"] --> B{"檢查條件"}
    
    B --> C["敵人數量"]
    B --> D["英雄生命"]
    B --> E["分數門檻"]
    B --> F["關卡進度"]
    
    C --> C1{"敵人 = 0?"}
    D --> D1{"生命 = 0?"}
    E --> E1{"分數 ≥ 目標?"}
    F --> F1{"目標完成?"}
    
    C1 -->|是| G["🏆 勝利"]
    D1 -->|是| H["💀 失敗"]
    E1 -->|是| G
    F1 -->|是| G
    
    C1 -->|否| B
    D1 -->|否| B
    E1 -->|否| B
    F1 -->|否| B
    
    G --> I["🔄 重新開始選項"]
    H --> I
    
    style G fill:#e8f5e8
    style H fill:#ffebee
    style I fill:#e3f2fd
  • 已摧毀 N 艘敵方飛船:如果你將遊戲分成不同關卡,需要摧毀 N 艘敵方飛船才能完成該關是相當常見的
  • 你的飛船被摧毀:確實有遊戲是飛船被摧毀就輸。另一個常見方式是有生命值概念。每次飛船被摧毀會扣一條命,當所有命都用完後你就輸了
  • 你已收集 N 分數:另一個常見結束條件是你收集分數。你怎麼獲得分數取決於你,但通常會對摧毀敵人或收集被摧毀敵人掉落的物品給予分數
  • 完成一個關卡:這可能包含多個條件,例如摧毀 X 艘敵人、收集 Y 分數或收集特定物品

實現遊戲重新開始功能

好的遊戲通過流暢的重新開始機制鼓勵重玩。當玩家結束遊戲(或失敗)時,他們通常想立即再試一次——無論是為了打破自己的紀錄還是提升表現。

stateDiagram-v2
    [*] --> Playing: 遊戲開始
    Playing --> Victory: 全部敵人被消滅
    Playing --> Defeat: 生命值 = 0
    
    Victory --> MessageDisplay: 顯示勝利訊息
    Defeat --> MessageDisplay: 顯示失敗訊息
    
    MessageDisplay --> WaitingRestart: 按下 Enter 提示
    WaitingRestart --> Resetting: 按下 Enter 鍵
    
    Resetting --> CleanupMemory: 清除計時器
    CleanupMemory --> ClearEvents: 移除監聽器
    ClearEvents --> InitializeGame: 全新開始
    InitializeGame --> Playing: 新遊戲開始
    
    note right of MessageDisplay
        顏色標示回饋:
        綠色 = 勝利
        紅色 = 失敗
    end note
    
    note right of Resetting
        完成狀態重設
        防止記憶體洩漏
    end note

俄羅斯方塊完美詮釋了這一點:當方塊堆到頂端,你可以立即開始新遊戲,無需瀏覽複雜的選單。我們將建立類似的重新開始系統,乾淨地重置遊戲狀態,讓玩家迅速回到遊戲中。

思考點:想想你玩過的遊戲。它們在什麼條件下結束,並且如何提示你重新開始?什麼讓重新開始的體驗感覺流暢而不是令人沮喪?

你將建立什麼

你將實作最終功能,讓專案轉變為完整的遊戲體驗。這些元素讓精緻遊戲有別於基本原型。

我們今天要加的東西:

  1. 勝利條件:消滅所有敵人並有適當的慶祝(你應得的!)
  2. 失敗條件:命盡時出現失敗畫面接受結果
  3. 重新開始機制:按 Enter 立即重新開始—因為玩一次永遠不夠
  4. 狀態管理:每次都是乾淨的起點——沒有殘留敵人或上局的怪異錯誤

開始吧

讓我們準備你的開發環境。你應該已擁有前幾課的太空遊戲所有檔案。

你的專案應該長這樣:

-| assets
  -| enemyShip.png
  -| player.png
  -| laserRed.png
  -| life.png
-| index.html
-| app.js
-| package.json

啟動開發伺服器:

cd your-work
npm start

這個指令會:

  • 啟動本地伺服器於 http://localhost:5000
  • 正確提供你的檔案
  • 讓你更改時自動重新整理

打開瀏覽器進入 http://localhost:5000,確認遊戲在運行。你應該能移動、射擊及與敵人互動。確認後,我們就能進行實作了。

💡 專家提示:為避免 Visual Studio Code 警告,建議在檔案頂部宣告 let gameLoopId;,而非在 window.onload 函數內宣告。這符合現代JavaScript變數宣告最佳實踐。

flowchart TD
    A["1. 狀態追蹤"] --> B["2. 事件處理器"]
    B --> C["3. 訊息常數"]
    C --> D["4. 重新開始控制"]
    D --> E["5. 訊息顯示"]
    E --> F["6. 系統重設"]
    
    G["isHeroDead()\nisEnemiesDead()"] --> A
    H["碰撞事件\n結束遊戲事件"] --> B
    I["遊戲結束_勝利\n遊戲結束_失敗"] --> C
    J["Enter 鍵\n重新開始觸發"] --> D
    K["勝利/失敗\n顏色標示文字"] --> E
    L["狀態清理\n全新初始化"] --> F
    
    F --> M["🎮 完整遊戲"]
    
    style A fill:#e3f2fd
    style B fill:#e8f5e8
    style C fill:#fff3e0
    style D fill:#f3e5f5
    style E fill:#e0f2f1
    style F fill:#fce4ec
    style M fill:#e1f5fe

實作步驟

步驟1建立結束條件追蹤函數

我們需要函數監控遊戲何時結束。就像國際太空站經常監控關鍵系統一樣,這些函數會不斷檢查遊戲狀態。

function isHeroDead() {
  return hero.life <= 0;
}

function isEnemiesDead() {
  const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead);
  return enemies.length === 0;
}

幕後發生的事:

  • 檢查我們的主角是否已無命(好痛!)
  • 統計還有多少敵人活著努力抵抗
  • 回傳當戰場清空敵人時返回 true
  • 使用簡單的真/假邏輯讓流程簡潔明瞭
  • 篩選過所有遊戲物件尋找存活者

步驟2更新事件處理程序連結結束條件

現在將這些條件檢查連結到遊戲事件系統。每次發生碰撞時,遊戲會評估是否觸發結束條件。這會讓重要事件有即時反饋。

sequenceDiagram
    participant Collision
    participant GameLogic
    participant Conditions
    participant EventSystem
    participant Display
    
    Collision->>GameLogic: 激光擊中敵人
    GameLogic->>GameLogic: 毀壞物件
    GameLogic->>Conditions: 檢查 isEnemiesDead()
    
    alt 所有敵人被擊敗
        Conditions->>EventSystem: 發出 GAME_END_WIN
        EventSystem->>Display: 顯示勝利訊息
    else 敵人仍存在
        Conditions->>GameLogic: 繼續遊戲
    end
    
    Collision->>GameLogic: 敵人擊中英雄
    GameLogic->>GameLogic: 生命減少
    GameLogic->>Conditions: 檢查 isHeroDead()
    
    alt 生命 = 0
        Conditions->>EventSystem: 發出 GAME_END_LOSS
        EventSystem->>Display: 顯示失敗訊息
    else 生命剩餘
        GameLogic->>Conditions: 檢查 isEnemiesDead()
        alt 所有敵人被擊敗
            Conditions->>EventSystem: 發出 GAME_END_WIN
        end
    end
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; // 凱旋之前的損失
    }
    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常數物件新增訊息類型。這些常數有助於保持一致性並防止事件系統中打錯字。

GAME_END_LOSS: "GAME_END_LOSS",
GAME_END_WIN: "GAME_END_WIN",

於上文中我們:

  • 新增遊戲結束事件的常數以維持一致
  • 使用清晰說明事件用途的名稱
  • 遵循既有訊息類型命名慣例

步驟4實作重新開始控制

現在你將新增鍵盤控制允許玩家重新開始遊戲。Enter 鍵是自然選擇,因為它通常與確認操作和開始新遊戲相關。

將 Enter 鍵偵測加入現有 keydown 事件監聽器:

else if(evt.key === "Enter") {
   eventEmitter.emit(Messages.KEY_EVENT_ENTER);
}

新增訊息常數:

KEY_EVENT_ENTER: "KEY_EVENT_ENTER",

你需要知道的:

  • 擴充現有鍵盤事件處理系統
  • 使用Enter 鍵作為重新開始觸發器,提供直覺使用體驗
  • 發出自訂事件供遊戲其他部分監聽
  • 維持與其他鍵盤控制相同的模式

步驟5建立訊息顯示系統

你的遊戲需要清晰地傳達結果給玩家。我們將建立訊息系統,用顏色區分勝敗狀態,類似早期電腦系統終端,綠色表示成功,紅色代表錯誤。

建立 displayMessage() 函式:

function displayMessage(message, color = "red") {
  ctx.font = "30px Arial";
  ctx.fillStyle = color;
  ctx.textAlign = "center";
  ctx.fillText(message, canvas.width / 2, canvas.height / 2);
}

步驟解析:

  • 設定字型大小及字體以利清楚閱讀
  • 應用顏色參數,預設為紅色警告色
  • 文本在畫布中水平與垂直置中
  • 採用現代JavaScript預設參數提供彈性調色
  • 利用畫布2D上下文直接渲染文字

建立 endGame() 函式:

function endGame(win) {
  clearInterval(gameLoopId);

  // 設定延遲以確保任何待處理的渲染完成
  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() 函式:

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() 函式:

eventEmitter.on(Messages.KEY_EVENT_ENTER, () => {
  resetGame();
});

新增 clear() 方法到你的 EventEmitter 類別:

clear() {
  this.listeners = {};
}

重點提醒:

  • 連結Enter鍵按下與遊戲重置功能
  • 在遊戲初始化時註冊此事件監聽器
  • 提供清理所有事件監聽器的乾淨方法
  • 防止記憶體洩漏,重新遊戲間清空事件處理器
  • 監聽器物件重置成空狀態,利於後續初始化

恭喜你!🎉

👽 💥 🚀 你成功從零建構一款完整遊戲。就像1970年代第一批電玩開發者一樣你將一行行程式碼轉化為擁有正確遊戲機制與用戶回饋的互動體驗。🚀 💥 👽

你已達成:

  • 實作完整的勝利與失敗條件並有使用者反饋
  • 建立無縫的重新開始系統,支持持續遊玩
  • 設計清晰視覺溝通呈現遊戲狀態
  • 管理複雜的遊戲狀態轉換與清理
  • 組裝所有組件成為一款可玩且流暢的遊戲

🔄 教學檢核點

完整遊戲開發系統:慶祝你掌握整個遊戲開發流程:

  • 結束條件如何創造令人滿意的玩家體驗?
  • 為何適當的狀態管理對遊戲穩定性至關重要?
  • 視覺回饋如何提升玩家理解?
  • 重新開始系統對玩家留存起什麼作用?

系統熟練:你的完整遊戲展現了:

  • 全端遊戲開發:從圖像到輸入到狀態管理
  • 專業架構:事件驅動系統並有妥善清理
  • 使用者體驗設計:清晰回饋和直覺控制
  • 效能優化:高效率渲染與記憶體管理
  • 精緻與完整:所有讓遊戲感覺完成的細節

業界技能:你已實作:

  • 遊戲循環架構:即時系統具持續穩定性
  • 事件驅動程式設計:解耦能有效擴展系統
  • 狀態管理:複雜資料處理與生命週期管理
  • 使用者介面設計:清晰溝通與流暢操作
  • 測試與除錯:反覆開發與問題解決

你接下來5分鐘能做的事

  • 玩完整遊戲並測試所有勝利與失敗條件
  • 嘗試不同的結束條件參數
  • 加入 console.log 語句追蹤遊戲狀態變化
  • 與朋友分享遊戲,蒐集回饋

🎯 你這小時能完成的目標

  • 完成課後測驗並反思你的遊戲開發歷程
  • 加入勝利與失敗狀態的音效效果
  • 實作額外結束條件,例如限時或獎勵目標
  • 創建不同難度關卡與不同敵人數量
  • 美化視覺呈現,改善字體與色彩

📅 你一週內的遊戲開發精修計畫

  • 完成擴充版太空遊戲,有多重關卡與進度追蹤
  • 新增強化道具、不同敵人類型及特殊武器
  • 建立高分系統與持久化存儲
  • 設計使用者介面包含選單、設定及遊戲選項
  • 優化不同裝置和瀏覽器的效能
  • 將遊戲部署至網路並與社群分享

🌟 你的一個月遊戲開發職業路線

  • 製作多款完整遊戲,探索不同類型與機制
  • 學習進階遊戲開發框架,如 Phaser 或 Three.js
  • 參與開源遊戲開發專案
  • 研讀遊戲設計原理與玩家心理學
  • 建立展示你遊戲開發技能的作品集
  • 與遊戲開發社群連結,持續學習

🎯 你的完整遊戲開發精通時間表

timeline
    title 完整遊戲開發學習進度
    
    section 基礎 (課程 1-2)
        遊戲架構:專案結構
                 :資產管理
                 :畫布基礎
                 :事件系統
        
    section 互動系統 (課程 3-4)
        玩家操作:輸入處理
                  :移動機制
                  :碰撞偵測
                  :物理模擬
        
    section 遊戲機制 (課程 5)
        反饋系統:計分機制
                  :生命管理
                  :視覺傳達
                  :玩家動機
        
    section 遊戲完成 (課程 6)
        打磨與流程:結束條件
                   :狀態管理
                   :重啟系統
                   :使用者體驗
        
    section 進階功能 (1 週)
        強化技能:音效整合
                  :視覺效果
                  :關卡進展
                  :效能優化
        
    section 專業發展 (1 個月)
        行業準備:框架掌握
                  :團隊合作
                  :作品集開發
                  :社群參與
        
    section 職涯提升 (3 個月)
        專門化:高級遊戲引擎
                :平台部署
                :貨幣化策略
                :產業人脈

🛠️ 你的完整遊戲開發工具總結

完成這整個太空遊戲系列後,你已掌握:

  • 遊戲架構:事件驅動系統、遊戲循環與狀態管理
  • 圖形程式設計Canvas API、精靈渲染與視覺特效
  • 輸入系統:鍵盤處理、碰撞偵測與回應式控制
  • 遊戲設計:玩家反饋、進程系統與吸引力機制
  • 效能優化:高效渲染、記憶體管理與幀率控制
  • 使用者體驗:清晰溝通、直覺控制與細節打磨
  • 專業模式:乾淨程式碼、除錯技巧與專案組織

實務運用:你的遊戲開發技能直接適用於:

  • 互動網頁應用程式:動態介面與即時系統
  • 資料視覺化:動畫圖表與互動圖形
  • 教育科技:遊戲化與引人入勝的學習體驗
  • 行動開發:觸控互動與效能優化
  • 模擬軟體:物理引擎與即時建模
  • 創意產業:互動藝術、娛樂與數位體驗

獲得的專業技能:你現在可以:

  • 架構 複雜互動系統從零開始
  • 除錯 使用系統性方法處理即時應用程式
  • 優化 效能,提供順暢用戶體驗
  • 設計 吸引人的使用者介面與互動模式
  • 協作 透過良好程式碼組織,有效參與技術專案

掌握的遊戲開發概念

  • 即時系統:遊戲循環、幀率管理與效能
  • 事件驅動架構:解耦系統與訊息傳遞
  • 狀態管理:複雜資料處理和生命週期管理
  • 使用者介面程式設計Canvas 圖形與響應式設計
  • 遊戲設計理論:玩家心理與吸引力機制

下一階段你已準備好探索進階遊戲框架、3D 圖像、多玩家系統,或轉向專業遊戲開發職涯!

🌟 成就解鎖:你完成了一整趟遊戲開發旅程,並從零打造出專業品質的互動體驗!

歡迎加入遊戲開發社群! 🎮

GitHub Copilot Agent 挑戰 🚀

使用 Agent 模式完成以下挑戰:

說明: 強化太空遊戲,實作關卡進程系統,難度逐級提升並有獎勵功能。

提示: 創建一個多關卡太空遊戲系統,每關有更多敵機且速度與血量提升。增加隨關數增加的分數倍率,並實現當敵機被摧毀時隨機出現的能力提升(如連發射擊或護盾)。加入關卡完成獎勵,並在螢幕上同時顯示當前關卡、現有分數與生命數。

在此了解更多關於 agent 模式

🚀 選擇性強化挑戰

為你的遊戲加入音效:透過音效提升遊戲體驗!可考慮新增:

  • 玩家射擊時的雷射聲
  • 敵機被摧毀時的爆炸聲
  • 主角受傷時的受擊聲
  • 勝利時的勝利音樂
  • 失敗時的失敗音效

音效實作範例:

// 建立音訊物件
const laserSound = new Audio('assets/laser.wav');
const explosionSound = new Audio('assets/explosion.wav');

// 在遊戲事件期間播放聲音
function playLaserSound() {
  laserSound.currentTime = 0; // 重設到開始位置
  laserSound.play();
}

你需要知道:

  • 建立 不同音效的 Audio 物件
  • 重置 currentTime 以允許快速連續播放聲音
  • 處理 瀏覽器自動播放政策,靠用戶互動觸發音效
  • 管理 音量與時間安排,提升遊戲體驗

💡 學習資源:探索此 音效沙盒,了解更多如何在 JavaScript 遊戲中實作音效。

課堂後小測驗

課堂後小測驗

複習與自學

你的任務是創作一款新的樣本遊戲,可以瀏覽一些有趣的遊戲,看看你想製作什麼類型的遊戲。

作業

製作一款樣本遊戲


免責聲明
本文件乃使用 AI 翻譯服務 Co-op Translator 進行翻譯。雖然我們致力於確保準確性,但請注意,自動翻譯可能包含錯誤或不準確之處。原始文件之母語版本應被視為權威來源。對於重要資訊,建議採用專業人工翻譯。本公司不對因使用此翻譯而引起之任何誤解或誤釋承擔責任。