观察者模式

在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. 场景配置步骤

  1. 创建空物体命名为Player,挂载PlayerCharacter脚本;
  2. 创建UI Canvas,添加Slider作为血条,命名为BloodSlider,挂载UIBloodObserver脚本,并将Slider拖入脚本的bloodSlider字段;
  3. 创建空物体命名为AudioManager,添加AudioSource组件,挂载AudioObserver脚本,拖入对应的音效Clip;
  4. 创建空物体命名为MissionManager,挂载MissionObserver脚本;
  5. 创建空物体命名为GameManager,挂载GameManager脚本,将上述PlayerUIBloodObserverAudioObserverMissionObserver拖入对应字段;
  6. 运行场景,控制台输出如下:
[玩家角色] 注册观察者: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场景)

  1. 解耦发布者与订阅者:玩家角色无需知道UI、音效、任务系统的存在,只需发布事件;观察者也无需知道事件的发布者,只需处理事件,模块间完全解耦;
  2. 灵活扩展:新增观察者(如“成就系统观察者”)时,无需修改玩家角色代码,只需实现IObserver并注册,符合“开闭原则”;
  3. 多对多通信:一个事件可被多个观察者处理(血量变化同时更新UI、播放音效、记录日志),一个观察者也可处理多个事件;
  4. 动态绑定/解绑:可在运行时动态注册/移除观察者(如暂停游戏时移除音效观察者,恢复时重新注册)。

六、观察者模式 vs 其他模式(Unity场景对比)

模式 核心差异 Unity适用场景
观察者模式 一对多事件通知,解耦通信 游戏事件系统(血量/金币/任务/成就)、UI交互、状态同步
责任链模式 请求沿链传递,单个处理者 审批流程、伤害计算链、输入事件处理
中介者模式 多对象交互通过中介封装 聊天室、多UI组件联动、游戏对象交互管理

观察者模式是Unity开发中使用最广泛的设计模式之一,几乎所有游戏的事件系统、UI交互、状态同步都会用到,是实现模块解耦的核心手段。