一、观察者模式概述
意图:定义对象间的一种一对多依赖,当一个对象状态变化时,所有依赖它的对象都会得到通知并自动更新。
别名:发布–订阅(Publish–Subscribe)、事件(Event)模式。
1.1 角色
- Subject(被观察者):持有一组观察者引用,状态变化时负责通知它们。
- Observer(观察者):实现回调接口或注册到事件,当被观察者发出通知时执行响应逻辑。
1.2 UML 类图

二、Unity 中的观察者实现
2.1 基于 C# 事件
public class Health : MonoBehaviour {
public event Action<float> OnHealthChanged;
private float _current;
public float Current {
get => _current;
set {
_current = Mathf.Clamp(value, 0, Max);
OnHealthChanged?.Invoke(_current);
}
}
public float Max = 100f;
}
public class HealthBar : MonoBehaviour {
[SerializeField] private Health playerHealth;
[SerializeField] private Image fillImage;
void OnEnable() {
playerHealth.OnHealthChanged += HandleHealthChanged;
}
void OnDisable() {
playerHealth.OnHealthChanged -= HandleHealthChanged;
}
private void HandleHealthChanged(float newHealth) {
fillImage.fillAmount = newHealth / playerHealth.Max;
}
}
- 优点:语法简洁、性能开销低;
- 注意:订阅后务必在
OnDisable(或OnDestroy)时取消订阅,否则内存泄漏。
2.2 基于 UnityEvent
[Serializable]
public class FloatEvent : UnityEngine.Events.UnityEvent<float> { }
public class Health : MonoBehaviour {
public FloatEvent OnHealthChanged = new FloatEvent();
private float _current;
public float Current {
get => _current;
set {
_current = Mathf.Clamp(value, 0, Max);
OnHealthChanged.Invoke(_current);
}
}
public float Max = 100f;
}
- 优点:可在 Inspector 直接绑定监听器,利于策划可视化配置;
- 缺点:UnityEvent 对象分配带来少量 GC,需要留意高频调用场景。
2.3 事件总线(EventManager)
当场景中有大量事件类型和监听者时,可引入事件总线集中管理:
public static class EventBus {
private static readonly Dictionary<string, Action<object>> _eventTable
= new Dictionary<string, Action<object>>();
public static void Subscribe(string eventName, Action<object> handler) {
if (!_eventTable.ContainsKey(eventName))
_eventTable[eventName] = delegate { };
_eventTable[eventName] += handler;
}
public static void Unsubscribe(string eventName, Action<object> handler) {
if (_eventTable.ContainsKey(eventName))
_eventTable[eventName] -= handler;
}
public static void Publish(string eventName, object param = null) {
if (_eventTable.TryGetValue(eventName, out var handlers))
handlers.Invoke(param);
}
}
public class EnemySpawner : MonoBehaviour {
void Spawn() {
// ...
EventBus.Publish("EnemySpawned", newEnemy);
}
}
public class Minimap : MonoBehaviour {
void OnEnable() {
EventBus.Subscribe("EnemySpawned", OnEnemySpawned);
}
void OnDisable() {
EventBus.Unsubscribe("EnemySpawned", OnEnemySpawned);
}
private void OnEnemySpawned(object param) {
var enemy = param as Enemy;
// 在小地图上添加标记
}
}
- 优点:完全松耦合,发布者和订阅者互不依赖;
- 注意:字符串事件易出错,可用
enum或常量类统一管理;需要在退出时取消订阅,否则也会内存泄漏。
三、优化策略
3.1 事件参数封装
- 不建议直接传
object,可定义强类型事件数据类,再将Action<object>改为泛型Action<HealthChangedEvent>,提高类型安全:
public class HealthChangedEvent {
public readonly float NewHealth;
public HealthChangedEvent(float h) { NewHealth = h; }
}
3.2 事件分组与通道(Channel)
- 对于大规模项目,可按模块或场景划分通道,避免全局总线单点过于拥挤。
public static class UIEvents {
public static event Action OnMenuOpened;
}
public static class GameEvents {
public static event Action<int> OnScoreChanged;
}
3.3 异步与缓冲
- 若事件触发频率过高(如每帧多次发射),可缓冲或批量分发,降低性能开销。
private Queue<GameEvent> _queue = new Queue<GameEvent>();
void Update() {
while (_queue.Count > 0) {
var e = _queue.Dequeue();
EventBus.Publish(e.Name, e.Param);
}
}
public void PublishDeferred(string name, object param = null) {
_queue.Enqueue(new GameEvent(name, param));
}
3.4 内存泄漏防护
- 静态事件:订阅后若不取消,监听对象永远不会被 GC;
- 弱引用:高级场景可用
WeakReference存储订阅者,避免因忘记.Unsubscribe()而导致泄漏。
四、注意事项
| 场景 | 陷阱 | 建议 |
|---|---|---|
| 忘记取消订阅 | 导致对象无法回收 | 在 OnDisable / OnDestroy 必须取消所有订阅 |
| 字符串事件名拼写错误 | 无法触发或捕获事件 | 使用常量或 enum,或代码生成统一事件名 |
| 事件泛滥 | 监听者过多,性能下降 | 合理拆分通道,按需订阅;高频事件考虑批量/节流 |
| 直接依赖业务类型 | 发布–订阅失效松耦合 | 事件数据使用 DTO 或接口,不要传整个 MonoBehaviour |
| 跨线程触发 | UnityAPI 仅主线程可调用 | 确保事件在主线程分发,或用 UnityMainThreadDispatcher |
五、小结
- 首选 C# 事件/UnityEvent:语法最直观、性能开销小;
- 集中管理:对于复杂项目,使用
EventBus但需分通道、强类型; - 严格取消订阅:在生命周期钩子中解除绑定,防止内存泄漏;
- 分层发布:业务事件、UI 事件、系统事件各归一类,保持清晰边界;
- 性能监控:用 Profiler 跟踪
Invoke调用次数,确保事件系统稳定。