【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.mzph.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; 注意…