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背后的核心原理全揭秘
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)
欢迎来到《C# for Unity游戏开发学习之旅》的第36天!经过前几周对C#基础、面向对象、数据结构以及Unity核心机制的学习,今天我们将进入一个提升代码质量和项目可维护性的关键领域——设计模式。设计模式是软件开发中经过验证的、解决特定问题的可复用方案。在复杂的游戏项目中,合理运用设计模式能够显著优化游戏架构,降低模块间的耦合度,提高代码的可读性、扩展性和健壮性。
本篇将聚焦于Unity开发中最常用、也最基础的两个设计模式:单例(Singleton)模式和观察者(Observer)模式。我们将深入探讨它们的原理、实现方式、在Unity中的具体应用场景(如全局管理器、事件驱动系统),并进行利弊分析。最后,我们会通过一个实战演练,亲手实现一个GameManager
单例,并运用观察者模式构建一个简单的成就解锁通知系统。让我们一起学习如何运用这些经典模式,让我们的Unity项目架构更加优雅和高效!
设计模式(Design Pattern)是在软件工程领域中,针对特定问题、环境下反复出现的、经过实践验证的、可复用的解决方案。它不是一段可以直接复制粘贴的代码,而是一种思想、一种最佳实践、一种通用词汇。
目的:
类比: 就像建筑师使用标准化的蓝图元素(如承重墙、窗户类型)来设计建筑一样,软件开发者使用设计模式来构建健壮、灵活的软件系统。
设计模式通常分为三大类:
Unity本身基于**组件化(Component-Based)**的设计哲学,这本身就是一种架构模式。然而,随着项目规模的增长,单纯的组件化可能会导致以下问题:
设计模式可以帮助我们解决这些问题:
因此,在Unity项目中理解并恰当运用设计模式,对于构建可维护、可扩展、高性能的游戏至关重要。
单例模式确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一的实例。
通常通过以下方式实现:
private
),防止外部直接通过 new
创建实例。static
)私有实例变量。public
)静态方法或属性,用于获取这个唯一的实例。如果实例不存在,则创建它;如果已存在,则直接返回。类比: 就像一所学校只有一个校长办公室,所有需要找校长处理事务的人都必须通过这个唯一的入口。
在Unity中,单例通常用于管理全局性的游戏状态或服务,并且这些管理器往往需要是MonoBehaviour
以便能挂载到游戏对象上,并利用Unity的生命周期函数(如Awake
, Start
)。
using UnityEngine;
// 适用于不需要挂载到GameObject上的纯C#类
public class SettingsManager
{
// 1. 私有静态实例变量
private static SettingsManager _instance;
// 3. 公共静态属性,用于获取实例 (懒汉式加载)
public static SettingsManager Instance
{
get
{
// 如果实例不存在,则创建
if (_instance == null)
{
_instance = new SettingsManager();
Debug.Log("SettingsManager instance created.");
// 在这里可以进行初始化设置加载等
}
return _instance;
}
}
// 2. 私有构造函数
private SettingsManager()
{
// 防止外部 new
LoadSettings(); // 示例:构造时加载设置
}
// 示例:成员变量和方法
public float MasterVolume { get; private set; } = 1.0f;
private void LoadSettings()
{
// 实际中可能从PlayerPrefs或文件加载
MasterVolume = PlayerPrefs.GetFloat("MasterVolume", 1.0f);
Debug.Log("Settings loaded. Master Volume: " + MasterVolume);
}
public void SetMasterVolume(float volume)
{
MasterVolume = Mathf.Clamp01(volume);
PlayerPrefs.SetFloat("MasterVolume", MasterVolume);
Debug.Log("Master Volume set to: " + MasterVolume);
// 可能还需要触发音量更新事件
}
}
// 如何使用:
// SettingsManager.Instance.SetMasterVolume(0.8f);
// float currentVolume = SettingsManager.Instance.MasterVolume;
注意: 这种纯C#单例在Unity中用途有限,因为它无法利用MonoBehaviour
的特性(如协程、生命周期函数、Inspector面板)。
这是Unity中最常见的单例实现方式,通常在Awake
方法中进行初始化和实例检查。
using UnityEngine;
// 泛型 MonoBehaviour 单例基类,方便复用
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
private static readonly object _lock = new object(); // 用于线程安全(虽然Unity主线程操作通常不需要,但作为良好实践)
private static bool _applicationIsQuitting = false;
public static T Instance
{
get
{
if (_applicationIsQuitting)
{
Debug.LogWarning($"[Singleton] Instance '{typeof(T)}' already destroyed on application quit. Won't create again - returning null.");
return null;
}
lock (_lock) // 锁定以确保线程安全
{
if (_instance == null)
{
// 尝试在场景中查找实例
_instance = FindObjectOfType<T>();
// 如果场景中找到多个实例
if (FindObjectsOfType<T>().Length > 1)
{
Debug.LogError($"[Singleton] Something went really wrong - there should never be more than 1 singleton! Reopening the scene might fix it. Type: {typeof(T)}");
return _instance; // 返回找到的第一个,但发出严重错误警告
}
// 如果场景中没有找到实例
if (_instance == null)
{
// 尝试动态创建一个新的GameObject挂载单例脚本
GameObject singletonObject = new GameObject();
_instance = singletonObject.AddComponent<T>();
singletonObject.name = typeof(T).ToString() + " (Singleton)";
// 使单例在场景切换时不被销毁 (根据需要选择是否保留)
// DontDestroyOnLoad(singletonObject); // 按需取消注释
Debug.Log($"[Singleton] An instance of {typeof(T)} is needed in the scene, so '{singletonObject.name}' was created.");
}
else
{
Debug.Log($"[Singleton] Using instance already created: {_instance.gameObject.name}");
// 如果需要跨场景保留,确保找到的实例也被标记
// DontDestroyOnLoad(_instance.gameObject); // 按需取消注释
}
}
return _instance;
}
}
}
// 可选:在Awake中进行实例检查,防止手动放置多个实例
protected virtual void Awake()
{
if (_instance == null)
{
_instance = this as T;
// 根据需要决定是否跨场景保留
// DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
Debug.LogWarning($"[Singleton] Another instance of {typeof(T)} detected on {gameObject.name}. Destroying this duplicate.");
Destroy(gameObject); // 销毁重复的实例
}
}
// 防止在程序退出后,其他脚本调用Instance时重新创建幽灵对象
protected virtual void OnDestroy()
{
if (_instance == this)
{
_applicationIsQuitting = true; // 标记程序正在退出
}
}
// 可选:处理程序退出事件,更可靠地设置退出标记
protected virtual void OnApplicationQuit()
{
_applicationIsQuitting = true;
}
}
// 如何使用:
// 1. 创建你的管理器脚本,继承自 Singleton
public class GameManager : Singleton<GameManager>
{
// 在这里添加GameManager的特定逻辑和数据
public int Score { get; private set; }
// Awake可以被重写,但记得调用 base.Awake() 如果基类有逻辑
protected override void Awake()
{
base.Awake(); // 调用基类的Awake逻辑
Debug.Log("GameManager Awake called.");
// GameManager 特有的初始化
Score = 0;
}
public void AddScore(int amount)
{
Score += amount;
Debug.Log($"Score: {Score}");
// 这里可以触发一个得分更新事件(后面会讲观察者模式)
}
}
// 2. 在其他任何脚本中访问:
// GameManager.Instance.AddScore(10);
// int currentScore = GameManager.Instance.Score;
关键点:
where T : MonoBehaviour
: 泛型约束,确保T
必须是MonoBehaviour
或其子类。Awake()
: Unity的生命周期函数,在对象加载时调用,适合进行初始化和实例检查。FindObjectOfType()
: 如果实例未被赋值(例如场景刚加载),尝试在场景中查找。DontDestroyOnLoad(gameObject)
: (可选) 使挂载此脚本的GameObject在加载新场景时不会被销毁,常用于需要跨场景存在的管理器(如AudioManager, GameManager)。Awake
中的检查可以防止开发者不小心在场景中放置多个单例对象。_applicationIsQuitting
): 防止在应用程序关闭过程中,某个OnDestroy
里的代码又去访问即将销毁的单例,导致创建“幽灵”对象。在Unity中,大部分脚本逻辑运行在主线程上,因此基础的MonoBehaviour单例通常不需要复杂的线程锁。然而,如果你在Unity中使用了多线程(例如进行网络通信、后台数据处理),并且需要在其他线程访问单例,那么上面泛型示例中的lock (_lock)
就变得必要,以防止竞态条件。但在常规游戏逻辑中,过度使用锁可能带来不必要的性能开销。
单例模式非常适合用于表示那些在概念上全局唯一且需要方便访问的组件或服务:
GameManager
承担过多不相关的任务。DontDestroyOnLoad
)。不必要的常驻对象会占用内存。Start
或提供显式的初始化方法。观察者模式定义了一种一对多的依赖关系,让多个观察者(Observer)对象同时监听某一个主题(Subject)对象。当主题对象的状态发生变化时,它会自动通知所有依赖于它的观察者对象,使它们能够自动更新自己。
Attach
/Subscribe
)和移除(Detach
/Unsubscribe
)观察者的方法。Notify
)方法,遍历观察者列表,调用每个观察者的更新(Update
)方法。类比: 就像订阅报纸或YouTube频道。报社/UP主(Subject)发布新内容(状态变化)时,所有订阅者(Observer)都会收到通知(新报纸/视频推送),并可以自行决定如何处理(阅读/观看)。
在Unity C#中,有多种方式可以实现观察者模式,各有优劣。
通过定义接口 ISubject
和 IObserver
来实现。
using System.Collections.Generic;
using UnityEngine;
// 观察者接口
public interface IObserver
{
void OnNotify(object subject, string eventType); // 参数可以更具体,例如传递事件数据
}
// 主题接口
public interface ISubject
{
void AddObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers(string eventType);
}
// 示例:玩家状态作为主题
public class PlayerStatus : MonoBehaviour, ISubject
{
private List<IObserver> _observers = new List<IObserver>();
private int _health = 100;
public int Health
{
get => _health;
set
{
if (_health != value)
{
_health = value;
NotifyObservers("HealthChanged"); // 血量变化时通知
}
}
}
public void AddObserver(IObserver observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
}
public void RemoveObserver(IObserver observer)
{
if (_observers.Contains(observer))
{
_observers.Remove(observer);
}
}
public void NotifyObservers(string eventType)
{
// 创建副本以防在通知过程中列表被修改
List<IObserver> observersSnapshot = new List<IObserver>(_observers);
foreach (var observer in observersSnapshot)
{
observer.OnNotify(this, eventType);
}
}
// 模拟受到伤害
public void TakeDamage(int amount)
{
Health -= amount;
}
}
// 示例:UI血条作为观察者
public class HealthBarUI : MonoBehaviour, IObserver
{
public PlayerStatus playerStatus; // 引用主题
public UnityEngine.UI.Slider healthSlider; // 引用UI控件
void Start()
{
if (playerStatus != null)
{
playerStatus.AddObserver(this); // 注册自己
UpdateHealthBar(playerStatus.Health); // 初始化显示
}
}
void OnDestroy()
{
if (playerStatus != null)
{
playerStatus.RemoveObserver(this); // 对象销毁时注销,非常重要!
}
}
public void OnNotify(object subject, string eventType)
{
if (subject is PlayerStatus status && eventType == "HealthChanged")
{
UpdateHealthBar(status.Health);
}
}
private void UpdateHealthBar(int currentHealth)
{
if (healthSlider != null)
{
// 假设血量最大值为100
healthSlider.value = (float)currentHealth / 100.0f;
Debug.Log($"HealthBar UI updated: {currentHealth}%");
}
}
}
优点: 完全控制实现细节,最符合经典定义。
缺点: 代码量较多,需要手动管理订阅和取消订阅,容易忘记取消订阅导致内存泄漏。
C#内置的 event
关键字和委托(delegate
)是实现观察者模式的更简洁、类型安全的方式。这在第23天我们已经学习过。
using System;
using UnityEngine;
using UnityEngine.UI; // For Slider example
// 主题类 (发布者)
public class PlayerNotifier : MonoBehaviour
{
// 定义委托类型 (事件的签名)
public delegate void HealthChangedHandler(int newHealth);
// 定义事件 (基于委托)
public event HealthChangedHandler OnHealthChanged;
// 定义另一个事件
public event Action<int> OnScoreChanged; // 使用泛型Action委托更简洁
private int _health = 100;
private int _score = 0;
public int Health
{
get => _health;
set
{
if (_health != value)
{
_health = Mathf.Clamp(value, 0, 100);
// 触发事件,通知所有订阅者
OnHealthChanged?.Invoke(_health); // ?. 安全调用,如果没订阅者则不执行
Debug.Log($"Player health changed to: {_health}. Event invoked.");
}
}
}
public int Score
{
get => _score;
set
{
if (_score != value)
{
_score = value;
OnScoreChanged?.Invoke(_score);
Debug.Log($"Player score changed to: {_score}. Event invoked.");
}
}
}
// 模拟操作
void Update()
{
if (Input.GetKeyDown(KeyCode.DownArrow))
{
Health -= 10;
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
Score += 10;
}
}
}
// 观察者类 (订阅者)
public class UIManager : MonoBehaviour
{
public PlayerNotifier playerNotifier; // 引用主题
public Slider healthSlider;
public Text scoreText;
void Start()
{
if (playerNotifier != null)
{
// 订阅事件 (使用 +=)
playerNotifier.OnHealthChanged += UpdateHealthUI;
playerNotifier.OnScoreChanged += UpdateScoreUI;
// 初始化UI
UpdateHealthUI(playerNotifier.Health);
UpdateScoreUI(playerNotifier.Score);
Debug.Log("UIManager subscribed to PlayerNotifier events.");
}
else
{
Debug.LogError("PlayerNotifier reference not set in UIManager.");
}
}
void OnDestroy()
{
if (playerNotifier != null)
{
// 取消订阅 (使用 -=),非常重要!
playerNotifier.OnHealthChanged -= UpdateHealthUI;
playerNotifier.OnScoreChanged -= UpdateScoreUI;
Debug.Log("UIManager unsubscribed from PlayerNotifier events.");
}
}
// 事件处理方法 (签名需匹配委托)
private void UpdateHealthUI(int newHealth)
{
if (healthSlider != null)
{
healthSlider.value = (float)newHealth / 100.0f;
Debug.Log($"Health UI updated via event: {newHealth}");
}
}
private void UpdateScoreUI(int newScore)
{
if (scoreText != null)
{
scoreText.text = $"Score: {newScore}";
Debug.Log($"Score UI updated via event: {newScore}");
}
}
}
优点: 语法简洁,类型安全,由.NET运行时管理订阅列表,不易出错。是纯C#代码间解耦的首选。
缺点: 订阅和取消订阅仍需手动编写代码,在Inspector面板中不可见或配置。
UnityEvent
是Unity引擎提供的事件系统,优点是可以在Inspector面板中可视化地配置事件的触发和监听,非常适合设计师或非程序员使用,也方便在运行时动态添加/移除监听。这在第24天我们已经学习过。
using UnityEngine;
using UnityEngine.Events; // 引入UnityEvent命名空间
using UnityEngine.UI;
// 定义一个可以传递int参数的UnityEvent子类
[System.Serializable]
public class HealthChangedEvent : UnityEvent<int> { }
[System.Serializable]
public class ScoreChangedEvent : UnityEvent<int> { }
// 主题类 (发布者)
public class PlayerEvents : MonoBehaviour
{
// 在Inspector中可见并可配置的事件
public HealthChangedEvent OnHealthChanged = new HealthChangedEvent();
public ScoreChangedEvent OnScoreChanged = new ScoreChangedEvent();
private int _health = 100;
private int _score = 0;
public int Health
{
get => _health;
set
{
if (_health != value)
{
_health = Mathf.Clamp(value, 0, 100);
// 触发UnityEvent
OnHealthChanged.Invoke(_health);
Debug.Log($"Player health changed to: {_health}. UnityEvent invoked.");
}
}
}
public int Score
{
get => _score;
set
{
if (_score != value)
{
_score = value;
OnScoreChanged.Invoke(_score);
Debug.Log($"Player score changed to: {_score}. UnityEvent invoked.");
}
}
}
// 模拟操作
void Update()
{
if (Input.GetKeyDown(KeyCode.PageDown)) // 使用不同按键避免冲突
{
Health -= 10;
}
if (Input.GetKeyDown(KeyCode.PageUp))
{
Score += 10;
}
}
}
// 观察者类 (监听者) - 注意:这里的监听方法需要是public
public class GameUIController : MonoBehaviour
{
public Slider healthSlider;
public Text scoreText;
// 这个方法需要是 public 才能在 Inspector 中被 UnityEvent 选中
public void UpdateHealthDisplay(int newHealth)
{
if (healthSlider != null)
{
healthSlider.value = (float)newHealth / 100.0f;
Debug.Log($"Health display updated via UnityEvent: {newHealth}");
}
}
// 这个方法也需要是 public
public void UpdateScoreDisplay(int newScore)
{
if (scoreText != null)
{
scoreText.text = $"Score: {newScore}";
Debug.Log($"Score display updated via UnityEvent: {newScore}");
}
}
// 注意:使用UnityEvent时,通常不需要在代码中显式 Start/OnDestroy 中订阅/取消订阅
// 监听关系主要在 Inspector 中配置。
// 如果需要代码动态添加监听,可以使用:
// playerEventsInstance.OnHealthChanged.AddListener(UpdateHealthDisplay);
// playerEventsInstance.OnHealthChanged.RemoveListener(UpdateHealthDisplay);
}
配置步骤:
PlayerEvents
脚本挂载到一个GameObject上(例如 “Player”)。GameUIController
脚本挂载到另一个GameObject上(例如 “UIManager”),并将对应的Slider和Text组件拖拽到脚本的公共字段上。PlayerEvents
的GameObject的Inspector面板中,找到On Health Changed
和On Score Changed
事件。+
号添加监听器。GameUIController
的GameObject拖拽到事件监听器列表的 Object
字段上。GameUIController
-> UpdateHealthDisplay (int)
或 UpdateScoreDisplay (int)
。优点: Inspector可视化配置,方便非程序员协作;运行时动态添加/移除监听相对容易;Unity自动处理了部分生命周期管理(但在某些复杂情况下仍需手动移除监听)。
缺点: 相比C#事件可能有微小的性能开销(通常不显著);对于纯粹的代码逻辑耦合,不如C#事件直接。
特性 | 手动实现观察者模式 | C# 事件 (event ) |
UnityEvent |
---|---|---|---|
实现复杂度 | 高 | 中等 (语法糖) | 低 (主要靠Inspector) |
类型安全 | 取决于实现 | 强类型 | 强类型 (支持泛型) |
性能 | 取决于实现 | 通常最高 | 略有开销 (反射调用等) |
Inspector集成 | 无 | 无 | 强 (可视化配置) |
代码耦合 | 中等 (接口依赖) | 低 (委托/事件依赖) | 低 (Inspector配置/代码监听) |
适用场景 | 学习原理、特殊需求 | 纯C#类间、脚本核心逻辑解耦 | UI交互、编辑器配置、跨脚本通信 |
主要缺点 | 易出错、代码量大 | 无法Inspector配置、需手动管理 | 性能略低、过度配置可能混乱 |
结论:
观察者模式非常适合实现事件驱动的系统,当一个状态变化需要通知多个不相关的模块时:
RemoveObserver
, -=
, RemoveListener
),主题会一直持有对观察者的引用,导致观察者对象无法被垃圾回收器回收,造成内存泄漏。OnDestroy
或OnDisable
中),一定要记得取消订阅。这是使用观察者模式最需要注意的地方。Action
、UnityEvent
或自定义事件参数类。OnNotify
, 事件处理函数)应尽可能轻量,避免执行耗时操作,否则可能阻塞主题或其他观察者。如果需要执行复杂逻辑,可以考虑在更新方法中启动协程或放入任务队列。除了单例和观察者,Unity开发中还有一些其他常用的设计模式值得了解:
定义一个用于创建对象的接口(或基类),让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
EnemyFactory
,根据传入的类型(如"Goblin", “Orc”)或难度级别,创建并返回相应的敌人预制体实例。优点: 将对象的创建逻辑集中管理,客户端代码与具体产品类解耦。易于扩展,增加新产品时只需增加新的工厂子类或修改工厂方法。
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
优点: 将与特定状态相关的行为局部化到状态对象中,避免了巨大的条件语句(if/else 或 switch)。状态转换逻辑清晰。易于增加新状态。
预先创建并存储一组对象(“池”),当需要对象时,从池中获取一个,使用完毕后不销毁,而是归还到池中,以便后续复用。
Instantiate
和Destroy
操作带来的性能开销和内存碎片。这在第31天我们已经讨论过。优点: 显著提升性能,减少GC(垃圾回收)压力。
现在,我们将结合单例模式和观察者模式(使用C#事件)来实现一个简单的示例。
目标:
GameManager
单例,负责跟踪分数。AchievementManager
,监听GameManager
的分数变化事件。AchievementManager
触发一个“获得高分”成就的通知。GameManager.cs
脚本:using System;
using UnityEngine;
// 继承自之前定义的泛型 Singleton 基类
public class GameManager : Singleton<GameManager>
{
// 使用 C# 事件来实现分数变化的通知 (观察者模式)
public event Action<int> OnScoreChanged; // Action 是一个委托,表示接受一个int参数且无返回值的方法
private int _score = 0;
public int Score
{
get => _score;
private set // 外部只能读取,修改通过方法进行
{
if (_score != value)
{
_score = value;
Debug.Log($"[GameManager] Score updated to: {_score}");
// 触发分数变化事件,通知所有订阅者
OnScoreChanged?.Invoke(_score);
}
}
}
// Awake由基类处理实例保证,这里可以添加GameManager特定的初始化
protected override void Awake()
{
base.Awake(); // 必须调用基类的Awake来确保单例逻辑执行
// 确保GameManager跨场景存在 (如果需要)
DontDestroyOnLoad(gameObject); // 取消注释则跨场景保留
Debug.Log("[GameManager] Instance initialized.");
// 初始化分数 (虽然默认为0,显式写一下更清晰)
Score = 0;
}
// 公共方法用于增加分数
public void AddScore(int amount)
{
if (amount > 0)
{
Score += amount;
}
}
// 可以在这里添加其他全局管理功能,例如游戏状态控制
public enum GameState { MainMenu, Playing, Paused, GameOver }
public GameState CurrentState { get; private set; } = GameState.MainMenu;
public void StartGame()
{
if (CurrentState == GameState.MainMenu || CurrentState == GameState.GameOver)
{
CurrentState = GameState.Playing;
Score = 0; // 开始新游戏时重置分数
Debug.Log("[GameManager] Game Started!");
// 可能还需要加载游戏场景等操作
}
}
public void PauseGame(bool isPaused)
{
if (CurrentState == GameState.Playing || CurrentState == GameState.Paused)
{
CurrentState = isPaused ? GameState.Paused : GameState.Playing;
Time.timeScale = isPaused ? 0f : 1f; // 控制游戏时间流速实现暂停/恢复
Debug.Log($"[GameManager] Game {(isPaused ? "Paused" : "Resumed")}");
}
}
// ... 其他方法如 GameOver(), GoToMainMenu() 等
}
GameManager.cs
脚本挂载到这个GameObject上。GameManager
继承自Singleton
,它会自动处理实例的唯一性。AchievementManager.cs
脚本:using UnityEngine;
public class AchievementManager : MonoBehaviour
{
public int highScoreThreshold = 100; // 高分成就阈值,可在Inspector设置
private bool highScoreAchieved = false;
void Start()
{
// 检查GameManager实例是否存在并订阅事件
if (GameManager.Instance != null)
{
GameManager.Instance.OnScoreChanged += HandleScoreChanged;
Debug.Log("[AchievementManager] Subscribed to GameManager.OnScoreChanged event.");
// 初始化时检查一下当前分数是否已满足条件 (例如加载游戏存档后)
HandleScoreChanged(GameManager.Instance.Score);
}
else
{
Debug.LogError("[AchievementManager] GameManager instance not found on Start! Cannot subscribe to score changes.");
}
}
void OnDestroy()
{
// 在对象销毁时务必取消订阅,防止内存泄漏!
if (GameManager.Instance != null)
{
GameManager.Instance.OnScoreChanged -= HandleScoreChanged;
Debug.Log("[AchievementManager] Unsubscribed from GameManager.OnScoreChanged event.");
}
}
// 事件处理方法,当分数变化时由GameManager调用
private void HandleScoreChanged(int newScore)
{
// 检查是否达到高分阈值且尚未获得成就
if (!highScoreAchieved && newScore >= highScoreThreshold)
{
UnlockHighScoreAchievement();
}
}
private void UnlockHighScoreAchievement()
{
highScoreAchieved = true;
Debug.LogWarning($"[AchievementManager] Achievement Unlocked: High Score! (Reached {GameManager.Instance.Score} points, threshold was {highScoreThreshold})");
// 在这里可以触发UI提示、播放音效、保存成就状态等
// 例如:UIManager.Instance.ShowAchievementPopup("High Score!");
}
// 可选: 添加重置成就状态的方法,用于测试或新游戏
public void ResetAchievements()
{
highScoreAchieved = false;
Debug.Log("[AchievementManager] Achievements reset.");
}
}
AchievementManager.cs
脚本挂载到这个GameObject上。High Score Threshold
的值。为了测试,我们可以创建一个简单的脚本来模拟得分。
ScoreAdder.cs
脚本:using UnityEngine;
public class ScoreAdder : MonoBehaviour
{
public int scoreToAdd = 10;
void Update()
{
// 按下空格键时,通过GameManager单例增加分数
if (Input.GetKeyDown(KeyCode.Space))
{
if (GameManager.Instance != null)
{
GameManager.Instance.AddScore(scoreToAdd);
Debug.Log($"[ScoreAdder] Added {scoreToAdd} score via Spacebar.");
}
else
{
Debug.LogError("[ScoreAdder] GameManager instance not found! Cannot add score.");
}
}
// 按下 R 键重置成就 (用于测试)
if (Input.GetKeyDown(KeyCode.R))
{
AchievementManager am = FindObjectOfType<AchievementManager>();
if (am != null)
{
am.ResetAchievements();
if(GameManager.Instance != null)
{
GameManager.Instance.AddScore(0); // 触发一次分数更新以防万一初始分数就超过阈值
}
}
}
// 按下 S 键开始游戏 (用于测试GameManager状态)
if (Input.GetKeyDown(KeyCode.S))
{
if (GameManager.Instance != null)
{
GameManager.Instance.StartGame();
}
}
}
}
ScoreAdder.cs
脚本挂载到任意一个活动的GameObject上(例如主摄像机)。测试流程:
GameManager
和AchievementManager
已初始化并成功订阅事件。GameManager
触发了OnScoreChanged
事件。AchievementManager
中设置的highScoreThreshold
。AchievementManager
解锁成就的日志。highScoreAchieved
标志位的作用)。GameManager.StartGame()
测试状态切换和分数重置。这个简单的实战演练展示了如何结合使用单例模式(GameManager
作为全局访问点)和观察者模式(AchievementManager
监听GameManager
的分数变化事件)来构建一个解耦且可扩展的游戏系统。
今天我们深入探讨了Unity开发中两种至关重要的设计模式:单例模式和观察者模式。掌握它们对于构建结构清晰、易于维护和扩展的游戏项目非常有帮助。
以下是本次学习的核心要点:
MonoBehaviour
的泛型单例,利用Awake()
保证唯一性,可选DontDestroyOnLoad
实现跨场景。event
)或Unity内置的UnityEvent
。C#事件简洁高效,适用于代码逻辑;UnityEvent可在Inspector配置,适合UI交互和策划配置。GameManager
单例来管理分数,并使用C#事件实现了AchievementManager
作为观察者监听分数变化,从而解锁成就。这展示了模式结合使用的威力。设计模式是工具,不是银弹。理解其原理、优缺点和适用场景,并根据项目实际需求灵活选用、组合,才能真正发挥它们的价值。希望通过今天的学习,你能更有信心地在你的Unity项目中运用设计模式,编写出更优雅、更健壮的代码!继续加油!