8.3 观察者模式 (Observer) 八、设计模式 8.3 观察者模式 (Observer) 观察者模式是一种行为设计模式,它定义了对象之间一对多的依赖关系,当一个对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都将得到通知并自动更新。这种模式旨在降低对象之间的耦合度,使得系统更易于扩展和维护。 在游戏开发,特别是使用Unity3D引擎进行开发时,观察者模式的应用非常广泛。例如,当游戏角色的生命值发生变化时,我们需要更新UI界面上的生命值显示,同时可能还需要触发一些游戏逻辑,如检查角色是否死亡。使用观察者模式可以优雅地处理这类事件,使得游戏逻辑与UI更新解耦,提高代码的可维护性和灵活性。 8.3.
观察者模式是一种行为设计模式,它定义了对象之间一对多的依赖关系,当一个对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都将得到通知并自动更新。这种模式旨在降低对象之间的耦合度,使得系统更易于扩展和维护。
在游戏开发,特别是使用Unity3D引擎进行开发时,观察者模式的应用非常广泛。例如,当游戏角色的生命值发生变化时,我们需要更新UI界面上的生命值显示,同时可能还需要触发一些游戏逻辑,如检查角色是否死亡。使用观察者模式可以优雅地处理这类事件,使得游戏逻辑与UI更新解耦,提高代码的可维护性和灵活性。
意图:
定义对象之间的一种一对多的依赖关系,使得每当一个对象状态改变,则所有依赖于它的对象都会得到通知并被自动更新。
将观察者和被观察者解耦,使得各自的改变不会互相影响。
支持广播通信,被观察者的状态变化可以通知多个观察者。
结构:
观察者模式主要包含以下几个核心角色:
Subject(主题/被观察者): 维护观察者列表,提供添加、移除和通知观察者的方法。当Subject的状态发生变化时,它会遍历观察者列表并通知所有注册的观察者。
Observer(观察者): 定义一个更新接口,用于接收来自Subject的通知。
ConcreteSubject(具体主题/具体被观察者): Subject的具体实现类,负责维护自身的状态,并在状态改变时通知所有观察者。
ConcreteObserver(具体观察者): Observer的具体实现类,实现更新接口,在接收到Subject的通知后执行相应的操作。
使用 Mermaid 绘制的 graph TD 图可以更清晰地展示观察者模式的结构:
结构解析:
Subject 类定义了观察者模式的核心接口,它不关心具体的观察者是谁,只负责管理观察者列表和通知机制。
Observer 接口定义了观察者需要实现的 update() 方法,用于接收主题的通知。
ConcreteSubject 是具体的主题类,它继承自 Subject 或实现 Subject 的接口,并负责维护自身的状态。当状态发生变化时,它会调用 notifyObservers() 方法通知所有已注册的 ConcreteObserver。
ConcreteObserver 是具体的观察者类,它实现了 Observer 接口,并定义了接收到通知后需要执行的具体操作。ConcreteObserver 需要向 ConcreteSubject 注册,以便接收状态变化的通知。
参与者:
Subject (主题/被观察者):
知道它的观察者。可以有任意数量的观察者观察同一个主题。
提供注册和移除观察者对象的接口。
Observer (观察者):
为那些在主题状态发生改变时需要获得通知的对象定义一个更新接口。
Update() 接口是观察者模式的关键,定义了观察者接收通知的方式。
ConcreteSubject (具体主题/具体被观察者):
ConcreteSubject 是 Subject 的子类或实现类。
存储 ConcreteObserver 感兴趣的状态。
当 ConcreteSubject 的状态发生改变时,它会通知所有注册的观察者。
ConcreteObserver (具体观察者):
ConcreteObserver 是 Observer 接口的具体实现类。
维护一个指向 ConcreteSubject 实例的引用。
存储应该与主题状态保持一致的状态。
实现 Observer 的 Update() 接口,以便在主题状态改变时更新自身状态。
协作方式:
ConcreteObserver 向 ConcreteSubject 注册,表示它希望观察 ConcreteSubject 的状态变化。
ConcreteSubject 维护一个观察者列表,并将注册的 ConcreteObserver 添加到列表中。
当 ConcreteSubject 的状态发生变化时,它会遍历观察者列表,并调用每个 ConcreteObserver 的 Update() 方法,通知观察者状态已改变。
每个 ConcreteObserver 在接收到通知后,会根据需要从 ConcreteSubject 获取状态信息,并执行相应的更新操作。
观察者模式在以下情况下非常适用:
当一个对象的改变需要同时改变其他对象,而不知道具体有多少对象需要改变时。 观察者模式允许动态地添加和移除观察者,使得主题无需关心具体的观察者数量和类型。例如,在游戏中,一个角色的属性变化可能需要同步到多个UI元素、特效系统、AI系统等。
当对象之间的依赖关系是单向的,一个对象的改变只影响其他对象,而反之不然时。 观察者模式实现了主题到观察者的单向通知,观察者无需知道主题的内部实现,降低了耦合度。例如,UI元素观察游戏状态,但游戏状态的改变不需要UI元素的干预。
当需要在不破坏封装性的前提下,使一个对象的状态变化通知多个其他对象时。 观察者模式通过接口定义了主题和观察者之间的协议,主题只需要调用观察者的 Update() 方法,而无需了解观察者的具体实现细节,保持了主题的封装性。
在实现事件处理系统时。 观察者模式可以作为事件处理的基础,主题可以看作是事件源,观察者可以看作是事件监听器。当事件发生时(主题状态改变),主题通知所有监听器(观察者)。Unity 的事件系统(例如 UnityEvent 和 C# 的 event 关键字)在一定程度上也体现了观察者模式的思想。
在 Unity3D 游戏中,观察者模式可以应用于以下场景:
UI 更新: 当游戏数据(如生命值、得分、金币数量等)发生变化时,需要更新 UI 界面上的显示。
游戏事件系统: 例如,当角色死亡、敌人被击败、游戏关卡完成等事件发生时,需要通知多个系统进行相应的处理(如播放音效、更新游戏状态、显示游戏结束界面等)。
属性变化监听: 监听游戏对象的属性变化,例如,当角色的移动速度、攻击力等属性发生变化时,需要触发相应的逻辑。
动画和特效触发: 当游戏状态或角色状态发生变化时,需要触发动画或特效的播放。
状态同步: 在多人游戏中,当服务器端的状态发生变化时,需要通知所有客户端进行同步更新。
在 Unity3D 中,我们可以使用 C# 的委托(delegate)和事件(event)来实现观察者模式。这种方式既简洁又高效,并且与 Unity 的事件系统能够很好地集成。
示例场景: 假设我们有一个简单的游戏场景,其中有一个玩家角色和一个生命值系统。当玩家受到攻击导致生命值减少时,我们需要更新 UI 上的生命值显示,并且可能还需要触发一些其他的游戏逻辑。
1. 定义观察者接口 (可选,但推荐):
虽然在 C# 中使用委托和事件可以不需要显式的观察者接口,但为了更好地体现观察者模式的结构,并提高代码的可读性和可维护性,我们可以定义一个观察者接口。
// 观察者接口 public interface IHealthObserver { void OnHealthChanged(int currentHealth, int maxHealth); }
2. 定义主题 (被观察者) - 生命值系统:
创建一个 HealthSystem 类作为主题,负责管理角色的生命值,并维护观察者列表(使用事件实现)。
using System; using UnityEngine; public class HealthSystem : MonoBehaviour { [SerializeField] private int maxHealth = 100; private int currentHealth; // 定义生命值改变事件 public event Action<int, int> OnHealthChangedEvent; // 使用 Action 委托,更简洁 public int CurrentHealth => currentHealth; public int MaxHealth => maxHealth; private void Awake() { currentHealth = maxHealth; } public void TakeDamage(int damageAmount) { currentHealth -= damageAmount; currentHealth = Mathf.Max(currentHealth, 0); // 确保生命值不低于 0 // 通知所有观察者 OnHealthChangedEvent?.Invoke(currentHealth, maxHealth); if (currentHealth <= 0) { Die(); } } private void Die() { Debug.Log("Player Died!"); // 在这里添加角色死亡逻辑,例如禁用角色控制,播放死亡动画等 } public void Heal(int healAmount) { currentHealth += healAmount; currentHealth = Mathf.Min(currentHealth, maxHealth); // 确保生命值不超过最大值 // 通知所有观察者 OnHealthChangedEvent?.Invoke(currentHealth, maxHealth); } }
代码解释:
HealthSystem 类继承自 MonoBehaviour,可以在 Unity 中挂载到游戏对象上。
maxHealth 和 currentHealth 字段分别表示最大生命值和当前生命值。
OnHealthChangedEvent 是一个 Action<int, int> 类型的事件,它充当了观察者列表。当生命值发生变化时,我们会通过 OnHealthChangedEvent?.Invoke(currentHealth, maxHealth) 来通知所有已订阅该事件的观察者。
TakeDamage(int damageAmount) 方法用于减少生命值,并在生命值减少后调用 OnHealthChangedEvent 通知观察者。
Heal(int healAmount) 方法用于增加生命值,并在生命值增加后调用 OnHealthChangedEvent 通知观察者。
Die() 方法处理角色死亡的逻辑(示例中只是简单地打印日志)。
3. 创建具体观察者 - UI 生命值显示:
创建一个 HealthUI 类作为具体的观察者,负责显示玩家的生命值。
using UnityEngine; using UnityEngine.UI; public class HealthUI : MonoBehaviour, IHealthObserver // 实现观察者接口 (可选) { [SerializeField] private HealthSystem playerHealthSystem; // 引用玩家的生命值系统 [SerializeField] private Text healthText; // 用于显示生命值的 Text 组件 private void Start() { if (playerHealthSystem != null && healthText != null) { // 订阅生命值改变事件 playerHealthSystem.OnHealthChangedEvent += OnHealthChanged; UpdateHealthText(); // 初始化显示 } else { Debug.LogError("HealthSystem or Health Text is not assigned!"); } } private void OnDestroy() { // 取消订阅事件,防止内存泄漏 if (playerHealthSystem != null) { playerHealthSystem.OnHealthChangedEvent -= OnHealthChanged; } } // 实现观察者接口的方法 (可选) public void OnHealthChanged(int currentHealth, int maxHealth) { UpdateHealthText(); } private void UpdateHealthText() { healthText.text = "Health: " + playerHealthSystem.CurrentHealth + "/" + playerHealthSystem.MaxHealth; } }
代码解释:
HealthUI 类继承自 MonoBehaviour 并实现了 IHealthObserver 接口(可选)。
playerHealthSystem 字段用于引用玩家的 HealthSystem 组件,需要在 Unity 编辑器中手动拖拽赋值。
healthText 字段用于引用 UI 上的 Text 组件,也需要在 Unity 编辑器中手动拖拽赋值。
在 Start() 方法中,HealthUI 向 playerHealthSystem 的 OnHealthChangedEvent 事件注册了 OnHealthChanged 方法,表示 HealthUI 成为 HealthSystem 的观察者。
OnDestroy() 方法中,取消了事件订阅,这是一个重要的步骤,用于防止内存泄漏。当 HealthUI 对象被销毁时,需要取消对 HealthSystem 事件的订阅。
OnHealthChanged(int currentHealth, int maxHealth) 方法是事件处理函数,当 HealthSystem 的生命值改变事件被触发时,该方法会被调用。在这个方法中,我们调用 UpdateHealthText() 来更新 UI 上的生命值显示。
UpdateHealthText() 方法负责更新 healthText 组件的文本内容,显示当前的生命值信息。
4. 在 Unity 编辑器中配置:
创建一个新的 Unity 场景。
创建一个玩家角色 GameObject,并添加 HealthSystem 组件。
创建一个 UI Text GameObject,用于显示生命值。
创建一个新的 GameObject,并添加 HealthUI 组件。
在 HealthUI 组件的 Inspector 面板中,将玩家角色 GameObject 拖拽到 Player Health System 字段,将 UI Text GameObject 拖拽到 Health Text 字段。
5. 测试代码:
你可以创建一个简单的脚本,例如 DamageDealer,挂载到场景中的另一个 GameObject 上,用于模拟对玩家造成伤害。
using UnityEngine; public class DamageDealer : MonoBehaviour { [SerializeField] private HealthSystem playerHealthSystem; [SerializeField] private int damageAmount = 20; private void Update() { if (Input.GetKeyDown(KeyCode.Space)) { if (playerHealthSystem != null) { playerHealthSystem.TakeDamage(damageAmount); } else { Debug.LogError("Player Health System is not assigned!"); } } } }
将 DamageDealer 脚本挂载到一个 GameObject 上,并将玩家角色 GameObject 拖拽到 Player Health System 字段。运行游戏,按下空格键,你会看到玩家的生命值减少,并且 UI 上的生命值显示也会同步更新。
使用 Mermaid 绘制的类图:
优点:
降低耦合度: 主题和观察者之间是抽象耦合,主题不需要知道观察者的具体类型和实现,只需要知道它们实现了观察者接口。这使得系统更加灵活,易于扩展和维护。
支持广播通信: 主题可以同时通知多个观察者,实现一对多的关联关系,方便构建事件驱动的系统。
符合开闭原则: 增加新的观察者无需修改主题的代码,符合开闭原则,易于扩展新的功能。
动态地建立观察关系: 可以在运行时动态地添加和移除观察者,提高了系统的灵活性。
缺点:
意外更新问题: 如果一个主题对象有很多观察者,那么通知所有观察者可能会花费较多的时间和资源,尤其是在观察者执行的操作比较耗时的情况下。此外,观察者之间可能存在循环依赖,导致意外的更新和错误。
观察者可能不知道主题发生了什么变化: 简单的观察者模式只通知观察者“状态已改变”,但没有提供具体的状态信息。观察者可能需要主动向主题请求更详细的信息,这可能会增加系统的复杂性。
内存泄漏风险: 如果观察者没有及时取消订阅主题的事件,可能会导致内存泄漏。尤其是在观察者的生命周期短于主题时,更容易出现这个问题。在 Unity 中,需要在 OnDestroy() 方法中取消事件订阅。
选择合适的通知机制: 可以使用委托和事件、接口回调、消息队列等方式实现观察者模式。在 C# 和 Unity 中,委托和事件是常用且高效的选择。
处理观察者的注册和取消注册: 提供清晰的注册和取消注册接口,确保观察者可以方便地订阅和取消订阅主题的通知。特别注意在观察者对象销毁时,及时取消订阅,防止内存泄漏。
考虑通知的效率: 如果观察者数量非常多,或者通知频率很高,需要考虑通知的效率。可以采用异步通知、批量通知、或者只通知必要的信息等方式来优化性能。
传递必要的状态信息: 在通知观察者时,尽量传递必要的状态信息,避免观察者需要额外向主题请求信息。可以使用事件参数来传递状态信息,例如在 OnHealthChangedEvent 事件中传递了 currentHealth 和 maxHealth。
避免循环依赖: 设计观察者关系时,要避免观察者之间形成循环依赖,导致无限循环更新。
适当使用接口: 定义观察者接口可以提高代码的可读性和可维护性,但也并非总是必要的。在简单的场景中,直接使用委托和事件可能更简洁。
Unity 特性结合: 在 Unity 中,可以结合 Unity 的特性,例如 SendMessage、BroadcastMessage 等消息传递机制,以及 UnityEvent 组件,来实现观察者模式或类似的事件驱动机制。但需要注意这些 Unity 内置方法的性能开销,以及类型安全问题。
观察者模式 vs. 发布-订阅模式 (Publish-Subscribe): 发布-订阅模式是观察者模式的更广义形式。观察者模式通常是主题直接通知观察者,而发布-订阅模式引入了消息队列或事件总线等中间组件,发布者发布消息到中间组件,订阅者从中间组件订阅消息。发布-订阅模式更加解耦,发布者和订阅者之间无需直接了解对方,但实现也更复杂。在 Unity 中,可以使用第三方插件或自行实现简单的消息总线来构建发布-订阅系统。
观察者模式 vs. 中介者模式 (Mediator): 观察者模式用于解耦主题和观察者,实现一对多的关联关系。中介者模式用于解耦多个对象之间的交互,将对象之间的复杂交互集中到一个中介者对象中。观察者模式关注的是状态变化通知,中介者模式关注的是对象之间的协作和控制。
观察者模式 vs. 策略模式 (Strategy): 观察者模式是一种行为型模式,用于定义对象之间的一对多依赖关系。策略模式也是一种行为型模式,用于定义一组算法,并将每个算法封装起来,使它们可以互相替换。观察者模式关注的是对象状态变化时的通知机制,策略模式关注的是算法的选择和切换。
观察者模式是一种非常有用的设计模式,在 Unity3D 游戏开发中有着广泛的应用。它可以有效地降低对象之间的耦合度,提高代码的可维护性和灵活性,并方便构建事件驱动的系统。通过合理地使用观察者模式,可以使我们的游戏代码更加清晰、易于扩展和测试。
在 Unity 中,利用 C# 的委托和事件来实现观察者模式是一种简单而有效的方法。我们需要理解观察者模式的核心思想,掌握其结构和参与者,并根据具体的应用场景选择合适的实现方式。同时,也要注意观察者模式的缺点和潜在的问题,例如内存泄漏和性能开销,并在实践中采取相应的措施进行优化和改进。
希望本章节的内容能够帮助你更好地理解和应用观察者模式,并在 Unity3D 游戏开发中发挥其优势。