0x07. 观察者模式
观察者模式
在Unity中使用C#实现观察者模式(Observer Pattern),核心是定义「一对多」的依赖关系:当被观察者(主题)状态变化时,所有注册的观察者会自动收到通知并更新。结合游戏开发典型场景——游戏事件系统(如血量变化、任务完成、道具收集),能直观体现观察者模式“解耦事件发布者与订阅者”的核心特性。
一、核心思路(观察者模式)
| 角色 | 对应实现类 | 作用 |
|---|---|---|
| 抽象被观察者(主题) | IObservable(接口) |
定义注册/移除观察者、通知观察者的抽象方法 |
| 具体被观察者 | PlayerCharacter(玩家角色) |
维护状态(血量/金币),状态变化时主动通知所有观察者 |
| 抽象观察者 | IObserver(接口) |
定义接收通知的抽象方法(OnNotify) |
| 具体观察者 | UIBloodObserver/AudioObserver/MissionObserver |
实现观察者接口,接收通知后执行具体逻辑(更新UI、播放音效、完成任务) |
| 客户端 | GameManager(挂载到Unity物体) |
初始化被观察者和观察者,完成注册绑定 |
二、完整代码实现
1. 核心接口定义(抽象被观察者+抽象观察者)
1.1 事件类型枚举(统一事件标识)
/// <summary>
/// 游戏事件类型(被观察者通知的事件标识)
/// </summary>
public enum GameEventType
{
PlayerHpChanged, // 玩家血量变化
PlayerGoldChanged, // 玩家金币变化
MissionCompleted // 任务完成
}
/// <summary>
/// 事件数据(传递给观察者的具体信息)
/// </summary>
public class GameEventData
{
public GameEventType EventType; // 事件类型
public object Data; // 事件附加数据(如血量值、金币数)
public object Sender; // 事件发送者(被观察者)
}
1.2 抽象观察者接口:IObserver
/// <summary>
/// 抽象观察者接口:定义接收通知的方法
/// </summary>
public interface IObserver
{
/// <summary>
/// 接收被观察者的通知
/// </summary>
/// <param name="eventData">事件数据</param>
void OnNotify(GameEventData eventData);
}
1.3 抽象被观察者接口:IObservable
using System.Collections.Generic;
/// <summary>
/// 抽象被观察者接口:定义注册/移除/通知观察者的方法
/// </summary>
public interface IObservable
{
// 注册观察者
void AddObserver(IObserver observer);
// 移除观察者
void RemoveObserver(IObserver observer);
// 通知所有观察者
void NotifyObservers(GameEventData eventData);
}
2. 具体被观察者:PlayerCharacter(玩家角色)
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 具体被观察者:玩家角色(维护血量/金币状态,状态变化时通知观察者)
/// </summary>
public class PlayerCharacter : MonoBehaviour, IObservable
{
// 观察者列表
private readonly List<IObserver> _observers = new List<IObserver>();
// 玩家状态
private int _hp;
public int Hp
{
get => _hp;
set
{
if (_hp != value)
{
_hp = value;
// 血量变化,通知所有观察者
NotifyObservers(new GameEventData
{
EventType = GameEventType.PlayerHpChanged,
Data = _hp,
Sender = this
});
}
}
}
private int _gold;
public int Gold
{
get => _gold;
set
{
if (_gold != value)
{
_gold = value;
// 金币变化,通知所有观察者
NotifyObservers(new GameEventData
{
EventType = GameEventType.PlayerGoldChanged,
Data = _gold,
Sender = this
});
}
}
}
// 注册观察者
public void AddObserver(IObserver observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
Debug.Log($"[玩家角色] 注册观察者:{observer.GetType().Name}");
}
}
// 移除观察者
public void RemoveObserver(IObserver observer)
{
if (_observers.Contains(observer))
{
_observers.Remove(observer);
Debug.Log($"[玩家角色] 移除观察者:{observer.GetType().Name}");
}
}
// 通知所有观察者
public void NotifyObservers(GameEventData eventData)
{
Debug.Log($"\n[玩家角色] 触发事件:{eventData.EventType},开始通知观察者...");
foreach (var observer in _observers)
{
observer.OnNotify(eventData);
}
}
// 模拟玩家受击(血量减少)
public void TakeDamage(int damage)
{
Debug.Log($"\n[玩家角色] 受到{damage}点伤害!");
Hp -= damage;
}
// 模拟玩家拾取金币
public void CollectGold(int amount)
{
Debug.Log($"\n[玩家角色] 拾取{amount}枚金币!");
Gold += amount;
}
// 模拟任务完成
public void CompleteMission(string missionName)
{
Debug.Log($"\n[玩家角色] 完成任务:{missionName}!");
NotifyObservers(new GameEventData
{
EventType = GameEventType.MissionCompleted,
Data = missionName,
Sender = this
});
}
}
3. 具体观察者实现(UI/音效/任务系统)
3.1 UI血量观察者:UIBloodObserver
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 具体观察者1:UI血量观察者(接收血量变化通知,更新血条)
/// </summary>
public class UIBloodObserver : MonoBehaviour, IObserver
{
public Slider bloodSlider; // 拖入Unity血条Slider
private void Awake()
{
// 初始化血条最大值
if (bloodSlider != null)
{
bloodSlider.maxValue = 100;
bloodSlider.value = 100;
}
}
public void OnNotify(GameEventData eventData)
{
// 只处理血量变化事件
if (eventData.EventType == GameEventType.PlayerHpChanged)
{
int currentHp = (int)eventData.Data;
Debug.Log($"[UI血量观察者] 接收血量变化通知,更新血条为{currentHp}!");
if (bloodSlider != null)
{
bloodSlider.value = currentHp;
}
// 血量低于0时触发额外逻辑
if (currentHp <= 0)
{
Debug.Log("[UI血量观察者] 玩家血量为0,显示死亡界面!");
}
}
}
}
3.2 音效观察者:AudioObserver
using UnityEngine;
/// <summary>
/// 具体观察者2:音效观察者(接收事件通知,播放对应音效)
/// </summary>
public class AudioObserver : MonoBehaviour, IObserver
{
public AudioSource audioSource; // 拖入Unity音频源
public AudioClip hurtClip; // 受击音效
public AudioClip goldClip; // 拾取金币音效
public AudioClip missionClip; // 任务完成音效
public void OnNotify(GameEventData eventData)
{
if (audioSource == null) return;
// 根据不同事件播放对应音效
switch (eventData.EventType)
{
case GameEventType.PlayerHpChanged:
int hp = (int)eventData.Data;
if (hp < ((PlayerCharacter)eventData.Sender).Hp + 1) // 血量减少时播放受击音效
{
audioSource.PlayOneShot(hurtClip);
Debug.Log($"[音效观察者] 播放受击音效!");
}
break;
case GameEventType.PlayerGoldChanged:
audioSource.PlayOneShot(goldClip);
Debug.Log($"[音效观察者] 播放拾取金币音效!");
break;
case GameEventType.MissionCompleted:
audioSource.PlayOneShot(missionClip);
Debug.Log($"[音效观察者] 播放任务完成音效!");
break;
}
}
}
3.3 任务观察者:MissionObserver
using UnityEngine;
/// <summary>
/// 具体观察者3:任务观察者(接收任务完成通知,发放奖励)
/// </summary>
public class MissionObserver : MonoBehaviour, IObserver
{
private PlayerCharacter _player; // 玩家引用
private void Awake()
{
// 获取玩家角色组件
_player = FindObjectOfType<PlayerCharacter>();
}
public void OnNotify(GameEventData eventData)
{
// 只处理任务完成事件
if (eventData.EventType == GameEventType.MissionCompleted)
{
string missionName = (string)eventData.Data;
Debug.Log($"[任务观察者] 接收任务完成通知:{missionName}");
// 根据任务名称发放奖励
switch (missionName)
{
case "新手任务":
_player.Gold += 100; // 奖励100金币
_player.Hp = 100; // 满血
Debug.Log($"[任务观察者] 发放新手任务奖励:100金币 + 满血!");
break;
case "击杀BOSS":
_player.Gold += 500;
Debug.Log($"[任务观察者] 发放击杀BOSS奖励:500金币!");
break;
}
}
}
}
4. 客户端调用:GameManager(Unity场景挂载)
using UnityEngine;
/// <summary>
/// 游戏管理器(客户端):初始化观察者与被观察者的绑定,模拟游戏事件
/// </summary>
public class GameManager : MonoBehaviour
{
public PlayerCharacter player; // 拖入玩家角色物体
public UIBloodObserver uiBloodObserver; // 拖入UI血量观察者物体
public AudioObserver audioObserver; // 拖入音效观察者物体
public MissionObserver missionObserver; // 拖入任务观察者物体
private void Start()
{
// 初始化玩家初始状态
player.Hp = 100;
player.Gold = 0;
// 注册所有观察者到玩家(被观察者)
player.AddObserver(uiBloodObserver);
player.AddObserver(audioObserver);
player.AddObserver(missionObserver);
// 模拟游戏流程
SimulateGameProcess();
}
/// <summary>
/// 模拟游戏事件流程
/// </summary>
private void SimulateGameProcess()
{
// 1. 玩家受击
player.TakeDamage(20);
// 2. 玩家拾取金币
player.CollectGold(50);
// 3. 玩家完成新手任务
player.CompleteMission("新手任务");
// 4. 玩家再次受击
player.TakeDamage(30);
// 5. 移除UI血量观察者(后续血量变化不再更新UI)
player.RemoveObserver(uiBloodObserver);
// 6. 玩家再次受击(UI不再更新,音效仍会播放)
player.TakeDamage(40);
}
}
三、Unity场景配置与运行效果
1. 场景配置步骤
- 创建空物体命名为
Player,挂载PlayerCharacter脚本; - 创建UI Canvas,添加Slider作为血条,命名为
BloodSlider,挂载UIBloodObserver脚本,并将Slider拖入脚本的bloodSlider字段; - 创建空物体命名为
AudioManager,添加AudioSource组件,挂载AudioObserver脚本,拖入对应的音效Clip; - 创建空物体命名为
MissionManager,挂载MissionObserver脚本; - 创建空物体命名为
GameManager,挂载GameManager脚本,将上述Player、UIBloodObserver、AudioObserver、MissionObserver拖入对应字段; - 运行场景,控制台输出如下:
[玩家角色] 注册观察者:UIBloodObserver
[玩家角色] 注册观察者:AudioObserver
[玩家角色] 注册观察者:MissionObserver
[玩家角色] 受到20点伤害!
[玩家角色] 触发事件:PlayerHpChanged,开始通知观察者...
[UI血量观察者] 接收血量变化通知,更新血条为80!
[音效观察者] 播放受击音效!
[玩家角色] 拾取50枚金币!
[玩家角色] 触发事件:PlayerGoldChanged,开始通知观察者...
[音效观察者] 播放拾取金币音效!
[玩家角色] 完成任务:新手任务!
[玩家角色] 触发事件:MissionCompleted,开始通知观察者...
[任务观察者] 接收任务完成通知:新手任务
[任务观察者] 发放新手任务奖励:100金币 + 满血!
[玩家角色] 触发事件:PlayerGoldChanged,开始通知观察者...
[音效观察者] 播放拾取金币音效!
[玩家角色] 触发事件:PlayerHpChanged,开始通知观察者...
[UI血量观察者] 接收血量变化通知,更新血条为100!
[玩家角色] 受到30点伤害!
[玩家角色] 触发事件:PlayerHpChanged,开始通知观察者...
[UI血量观察者] 接收血量变化通知,更新血条为70!
[音效观察者] 播放受击音效!
[玩家角色] 移除观察者:UIBloodObserver
[玩家角色] 受到40点伤害!
[玩家角色] 触发事件:PlayerHpChanged,开始通知观察者...
[音效观察者] 播放受击音效!
四、Unity场景扩展(贴合实际开发)
1. 通用事件中心(全局观察者模式)
实际项目中常用「全局事件中心」替代单个被观察者,实现跨模块解耦:
/// <summary>
/// 全局事件中心(单例):统一管理所有事件的订阅/发布
/// </summary>
public class EventCenter : IObservable
{
// 单例实例
private static EventCenter _instance;
public static EventCenter Instance => _instance ?? (_instance = new EventCenter());
// 按事件类型分组存储观察者
private Dictionary<GameEventType, List<IObserver>> _eventObservers = new Dictionary<GameEventType, List<IObserver>>();
// 注册观察者(指定关注的事件类型)
public void AddObserver(GameEventType eventType, IObserver observer)
{
if (!_eventObservers.ContainsKey(eventType))
{
_eventObservers[eventType] = new List<IObserver>();
}
if (!_eventObservers[eventType].Contains(observer))
{
_eventObservers[eventType].Add(observer);
}
}
// 移除观察者
public void RemoveObserver(GameEventType eventType, IObserver observer)
{
if (_eventObservers.ContainsKey(eventType) && _eventObservers[eventType].Contains(observer))
{
_eventObservers[eventType].Remove(observer);
}
}
// 发布事件(通知对应类型的所有观察者)
public void NotifyObservers(GameEventData eventData)
{
if (_eventObservers.ContainsKey(eventData.EventType))
{
foreach (var observer in _eventObservers[eventData.EventType])
{
observer.OnNotify(eventData);
}
}
}
// 兼容原有接口(无事件类型的注册,默认关注所有事件)
public void AddObserver(IObserver observer)
{
foreach (var eventType in System.Enum.GetValues(typeof(GameEventType)))
{
AddObserver((GameEventType)eventType, observer);
}
}
public void RemoveObserver(IObserver observer)
{
foreach (var eventType in System.Enum.GetValues(typeof(GameEventType)))
{
RemoveObserver((GameEventType)eventType, observer);
}
}
}
// 玩家角色中使用全局事件中心发布事件
public class PlayerCharacter : MonoBehaviour
{
private void TakeDamage(int damage)
{
Hp -= damage;
// 发布事件到全局中心
EventCenter.Instance.NotifyObservers(new GameEventData
{
EventType = GameEventType.PlayerHpChanged,
Data = Hp,
Sender = this
});
}
}
// 观察者订阅指定事件
public class UIBloodObserver : MonoBehaviour, IObserver
{
private void Awake()
{
// 只订阅血量变化事件
EventCenter.Instance.AddObserver(GameEventType.PlayerHpChanged, this);
}
private void OnDestroy()
{
// 销毁时移除订阅,避免内存泄漏
EventCenter.Instance.RemoveObserver(GameEventType.PlayerHpChanged, this);
}
}
2. 避免空引用与内存泄漏
- 销毁时移除观察者:在观察者的
OnDestroy中移除订阅,防止空引用报错:private void OnDestroy() { if (player != null) { player.RemoveObserver(this); } } - 弱引用观察者:使用
WeakReference包装观察者,避免观察者销毁后仍被引用:private readonly List<WeakReference<IObserver>> _observers = new List<WeakReference<IObserver>>(); public void AddObserver(IObserver observer) { _observers.Add(new WeakReference<IObserver>(observer)); } public void NotifyObservers(GameEventData eventData) { // 清理无效的弱引用 _observers.RemoveAll(w => !w.TryGetTarget(out _)); foreach (var weakRef in _observers) { if (weakRef.TryGetTarget(out var observer)) { observer.OnNotify(eventData); } } }
3. 结合Unity事件(UnityEvent)
Unity内置的UnityEvent本质是观察者模式的封装,可结合使用简化UI交互:
using UnityEngine.Events;
public class PlayerCharacter : MonoBehaviour
{
// Unity事件(可视化配置观察者)
public UnityEvent<int> OnHpChanged;
public UnityEvent<int> OnGoldChanged;
private int _hp;
public int Hp
{
get => _hp;
set
{
_hp = value;
OnHpChanged?.Invoke(_hp); // 触发Unity事件
}
}
// 其他逻辑...
}
在Inspector面板中,可直接将UI血条的更新方法拖入OnHpChanged事件,无需手动注册观察者。
五、观察者模式核心优势(Unity场景)
- 解耦发布者与订阅者:玩家角色无需知道UI、音效、任务系统的存在,只需发布事件;观察者也无需知道事件的发布者,只需处理事件,模块间完全解耦;
- 灵活扩展:新增观察者(如“成就系统观察者”)时,无需修改玩家角色代码,只需实现
IObserver并注册,符合“开闭原则”; - 多对多通信:一个事件可被多个观察者处理(血量变化同时更新UI、播放音效、记录日志),一个观察者也可处理多个事件;
- 动态绑定/解绑:可在运行时动态注册/移除观察者(如暂停游戏时移除音效观察者,恢复时重新注册)。
六、观察者模式 vs 其他模式(Unity场景对比)
| 模式 | 核心差异 | Unity适用场景 |
|---|---|---|
| 观察者模式 | 一对多事件通知,解耦通信 | 游戏事件系统(血量/金币/任务/成就)、UI交互、状态同步 |
| 责任链模式 | 请求沿链传递,单个处理者 | 审批流程、伤害计算链、输入事件处理 |
| 中介者模式 | 多对象交互通过中介封装 | 聊天室、多UI组件联动、游戏对象交互管理 |
观察者模式是Unity开发中使用最广泛的设计模式之一,几乎所有游戏的事件系统、UI交互、状态同步都会用到,是实现模块解耦的核心手段。