LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(6):观察者模式(Observer/Event)


一、观察者模式概述

意图:定义对象间的一种一对多依赖,当一个对象状态变化时,所有依赖它的对象都会得到通知并自动更新。
别名:发布–订阅(Publish–Subscribe)、事件(Event)模式。

1.1 角色

  • Subject(被观察者):持有一组观察者引用,状态变化时负责通知它们。
  • Observer(观察者):实现回调接口或注册到事件,当被观察者发出通知时执行响应逻辑。

1.2 UML 类图

ObserverPattern

二、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

五、小结

  1. 首选 C# 事件/UnityEvent:语法最直观、性能开销小;
  2. 集中管理:对于复杂项目,使用 EventBus 但需分通道、强类型;
  3. 严格取消订阅:在生命周期钩子中解除绑定,防止内存泄漏;
  4. 分层发布:业务事件、UI 事件、系统事件各归一类,保持清晰边界;
  5. 性能监控:用 Profiler 跟踪 Invoke 调用次数,确保事件系统稳定。