【Unity C#從零到精通】項目深化:構建核心游戲循環、UI與動態敵人系統

Langchain系列文章目錄

01-玩轉LangChain:從模型調用到Prompt模板與輸出解析的完整指南
02-玩轉 LangChain Memory 模塊:四種記憶類型詳解及應用場景全覆蓋
03-全面掌握 LangChain:從核心鏈條構建到動態任務分配的實戰指南
04-玩轉 LangChain:從文檔加載到高效問答系統構建的全程實戰
05-玩轉 LangChain:深度評估問答系統的三種高效方法(示例生成、手動評估與LLM輔助評估)
06-從 0 到 1 掌握 LangChain Agents:自定義工具 + LLM 打造智能工作流!
07-【深度解析】從GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘

PyTorch系列文章目錄

Python系列文章目錄

C#系列文章目錄

01-C#與游戲開發的初次見面:從零開始的Unity之旅
02-C#入門:從變量與數據類型開始你的游戲開發之旅
03-C#運算符與表達式:從入門到游戲傷害計算實踐
04-從零開始學C#:用if-else和switch打造智能游戲邏輯
05-掌握C#循環:for、while、break與continue詳解及游戲案例
06-玩轉C#函數:參數、返回值與游戲中的攻擊邏輯封裝
07-Unity游戲開發入門:用C#控制游戲對象移動
08-C#面向對象編程基礎:類的定義、屬性與字段詳解
09-C#封裝與訪問修飾符:保護數據安全的利器
10-如何用C#繼承提升游戲開發效率?Enemy與Boss案例解析
11-C#多態性入門:從零到游戲開發實戰
12-C#接口王者之路:從入門到Unity游戲開發實戰 (IAttackable案例詳解)
13-C#靜態成員揭秘:共享數據與方法的利器
14-Unity 面向對象實戰:掌握組件化設計與腳本通信,構建玩家敵人交互
15-C#入門 Day15:徹底搞懂數組!從基礎到游戲子彈管理實戰
16-C# List 從入門到實戰:掌握動態數組,輕松管理游戲敵人列表 (含代碼示例)
17-C# 字典 (Dictionary) 完全指南:從入門到游戲屬性表實戰 (Day 17)
18-C#游戲開發【第18天】 | 深入理解隊列(Queue)與棧(Stack):從基礎到任務隊列實戰
19-【C# 進階】深入理解枚舉 Flags 屬性:游戲開發中多狀態組合的利器
20-C#結構體(Struct)深度解析:輕量數據容器與游戲開發應用 (Day 20)
21-Unity數據持久化進階:告別硬編碼,用ScriptableObject優雅管理游戲配置!(Day 21)
22-Unity C# 健壯性編程:告別崩潰!掌握異常處理與調試的 4 大核心技巧 (Day 22)
23-C#代碼解耦利器:委托與事件(Delegate & Event)從入門到實踐 (Day 23)
24-Unity腳本通信終極指南:從0到1精通UnityEvent與事件解耦(Day 24)
25-精通C# Lambda與LINQ:Unity數據處理效率提升10倍的秘訣! (Day 25)
26-# Unity C#進階:掌握泛型編程,告別重復代碼,編寫優雅復用的通用組件!(Day26)
27-Unity協程從入門到精通:告別卡頓,用Coroutine優雅處理異步與時序任務 (Day 27)
28-搞定玩家控制!Unity輸入系統、物理引擎、碰撞檢測實戰指南 (Day 28)
29-# Unity動畫控制核心:Animator狀態機與C#腳本實戰指南 (Day 29)
30-Unity UI 從零到精通 (第30天): Canvas、布局與C#交互實戰 (Day 30)
31-Unity性能優化利器:徹底搞懂對象池技術(附C#實現與源碼解析)
32-Unity C#進階:用狀態模式與FSM優雅管理復雜敵人AI,告別Spaghetti Code!(Day32)
33-Unity游戲開發實戰:從PlayerPrefs到JSON,精通游戲存檔與加載機制(Day 33)
34-Unity C# 實戰:從零開始為游戲添加背景音樂與音效 (AudioSource/AudioClip/AudioMixer 詳解)(Day 34)
35-Unity 場景管理核心教程:從 LoadScene 到 Loading Screen 實戰 (Day 35)
36-Unity設計模式實戰:用單例和觀察者模式優化你的游戲架構 (Day 36)
37-Unity性能優化實戰:用Profiler揪出卡頓元兇 (CPU/GPU/內存/GC全面解析) (Day 37)
38-Unity C# 與 Shader 交互入門:腳本動態控制材質與視覺效果 (含 MaterialPropertyBlock 詳解)(Day 38)
39-Unity網絡編程入門:掌握Netcode for GameObjects實現多人游戲基礎(Day 39)
40-Unity C#入門到實戰: 啟動你的第一個2D游戲項目(平臺跳躍/俯視角射擊) - 規劃與核心玩法實現 (Day 40)
41-【Unity C#從零到精通】項目深化:構建核心游戲循環、UI與動態敵人系統(Day 41)


文章目錄

  • Langchain系列文章目錄
  • PyTorch系列文章目錄
  • Python系列文章目錄
  • C#系列文章目錄
  • 前言
  • 一、核心游戲循環:賦予游戲生命
    • 1.1 定義游戲循環的基本要素
      • 1.1.1 關卡概念與設計(簡化)
      • 1.1.2 積分系統實現
        • (1) 積分變量
        • (2) 觸發加分
      • 1.1.3 勝負條件判斷
        • (1) 失敗條件
        • (2) 勝利條件
    • 1.2 實現游戲狀態管理
      • 1.2.1 引入游戲狀態枚舉
      • 1.2.2 編寫GameManager控制流程
    • 1.3 代碼示例:基礎游戲循環框架
  • 二、UI集成:連接玩家與游戲世界
    • 2.1 必要UI元素添加
      • 2.1.1 生命值顯示
        • (1) 使用Slider (血條)
        • (2) 使用Text (數字顯示)
      • 2.1.2 分數實時更新
      • 2.1.3 基礎菜單交互(可選,此處簡化)
    • 2.2 UI更新邏輯實現
      • 2.2.1 通過事件驅動UI更新 (推薦)
      • 2.2.2 UIManager腳本設計 (或在GameManager中處理)
    • 2.3 實踐:將UI與GameManager關聯
  • 三、敵人系統完善:動態與挑戰
    • 3.1 敵人生成機制
      • 3.1.1 設置敵人生成點(Spawn Points)
      • 3.1.2 定時或按條件生成敵人
    • 3.2 引入對象池優化 (回顧第31天)
      • 3.2.1 回顧對象池原理
      • 3.2.2 集成對象池管理敵人實例
    • 3.3 敵人管理策略
      • 3.3.1 追蹤當前敵人數量
      • 3.3.2 敵人銷毀與回收
  • 四、互動元素:增加游戲趣味性
    • 4.1 設計簡單的拾取物系統
      • 4.1.1 創建拾取物預制體
      • 4.1.2 拾取邏輯實現(碰撞/觸發器)
    • 4.2 拾取效果處理
      • 4.2.1 更新玩家狀態(生命/分數)
      • 4.2.2 拾取物自身的銷毀/回收
    • 4.3 代碼示例:可拾取物品腳本
  • 五、常見問題與排查建議
    • 5.1 UI不更新怎么辦?
    • 5.2 對象池回收出錯?
    • 5.3 勝負條件不觸發?
  • 六、總結


前言

大家好!歡迎來到“Unity C#從零到精通”系列專欄的第41天。在前一天的學習中(第40天),我們啟動了一個綜合項目(2D平臺跳躍或俯視角射擊),并搭建了基礎框架,實現了核心的角色控制。今天,我們的任務是深化這個項目,為它注入真正的“靈魂”——完善核心游戲系統,增加必要的交互內容,讓它從一個簡單的原型向一個更完整的游戲體驗邁進。

在本節中,我們將重點關注以下幾個關鍵方面:

  1. 核心游戲循環 (Core Game Loop): 設計并實現游戲的基本流程,包括關卡概念、得分機制以及勝負條件的判斷。
  2. UI 集成 (UI Integration): 將玩家的關鍵信息(如生命值、分數)通過UI實時展示出來。
  3. 敵人系統完善 (Enemy System Enhancement): 實現敵人的動態生成,并引入對象池技術進行優化。
  4. 互動元素添加 (Adding Interactive Elements): 創建簡單的拾取物(如加血包、得分道具),增加游戲的可玩性。

通過今天的學習與實踐,你將掌握如何將各個獨立的功能模塊(玩家、敵人、UI、游戲邏輯)有機地結合起來,構建一個功能相對完善的游戲核心。準備好了嗎?讓我們開始填充我們的游戲世界吧!

一、核心游戲循環:賦予游戲生命

游戲循環是任何游戲運行的基礎,它定義了游戲從開始到結束的基本流程和規則。一個良好的游戲循環能夠引導玩家,提供明確的目標和反饋。

1.1 定義游戲循環的基本要素

一個基本的游戲循環至少需要包含目標、進程反饋和結束條件。

1.1.1 關卡概念與設計(簡化)

對于我們當前的綜合項目,可以將“關卡”簡化為單個游戲場景內的挑戰。例如,目標可能是“存活指定時間”、“達到特定分數”或“消滅所有敵人”。更復雜的關卡設計(如多場景切換)將在后續(如第35天)涉及,現在我們聚焦于單場景內的核心循環。

1.1.2 積分系統實現

積分是衡量玩家表現的常用方式。我們需要一個機制來追蹤和更新玩家的得分。

(1) 積分變量

通常在全局管理器(如 GameManager)中定義一個變量來存儲分數:

// GameManager.cs
using UnityEngine;
using UnityEngine.UI; // 引入UI命名空間public class GameManager : MonoBehaviour
{public static GameManager Instance { get; private set; } // 單例模式public int score = 0;// 后面會添加UI引用// public Text scoreText;void Awake(){if (Instance == null){Instance = this;// DontDestroyOnLoad(gameObject); // 如果需要跨場景保持,取消注釋}else{Destroy(gameObject);}}public void AddScore(int points){score += points;Debug.Log("Score: " + score); // 臨時日志輸出// 更新UI顯示 (稍后實現)// UpdateScoreUI();}// 后面會添加UI更新方法// void UpdateScoreUI() { ... }
}
(2) 觸發加分

在需要加分的地方(例如,敵人被消滅、拾取物被收集),調用 GameManagerAddScore 方法:

// EnemyHealth.cs (假設敵人有這個腳本)
public class EnemyHealth : MonoBehaviour
{public int scoreValue = 10; // 消滅該敵人獲得的分數public void TakeDamage(int damage){// ... 扣血邏輯 ...if (/* 生命值 <= 0 */){Die();}}void Die(){// 調用GameManager增加分數if (GameManager.Instance != null){GameManager.Instance.AddScore(scoreValue);}// ... 銷毀或回收到對象池 ...gameObject.SetActive(false); // 示例:簡單禁用,用于對象池}
}

1.1.3 勝負條件判斷

游戲需要明確的結束條件,告訴玩家他們是贏了還是輸了。

(1) 失敗條件

常見的失敗條件是玩家生命值耗盡。

// PlayerHealth.cs
public class PlayerHealth : MonoBehaviour
{public int maxHealth = 100;public int currentHealth;void Start(){currentHealth = maxHealth;// 更新UI (稍后實現)}public void TakeDamage(int damage){currentHealth -= damage;currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth); // 防止生命值低于0或超過上限Debug.Log("Player Health: " + currentHealth);// 更新UI (稍后實現)if (currentHealth <= 0){Die();}}void Die(){Debug.Log("Player Died! Game Over.");// 通知GameManager游戲結束if (GameManager.Instance != null){GameManager.Instance.GameOver();}// 可能禁用玩家控制、播放死亡動畫等gameObject.SetActive(false);}public void Heal(int amount){currentHealth += amount;currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);Debug.Log("Player Healed. Current Health: " + currentHealth);// 更新UI (稍后實現)}
}
(2) 勝利條件

勝利條件可以多樣,例如:達到目標分數、消滅所有敵人、到達終點等。

// GameManager.cs (續)
public int scoreToWin = 100; // 示例:勝利所需分數
public bool isGameOver = false;void Update()
{if (isGameOver) return; // 游戲結束后不再檢測// 檢查勝利條件 (示例:達到分數)if (score >= scoreToWin){WinGame();}
}public void GameOver()
{if (isGameOver) return; // 防止重復調用isGameOver = true;Debug.Log("Game Over!");Time.timeScale = 0f; // 暫停游戲// 顯示失敗UI (稍后實現)// ShowGameOverUI();
}void WinGame()
{if (isGameOver) return; // 防止重復調用isGameOver = true;Debug.Log("You Win!");Time.timeScale = 0f; // 暫停游戲// 顯示勝利UI (稍后實現)// ShowWinUI();
}// 在游戲開始或重新開始時重置狀態
public void StartGame()
{score = 0;isGameOver = false;Time.timeScale = 1f; // 恢復游戲速度// 重置玩家狀態、敵人等...// 隱藏結束UI
}

1.2 實現游戲狀態管理

為了更好地控制游戲流程(如開始、暫停、結束),引入游戲狀態機的概念很有幫助。

1.2.1 引入游戲狀態枚舉

使用枚舉(Enum,第19天學習過)來定義不同的游戲狀態:

// GameManager.cs (添加枚舉定義)
public enum GameState
{MainMenu, // 主菜單(如果需要)Playing,  // 游戲中Paused,   // 暫停GameOver, // 游戲失敗Win       // 游戲勝利
}public class GameManager : MonoBehaviour
{// ... 其他變量 ...public GameState currentState = GameState.Playing; // 初始狀態設為Playing (根據實際需要調整)// ... Awake, AddScore ...void Update(){// 根據狀態執行不同邏輯switch (currentState){case GameState.Playing:if (isGameOver) return; // 檢查是否已結束// 檢查勝利條件if (score >= scoreToWin){ChangeState(GameState.Win);}// 處理暫停輸入 (示例: 按下P鍵)if (Input.GetKeyDown(KeyCode.P)){ChangeState(GameState.Paused);}break;case GameState.Paused:// 處理恢復輸入 (示例: 再次按下P鍵)if (Input.GetKeyDown(KeyCode.P)){ChangeState(GameState.Playing);}break;case GameState.GameOver:case GameState.Win:// 游戲結束狀態,可以等待玩家輸入重新開始if (Input.GetKeyDown(KeyCode.R)) // 示例:按R重新開始{RestartGame(); // 需要實現RestartGame方法,可能涉及場景重新加載}break;}}public void ChangeState(GameState newState){if (currentState == newState) return; // 狀態未改變currentState = newState;Debug.Log("Game State Changed to: " + newState);switch (currentState){case GameState.Playing:Time.timeScale = 1f; // 恢復游戲// 可能隱藏暫停菜單break;case GameState.Paused:Time.timeScale = 0f; // 暫停游戲// 顯示暫停菜單break;case GameState.GameOver:isGameOver = true; // 確保設置結束標志Time.timeScale = 0f;// 顯示失敗UIbreak;case GameState.Win:isGameOver = true; // 確保設置結束標志Time.timeScale = 0f;// 顯示勝利UIbreak;}}// 在PlayerHealth的Die方法中調用這個public void TriggerGameOver(){ChangeState(GameState.GameOver);}// 實現RestartGame方法 (簡化版,可能需要重新加載場景)public void RestartGame(){Debug.Log("Restarting Game...");Time.timeScale = 1f;// 對于簡單項目,可以考慮重新加載當前場景UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name);// 注意:如果GameManager設置了DontDestroyOnLoad,需要額外處理狀態重置// 否則,重新加載場景會自動重置大部分狀態}// ... 其他方法 ...
}// PlayerHealth.cs 的 Die 方法修改為調用 TriggerGameOver
void Die()
{Debug.Log("Player Died!");if (GameManager.Instance != null){GameManager.Instance.TriggerGameOver(); // 調用GameManager的狀態改變方法}gameObject.SetActive(false);
}

1.2.2 編寫GameManager控制流程

GameManager 現在成為了游戲狀態和核心循環的中樞。它負責監聽事件(如玩家死亡、達到分數)、改變狀態,并根據當前狀態控制游戲行為(如暫停)。

  • 單例模式 (Singleton): 確保全局只有一個 GameManager 實例,方便其他腳本訪問。
  • 狀態機 (State Machine): 使用 GameState 枚舉和 ChangeState 方法管理游戲的不同階段。
  • 時間控制 (Time Scale): 通過 Time.timeScale 實現游戲的暫停與恢復。

1.3 代碼示例:基礎游戲循環框架

上面 GameManager 的代碼已經構成了一個基礎的游戲循環框架。它包含了:

  • 狀態定義 (GameState)
  • 狀態切換邏輯 (ChangeState)
  • 得分管理 (score, AddScore)
  • 勝負條件判斷 (在 Update 或狀態切換中處理)
  • 游戲暫停/恢復 (Time.timeScale)

實踐要點:

  1. 創建一個名為 GameManager 的空 GameObject。
  2. GameManager.cs 腳本附加到該 GameObject 上。
  3. 根據你的游戲設計,調整 scoreToWin 等參數。
  4. 確保 Player 和 Enemy 的腳本能夠正確調用 GameManager.Instance 的方法(如 AddScore, TriggerGameOver)。
Player Dies
Reaches Score Goal
Press Pause Key
Press Pause Key Again
Press Restart Key
Press Restart Key
Playing
GameOver
Win
Paused
Restart Game / Reload Scene

圖1: 簡化的游戲狀態流程圖

二、UI集成:連接玩家與游戲世界

有了核心邏輯,我們需要將關鍵信息反饋給玩家。UI(用戶界面)是實現這一目標的主要途徑。

2.1 必要UI元素添加

我們需要在場景中創建基本的UI元素來顯示信息。(回顧第30天:UI開發與交互)

2.1.1 生命值顯示

通常使用 SliderText 來顯示生命值。

(1) 使用Slider (血條)
  1. 在 Hierarchy 窗口右鍵 -> UI -> Slider,創建一個 Slider。
  2. 調整 Slider 的樣式,可以去掉 Handle(滑塊),改變 Fill Area 的顏色。
  3. 設置 Slider 的 Min Value 為 0,Max Value 為玩家的最大生命值 (maxHealth)。
(2) 使用Text (數字顯示)
  1. 在 Hierarchy 窗口右鍵 -> UI -> Text (或 TextMeshPro),創建一個文本元素。
  2. 調整字體、大小、顏色等。

2.1.2 分數實時更新

使用 Text 元素來顯示分數。

  1. 創建另一個 Text 元素用于顯示分數。
  2. 調整樣式。

2.1.3 基礎菜單交互(可選,此處簡化)

可以創建簡單的 Panel 元素,包含 “Game Over” 或 “You Win” 的文本,以及一個 “Restart” 按鈕。初始時將這些 Panel 設置為不激活 (SetActive(false)).

2.2 UI更新邏輯實現

需要編寫腳本來將游戲數據同步到UI元素上。

2.2.1 通過事件驅動UI更新 (推薦)

使用事件(C# event 或 UnityEvent,回顧第23、24天)是解耦UI更新邏輯的好方法。當玩家生命值或分數變化時,觸發事件,UI 管理器監聽這些事件并更新對應的UI元素。

示例 (使用簡單的直接引用更新): 為了簡化,我們先展示直接引用的方式。

2.2.2 UIManager腳本設計 (或在GameManager中處理)

可以創建一個 UIManager 腳本,或者將UI更新邏輯直接放在 GameManager 中(對于小型項目可行)。

// GameManager.cs (添加UI引用和更新方法)
using UnityEngine.UI; // 確保引入public class GameManager : MonoBehaviour
{// ... 其他變量 ...public Text scoreText;        // 在Inspector中拖入分數Text組件public Slider healthSlider;   // 在Inspector中拖入血條Slider組件public Text healthText;       // (可選) 在Inspector中拖入顯示具體血量數字的Text組件public GameObject gameOverPanel; // 在Inspector中拖入失敗UI Panelpublic GameObject winPanel;      // 在Inspector中拖入勝利UI Panel// ... Awake ...void Start() // Start中初始化UI{UpdateScoreUI();UpdateHealthUI(PlayerHealth.Instance.currentHealth, PlayerHealth.Instance.maxHealth); // 假設PlayerHealth也有單例或方便獲取if(gameOverPanel) gameOverPanel.SetActive(false); // 初始隱藏結束界面if(winPanel) winPanel.SetActive(false);}public void AddScore(int points){score += points;UpdateScoreUI(); // 分數變化時更新UI}public void UpdatePlayerHealthUI(int currentHealth, int maxHealth) // 由PlayerHealth調用{UpdateHealthUI(currentHealth, maxHealth);}void UpdateScoreUI(){if (scoreText != null){scoreText.text = "Score: " + score;}}void UpdateHealthUI(int currentHealth, int maxHealth){if (healthSlider != null){healthSlider.maxValue = maxHealth;healthSlider.value = currentHealth;}if (healthText != null){healthText.text = currentHealth + " / " + maxHealth;}}public void ChangeState(GameState newState){// ... (之前的狀態切換邏輯) ...switch (currentState){// ... 其他狀態 ...case GameState.GameOver:isGameOver = true;Time.timeScale = 0f;if(gameOverPanel) gameOverPanel.SetActive(true); // 顯示失敗UIbreak;case GameState.Win:isGameOver = true;Time.timeScale = 0f;if(winPanel) winPanel.SetActive(true); // 顯示勝利UIbreak;}// 在狀態切換時,也可以隱藏/顯示相應的UI面板if (newState != GameState.GameOver && gameOverPanel) gameOverPanel.SetActive(false);if (newState != GameState.Win && winPanel) winPanel.SetActive(false);}// PlayerHealth 需要獲取GameManager引用來更新UI,或者使用事件// PlayerHealth.cs (修改)// public class PlayerHealth : MonoBehaviour// {//     // ...//     void Start()//     {//         currentHealth = maxHealth;//         if (GameManager.Instance != null)//              GameManager.Instance.UpdatePlayerHealthUI(currentHealth, maxHealth);//     }//     public void TakeDamage(int damage)//     {//         // ...扣血...//         if (GameManager.Instance != null)//              GameManager.Instance.UpdatePlayerHealthUI(currentHealth, maxHealth); // 更新UI//         // ...死亡判斷...//     }//      public void Heal(int amount)//     {//        // ...加血...//        if (GameManager.Instance != null)//             GameManager.Instance.UpdatePlayerHealthUI(currentHealth, maxHealth); // 更新UI//     }// }// 更好的方式是PlayerHealth定義事件,GameManager監聽// public class PlayerHealth : MonoBehaviour {//     public event System.Action<int, int> OnHealthChanged;//     // ... 在TakeDamage和Heal中調用 OnHealthChanged?.Invoke(currentHealth, maxHealth); ...// }// GameManager.cs 的 Start() 中:// PlayerHealth.Instance.OnHealthChanged += UpdateHealthUI; // 訂閱事件// GameManager.cs 的 OnDestroy() 中:// if (PlayerHealth.Instance != null) PlayerHealth.Instance.OnHealthChanged -= UpdateHealthUI; // 取消訂閱
}

2.3 實踐:將UI與GameManager關聯

  1. 在 Unity 編輯器中,選中 GameManager GameObject。
  2. 在 Inspector 面板中,找到 GameManager (Script) 組件暴露出的 Score Text, Health Slider, Health Text, GameOver Panel, Win Panel 字段。
  3. 將場景中對應的 UI 元素拖拽到這些字段上。
  4. 確保 PlayerHealth 腳本能夠通知 GameManager 更新血量UI(通過直接調用或事件)。

三、敵人系統完善:動態與挑戰

靜態放置的敵人缺乏變化。我們需要讓敵人能夠動態地出現在游戲中,并且要考慮性能。

3.1 敵人生成機制

3.1.1 設置敵人生成點(Spawn Points)

  1. 在場景中創建幾個空的 GameObject,命名為 SpawnPoint1, SpawnPoint2 等。
  2. 將它們放置在希望敵人出現的位置。
  3. 可以給它們添加一個圖標以便在 Scene 視圖中看到。

3.1.2 定時或按條件生成敵人

創建一個 EnemySpawner 腳本來處理生成邏輯。

// EnemySpawner.cs
using UnityEngine;
using System.Collections; // 需要使用協程public class EnemySpawner : MonoBehaviour
{public GameObject enemyPrefab;    // 要生成的敵人預制體 (在Inspector中指定)public Transform[] spawnPoints; // 存儲所有生成點 (在Inspector中指定)public float spawnDelay = 2f;     // 生成間隔時間public int maxEnemies = 10;       // 場景中最大敵人數量 (可選)private int currentEnemyCount = 0; // 當前敵人數量 (如果需要限制)// 如果使用對象池,需要引用對象池public ObjectPool enemyPool; // 假設有一個名為ObjectPool的腳本 (在Inspector中指定)void Start(){// 檢查是否使用了對象池if (enemyPool == null){Debug.LogWarning("Enemy Spawner is not using an object pool. Performance might be affected.");}// 開始生成循環StartCoroutine(SpawnEnemyRoutine());}IEnumerator SpawnEnemyRoutine(){while (true) // 無限循環生成,直到腳本停止或條件不滿足{// 可選:檢查是否達到最大敵人數量// if (currentEnemyCount >= maxEnemies)// {//     yield return null; // 等待下一幀再檢查//     continue;// }// 隨機選擇一個生成點if (spawnPoints.Length > 0){int spawnIndex = Random.Range(0, spawnPoints.Length);Transform spawnPoint = spawnPoints[spawnIndex];// 從對象池獲取敵人 或 直接實例化GameObject enemyInstance = null;if (enemyPool != null){enemyInstance = enemyPool.GetPooledObject(); // 從池中獲取if (enemyInstance != null){enemyInstance.transform.position = spawnPoint.position;enemyInstance.transform.rotation = spawnPoint.rotation;enemyInstance.SetActive(true);// 可能需要重置敵人狀態 (如血量)EnemyHealth health = enemyInstance.GetComponent<EnemyHealth>();if(health != null) health.ResetHealth(); // 假設EnemyHealth有ResetHealth方法}}else // 沒有對象池,直接實例化{if(enemyPrefab != null)enemyInstance = Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);}if(enemyInstance != null){currentEnemyCount++; // 增加計數// 可以監聽敵人的死亡事件來減少計數// EnemyHealth health = enemyInstance.GetComponent<EnemyHealth>();// if(health != null) health.OnDeath += HandleEnemyDeath;}} else {Debug.LogWarning("No spawn points assigned to the EnemySpawner.");yield break; // 沒有生成點,退出協程}// 等待指定時間yield return new WaitForSeconds(spawnDelay);}}// 需要一個方法來處理敵人死亡,以便減少計數和回收對象public void HandleEnemyDeath(GameObject enemy) // 這個方法需要被EnemyHealth在死亡時調用{currentEnemyCount--;if (enemyPool != null){enemyPool.ReturnPooledObject(enemy); // 回收到對象池}else{// Destroy(enemy); // 如果沒有對象池,則銷毀}}
}// EnemyHealth.cs 需要修改Die方法來通知Spawner
// public class EnemyHealth : MonoBehaviour {
//     // ... 其他代碼 ...
//     public EnemySpawner spawner; // 需要引用Spawner, 或者通過事件解耦//     void Die() {
//         // ... 加分邏輯 ...
//         if (spawner != null) {
//             spawner.HandleEnemyDeath(gameObject);
//         } else {
//              // 如果沒有Spawner引用,直接禁用或銷毀(取決于是否用對象池)
//              gameObject.SetActive(false); // 或者 Destroy(gameObject);
//         }
//     }
//     // 重置血量的方法,在從對象池取出時調用
//     public void ResetHealth() { currentHealth = maxHealth; /* 可能還需要重置其他狀態 */ }
// }

3.2 引入對象池優化 (回顧第31天)

頻繁 Instantiate (創建) 和 Destroy (銷毀) GameObject 會產生性能開銷,特別是垃圾回收 (GC) 壓力。對象池通過復用對象來避免這個問題。

3.2.1 回顧對象池原理

對象池預先創建一定數量的對象(例如敵人),并將它們存儲在一個集合(如 ListQueue)中。當需要對象時,從池中取出一個激活;當對象不再需要時(如敵人死亡),將其禁用并放回池中等待下次使用。

3.2.2 集成對象池管理敵人實例

  1. 創建一個通用的 ObjectPool.cs 腳本(可以參考第31天的實現)。
  2. EnemySpawner 腳本中添加對 ObjectPool 的引用 (public ObjectPool enemyPool;)。
  3. 在 Unity 編輯器中,創建一個空 GameObject 作為對象池管理器,掛載 ObjectPool 腳本,并配置好要池化的敵人預制體 (objectToPool) 和初始數量 (amountToPool)。
  4. 將這個對象池管理器拖拽到 EnemySpawnerEnemy Pool 字段上。
  5. 修改 EnemySpawner 的生成邏輯,使用 enemyPool.GetPooledObject() 獲取對象。
  6. 修改敵人的死亡邏輯 (EnemyHealth.Die),使其調用 enemyPool.ReturnPooledObject(gameObject)gameObject.SetActive(false),并通過 EnemySpawnerHandleEnemyDeath 方法來管理回收。

對象池腳本 (簡化示例):

// ObjectPool.cs
using UnityEngine;
using System.Collections.Generic;public class ObjectPool : MonoBehaviour
{public static ObjectPool SharedInstance; // 可選的靜態實例,方便訪問public List<GameObject> pooledObjects;public GameObject objectToPool;public int amountToPool;void Awake(){// SharedInstance = this; // 如果使用靜態實例}void Start(){pooledObjects = new List<GameObject>();GameObject tmp;for (int i = 0; i < amountToPool; i++){tmp = Instantiate(objectToPool);tmp.SetActive(false); // 初始禁用pooledObjects.Add(tmp);tmp.transform.SetParent(this.transform); // (可選) 將池對象作為子對象,方便管理}}public GameObject GetPooledObject(){// 查找池中未激活的對象for (int i = 0; i < pooledObjects.Count; i++){if (!pooledObjects[i].activeInHierarchy){return pooledObjects[i];}}// 如果池中所有對象都在使用,可以選擇返回null或動態擴展池 (簡化版返回null)// 如果需要擴展:// GameObject tmp = Instantiate(objectToPool);// tmp.SetActive(false);// pooledObjects.Add(tmp);// tmp.transform.SetParent(this.transform);// return tmp;Debug.LogWarning("Object Pool for " + objectToPool.name + " is empty. Consider increasing amountToPool.");return null;}// 這個方法在EnemySpawner的HandleEnemyDeath中被間接調用public void ReturnPooledObject(GameObject obj){obj.SetActive(false);// 可選:重置位置到池管理器下// obj.transform.SetParent(this.transform);// obj.transform.localPosition = Vector3.zero;}
}

3.3 敵人管理策略

3.3.1 追蹤當前敵人數量

EnemySpawner 中的 currentEnemyCount 變量可以用來追蹤活動敵人的數量。這對于實現“消滅所有敵人”的勝利條件或根據敵人數量調整難度非常有用。

3.3.2 敵人銷毀與回收

確保敵人在死亡時被正確處理:

  • 使用對象池: 調用 ReturnPooledObject() 或簡單地 SetActive(false),并通過回調(如 HandleEnemyDeath)通知 Spawner 回收。
  • 不使用對象池: 調用 Destroy(gameObject)

四、互動元素:增加游戲趣味性

拾取物(Collectibles/Pickups)是增加游戲互動性和獎勵機制的常見方式。

4.1 設計簡單的拾取物系統

4.1.1 創建拾取物預制體

  1. 創建代表拾取物的 GameObject(例如,一個帶 Sprite Renderer 的 2D 對象,或一個簡單的 3D 模型)。
  2. 添加一個 Collider 組件(如 CircleCollider2DBoxCollider),并勾選 Is Trigger。這樣玩家可以穿過它,同時能檢測到接觸。
  3. 添加一個 Rigidbody 或 Rigidbody2D 組件,并將其 Body Type 設置為 Kinematic 或勾選 Is Trigger 的 Collider 通常就不需要 Rigidbody 來檢測 OnTriggerEnter 了(取決于具體 Unity 版本和設置,但推薦為 Trigger Collider 添加 Kinematic Rigidbody2D/Rigidbody 以確保觸發事件穩定觸發)。
  4. 創建一個腳本(如 PickupItem.cs)附加到該 GameObject 上。
  5. 將配置好的 GameObject 拖拽到 Project 窗口,創建成預制體 (Prefab)。

4.1.2 拾取邏輯實現(碰撞/觸發器)

PickupItem.cs 腳本中使用 OnTriggerEnterOnTriggerEnter2D 來檢測玩家的接觸。

// PickupItem.cs
using UnityEngine;public class PickupItem : MonoBehaviour
{public enum PickupType { Health, Score } // 定義拾取物類型public PickupType type = PickupType.Score; // 默認類型為分數public int value = 10; // 效果值 (加血量或分數)void OnTriggerEnter2D(Collider2D other) // 如果是3D項目,使用 OnTriggerEnter(Collider other){// 檢查接觸的是否是玩家if (other.CompareTag("Player")) // 確保玩家的 GameObject Tag 設置為 "Player"{ApplyEffect(other.gameObject);// 播放音效 (可選)// AudioManager.Instance.PlayPickupSound();// 銷毀或回收到對象池gameObject.SetActive(false); // 簡單禁用,適用于對象池或一次性拾取物// Destroy(gameObject); // 如果不使用對象池}}void ApplyEffect(GameObject player){switch (type){case PickupType.Health:PlayerHealth playerHealth = player.GetComponent<PlayerHealth>();if (playerHealth != null){playerHealth.Heal(value);Debug.Log("Picked up Health: +" + value);}break;case PickupType.Score:if (GameManager.Instance != null){GameManager.Instance.AddScore(value);Debug.Log("Picked up Score: +" + value);}break;}}
}

4.2 拾取效果處理

4.2.1 更新玩家狀態(生命/分數)

ApplyEffect 方法根據拾取物的 type 調用 PlayerHealthHeal 方法或 GameManagerAddScore 方法。

4.2.2 拾取物自身的銷毀/回收

OnTriggerEnter2D 檢測到玩家并應用效果后,拾取物需要從場景中移除。

  • 簡單禁用 (gameObject.SetActive(false)): 適用于一次性拾取物或未來可能通過對象池管理的拾取物。
  • 銷毀 (Destroy(gameObject)): 如果確定不需要復用。

4.3 代碼示例:可拾取物品腳本

上面的 PickupItem.cs 就是一個完整的可拾取物品腳本示例。你可以在 Inspector 中設置它的 Type(Health 或 Score)和 Value

實踐步驟:

  1. 創建拾取物預制體(如一個愛心代表加血,一個金幣代表加分)。
  2. PickupItem.cs 腳本附加到預制體上。
  3. 在 Inspector 中配置 TypeValue
  4. 將預制體拖拽到場景中進行測試,或讓 EnemySpawner (或另一個 Spawner) 也能生成拾取物。
  5. 確保玩家 GameObject 的 Tag 設置為 “Player”。

五、常見問題與排查建議

在整合多個系統時,難免會遇到問題。

5.1 UI不更新怎么辦?

  1. 檢查引用: 確保 GameManagerUIManager 中的 UI 元素引用(如 scoreText, healthSlider)已在 Inspector 中正確拖拽賦值,沒有丟失 (None)。
  2. 檢查腳本: 確認更新 UI 的代碼(如 UpdateScoreUI(), UpdateHealthUI()) 確實在數據變化時被調用了。使用 Debug.Log 跟蹤代碼執行流程。
  3. 檢查事件訂閱 (如果使用事件): 確保事件的發布者(如 PlayerHealth)和訂閱者(如 GameManager)都存在,并且事件訂閱 (+=) 和取消訂閱 (-=) 的邏輯正確,尤其是在對象銷毀或場景加載時。
  4. 檢查 Canvas 設置: 確保 Canvas 正常工作,沒有被禁用或被其他 UI 元素遮擋。
  5. 檢查 Time.timeScale: 如果游戲暫停 (Time.timeScale = 0f),某些依賴時間的 UI 動畫或更新可能停止。確保 UI 更新邏輯不完全依賴于 Time.deltaTime 且能在暫停時執行(如果需要)。

5.2 對象池回收出錯?

  1. 重復回收: 確保一個對象只被回收一次。在回收邏輯(如 HandleEnemyDeath)中添加檢查,防止對已禁用或已回收的對象再次操作。
  2. 未重置狀態: 從對象池取出對象時(GetPooledObject 之后),要確保其狀態被正確重置(如血量、位置、激活的子對象等)。在 EnemyHealth 中添加 ResetState()ResetHealth() 方法,并在 EnemySpawner 中獲取對象后調用它。
  3. 引用丟失: 如果對象池本身被銷毀,或者對池對象的引用丟失,會導致無法獲取或回收。

5.3 勝負條件不觸發?

  1. 邏輯錯誤: 仔細檢查 GameManager 中判斷勝負條件的邏輯 (if (score >= scoreToWin), if (currentHealth <= 0)) 是否正確。
  2. 變量未更新: 使用 Debug.Log 確認 scorecurrentHealth 等關鍵變量是否按預期更新。可能是在加分或扣血的邏輯鏈條中某處斷開了。
  3. 狀態機問題: 如果使用了狀態機,檢查狀態切換 (ChangeState) 是否按預期發生。是否有可能在進入 Win/GameOver 狀態后,條件判斷邏輯仍然在錯誤的狀態下執行?確保在 Update 中首先檢查當前狀態。
  4. 腳本未激活或被銷毀: 確保 GameManager 和相關的腳本(如 PlayerHealth)是激活狀態 (enabled) 且沒有被意外銷毀。

六、總結

恭喜你完成了第42天的學習!今天我們為綜合項目添加了關鍵的系統和內容,讓它變得更加完整和有趣。核心要點回顧:

  1. 構建了核心游戲循環: 我們定義了游戲的基本流程,實現了積分系統和基于玩家狀態(生命值)或目標達成(分數)的勝負條件判斷,并引入了游戲狀態機(GameState)來管理游戲的不同階段(Playing, Paused, GameOver, Win)。
  2. 集成了基礎UI: 我們將核心的游戲數據(生命值、分數)通過 UI 元素(Slider, Text)展示給玩家,并實現了 UI 的實時更新邏輯,同時設置了簡單的游戲結束界面。
  3. 完善了敵人系統: 通過 EnemySpawner 腳本實現了敵人的動態生成,利用生成點控制位置,并探討了引入對象池技術(ObjectPool)來優化性能、避免頻繁創建和銷毀對象的重要性。
  4. 增加了互動元素: 我們創建了可拾取的物品(如加血包、分數道具),使用觸發器 (OnTriggerEnter2D) 檢測玩家拾取,并實現了拾取后的效果處理和物品自身的移除。
  5. 強調了系統整合: 本節的關鍵在于將之前學習的各個模塊(玩家控制、敵人邏輯、UI、數據管理、對象池)通過 GameManager 和事件(或直接引用)有效地組織和連接起來,形成一個協同工作的整體。

通過今天的實踐,你的項目已經具備了一個基礎但完整的游戲框架。在接下來的學習中,我們將關注游戲測試、調試、打包發布,以及探索更多高級主題,繼續打磨我們的作品。繼續努力,你離成為一名合格的 Unity C# 開發者又近了一步!


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/79153.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/79153.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/79153.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SNR8016語音模塊詳解(STM32)

目錄 一、介紹 二、傳感器原理 1.原理圖 2.引腳描述 三、程序設計 main文件 usart.h文件 usart.c文件 四、實驗效果 五、資料獲取 項目分享 一、介紹 SNR8016語音模塊是智納捷科技生產的一種離線語音識別模塊&#xff0c;設計適合用于DIY領域&#xff0c;開放用戶設…

「動態規劃」線性DP:最長上升子序列(LIS)|編輯距離 / LeetCode 300|72(C++)

概述 DP&#xff0c;即動態規劃是解決最優化問題的一類算法&#xff0c;我們要通過將原始問題分解成規模更小的、相似的子問題&#xff0c;通過求解這些易求解的子問題來計算原始問題。 線性DP是一類基本DP&#xff0c;我們來通過它感受DP算法的奧義。 最長上升子序列&#x…

【NumPy完全指南】從基礎操作到高性能計算實戰

&#x1f4d1; 目錄 一、NumPy核心價值1.1 科學計算現狀分析1.2 ndarray設計哲學 二、核心數據結構解析2.1 ndarray內存布局2.2 數據類型體系 三、矢量化編程實踐3.1 通用函數(ufunc)示例3.2 廣播機制圖解 四、高性能計算進階4.1 內存預分配策略4.2 Cython混合編程 五、典型應用…

你的項目有‘哇‘點嗎?

你的項目有哇點嗎&#xff1f; 刷了一下午招聘軟件&#xff0c;發現沒&#xff1f;大廠JD里總愛寫有創新力者優先——可你們的簡歷&#xff0c;創新力還不如食堂菜單&#xff01; 程序員寫項目最大的誤區&#xff1a;把創新當彩蛋藏最后&#xff01;什么參與需求評審負責模塊…

2025年危化品安全員考試題庫及答案

一、單選題 126.安全生產監督管理部門和負有安全生產監督管理職責的有關部門逐級上報事故情況,每級上報的時間不得超過&#xff08;&#xff09;小時。 A.2 B.6 C.12 答案&#xff1a;A 127.按照《安全生產法》規定,危險化學品生產經營單位的從業人員不服從管理,違反安全生…

第十六屆藍橋杯 C/C++ B組 題解

做之前的真題就可以發現&#xff0c;藍橋杯特別喜歡出找規律的題&#xff0c;但是我還是低估了官方的執念。本博客用于記錄第一次藍橋的過程&#xff0c;代碼寫的很爛&#xff0c;洛谷已經有的題解&#xff0c;這里不再贅述&#xff0c;只說自己遇到的問題。用于以后回顧和查找…

C++ 基于多設計模式下的同步異步?志系統-2項目實現

?志系統框架設計 1.?志等級模塊:對輸出?志的等級進?劃分&#xff0c;以便于控制?志的輸出&#xff0c;并提供等級枚舉轉字符串功能。 ? OFF&#xff1a;關閉 ? DEBUG&#xff1a;調試&#xff0c;調試時的關鍵信息輸出。 ? INFO&#xff1a;提?&#xff0c;普通的提?…

提示詞工程(GOT)把思維鏈推理過程圖結構化

Graph of Thoughts&#xff08;GOT&#xff09;&#xff1f; 思維圖&#xff08;Graph of Thoughts&#xff09;是一種結構化的表示方法&#xff0c;用于描述和組織模型的推理過程。它將信息和思維過程以圖的形式表達&#xff0c;其中節點代表想法或信息&#xff0c;邊代表它們…

登錄github失敗---解決方案

登錄github失敗—解決方案 1.使用 Microsoft Edge 瀏覽器 2.https://www.itdog.cn/dns/ 查詢 github.global.ssl.fastly.net github.com 兩個 域名的 IP 3.修改DNS 為 8.8.8.8 8.8.4.4 4.修改windows hosts 文件 5. 使用 Microsoft Edge 瀏覽器 打開github.com

Spring AOP概念及其實現

一、什么是AOP 全稱Aspect Oriented Programming&#xff0c;即面向切面編程&#xff0c;AOP是Spring框架的第二大核心&#xff0c;第一大為IOC。什么是面向切面編程&#xff1f;切面就是指某一類特定的問題&#xff0c;所以AOP也可以稱為面向特定方法編程。例如對異常的統一處…

強化學習_Paper_2017_Curiosity-driven Exploration by Self-supervised Prediction

paper Link: ICM: Curiosity-driven Exploration by Self-supervised Prediction GITHUB Link: 官方: noreward-rl 1- 主要貢獻 對好奇心進行定義與建模 好奇心定義&#xff1a;next state的prediction error作為該state novelty 如果智能體真的“懂”一個state&#xff0c;那…

spring中的@Configuration注解詳解

一、概述與核心作用 Configuration是Spring框架中用于定義配置類的核心注解&#xff0c;旨在替代傳統的XML配置方式&#xff0c;通過Java代碼實現Bean的聲明、依賴管理及環境配置。其核心作用包括&#xff1a; 標識配置類&#xff1a;標記一個類為Spring的配置類&#xff0c;…

7.計算機網絡相關術語

7. 計算機網絡相關術語 ACK (Acknowledgement) 確認 ADSL (Asymmetric Digital Subscriber Line) 非對稱數字用戶線 AP (Access Point) 接入點 AP (Application) 應用程序 API (Application Programming Interface) 應用編程接口 APNIC (Asia Pacific Network Informatio…

Hadoop 集群基礎指令指南

目錄 &#x1f9e9; 一、Hadoop 基礎服務管理指令 ?? 啟動 Hadoop ?? 關閉 Hadoop &#x1f9fe; 查看進程是否正常運行 &#x1f4c1; 二、HDFS 常用文件系統指令 &#x1f6e0;? 三、MapReduce 作業運行指令 &#x1f4cb; 四、集群狀態監控指令 &#x1f4a1; …

【MySQL數據庫】事務

目錄 1&#xff0c;事務的詳細介紹 2&#xff0c;事務的屬性 3&#xff0c;事務常見的操作方式 1&#xff0c;事務的詳細介紹 在MySQL數據庫中&#xff0c;事務是指一組SQL語句作為一個指令去執行相應的操作&#xff0c;這些操作要么全部成功提交&#xff0c;對數據庫產生影…

一、OrcaSlicer源碼編譯

一、下載 1、OrcaSlicer 2.3.0版本的源碼 git clone https://github.com/SoftFever/OrcaSlicer.git -b v2.3.0 二、編譯 1、在OrcaSlicer目錄運行cmd窗口&#xff0c;輸入build_release.bat 2、如果出錯了&#xff0c;可以多運行幾次build_release.bat 3、在OrcaSlicer\b…

港口危貨儲存單位主要安全管理人員考試精選題目

港口危貨儲存單位主要安全管理人員考試精選題目 1、危險貨物儲存場所的電氣設備應符合&#xff08; &#xff09;要求。 A. 防火 B. 防爆 C. 防塵 D. 防潮 答案&#xff1a;B 解析&#xff1a;港口危貨儲存單位存在易燃易爆等危險貨物&#xff0c;電氣設備若不防爆&…

格雷希爾用于工業氣體充裝站的CZ系列氣罐充裝轉換連接器,其日常維護有哪些

格雷希爾氣瓶充裝連接器&#xff0c;長期用于壓縮氣體的快速充裝和壓縮氣瓶的氣密性檢測&#xff0c;需要進行定期的維護&#xff0c;為每一次的充裝提供更好的連接。下列建議的幾點維護準則適用于格雷希爾所有充注接頭&#xff0c;請非專業人士不要隨意拆卸連接器。 格雷希爾氣…

Java 多線程進階:什么是線程安全?

在多線程編程中&#xff0c;“線程安全”是一個非常重要但又常被誤解的概念。尤其對于剛接觸多線程的人來說&#xff0c;不理解線程安全的本質&#xff0c;容易寫出“偶爾出錯”的代碼——這類 bug 往往隱蔽且難以復現。 本文將用盡可能通俗的語言&#xff0c;從三個角度解釋線…

MSO-Player:基于vlc的Unity直播流播放器,支持主流RTSP、RTMP、HTTP等常見格式

MSO-Player 基于libVLC的Unity視頻播放解決方案 支持2D視頻和360度全景視頻播放的Unity插件 &#x1f4d1; 目錄 &#x1f3a5; MSO-Player &#x1f4cb; 功能概述&#x1f680; 快速入門&#x1f4da; 關鍵組件&#x1f4dd; 使用案例&#x1f50c; 依賴項&#x1f4cb; 注意…