【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)


文章目录


前言

大家好!欢迎来到“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;
                // 显示失败UI
                break;
            case GameState.Win:
                 isGameOver = true; // 确保设置结束标志
                Time.timeScale = 0f;
                // 显示胜利UI
                break;
        }
    }

    // 在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 Panel
    public 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); // 显示失败UI
                break;
            case GameState.Win:
                 isGameOver = true;
                Time.timeScale = 0f;
                 if(winPanel) winPanel.SetActive(true); // 显示胜利UI
                break;
        }
        // 在状态切换时,也可以隐藏/显示相应的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# 开发者又近了一步!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴师兄大模型

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值