观察者模式(Observer Pattern)是为了解决对象之间的“通信问题”。

在 Unity 中,观察者模式可以说是最重要、使用最频繁的设计模式,没有之一。它是帮你告别“意大利面条式代码”(也就是代码互相纠缠、耦合度极高)的绝对利器。

💡 为什么要用观察者模式?

假设你的玩家(Player)在游戏中被怪物攻击,扣了血,甚至死亡了。在这个瞬间,你需要做很多事情:

  1. UI 界面需要更新血条。
  2. 音效管理器需要播放受伤或死亡音效。
  3. 成就系统需要记录“玩家死亡100次”的成就。
  4. 游戏管理器需要弹出“Game Over”界面。

新手的灾难写法:
把所有引用都塞进 Player 脚本里。Player 受伤时,挨个调用 ui.UpdateHealth()audio.PlayDeath()... 这会导致 Player 类极其臃肿,而且只要 UI 或音效改了一点点代码,Player 脚本就会报错。

观察者模式的优雅解法:
让 Player 变成一个广播站(Subject/Publisher),它只负责大喊一声:“我受伤了,当前血量是X!”或者“我死了!”。
至于谁想听这个消息?Player 根本不关心。UI、音效、成就系统作为观察者(Observer/Subscriber),自己去订阅这个广播。


在 C# 中,我们通常不需要像传统的 GoF 书籍里那样去写繁琐的 IObserver 接口,因为 C# 原生提供了极其强大的委托(Delegate)事件(Event)

下面我为你演示两种在 Unity 中最实用的观察者模式写法。

一、 最经典的 C# Action 事件 (推荐写法)

这是日常开发中最常用的解耦方式,性能极高,代码简洁。

1. 广播站:玩家健康脚本 (PlayerHealth)

using System;
using UnityEngine;

public class PlayerHealth : MonoBehaviour
{
    public int currentHealth = 100;

    // 1. 定义事件(广播频道)
    // Action<int> 表示这个事件发生时,会顺便传递一个 int 类型的参数(比如当前血量)
    public event Action<int> OnHealthChanged; 
    // Action 不带参数,表示只通知事情发生了
    public event Action OnPlayerDied;       

    public void TakeDamage(int damage)
    {
        currentHealth -= damage;
        Debug.Log("玩家受到伤害,当前血量:" + currentHealth);

        // 2. 触发事件(开始广播!)
        // 使用 ?.Invoke() 是一种安全的写法,表示“如果有人订阅了这个事件,就通知他们”
        OnHealthChanged?.Invoke(currentHealth);

        if (currentHealth <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        Debug.Log("玩家死亡!");
        OnPlayerDied?.Invoke(); // 广播死亡事件
    }
}

2. 观察者:UI 管理器 (UIManager)

using UnityEngine;
using UnityEngine.UI;

public class UIManager : MonoBehaviour
{
    // 需要拿到玩家的引用,以便订阅它的事件
    public PlayerHealth player; 
    public Text healthText;

    // 当脚本启用时,订阅事件
    private void OnEnable()
    {
        if (player != null)
        {
            // 使用 += 来订阅事件
            player.OnHealthChanged += UpdateHealthUI;
            player.OnPlayerDied += ShowGameOverScreen;
        }
    }

    // 当脚本禁用或销毁时,必须取消订阅!(极其重要)
    private void OnDisable()
    {
        if (player != null)
        {
            // 使用 -= 来取消订阅
            player.OnHealthChanged -= UpdateHealthUI;
            player.OnPlayerDied -= ShowGameOverScreen;
        }
    }

    // 事件触发时要执行的具体方法
    private void UpdateHealthUI(int newHealth)
    {
        healthText.text = "血量: " + newHealth;
    }

    private void ShowGameOverScreen()
    {
        Debug.Log("UI:显示 Game Over 画面!");
    }
}

3. 另一个观察者:音效管理器 (AudioManager)

using UnityEngine;

public class AudioManager : MonoBehaviour
{
    public PlayerHealth player;

    private void OnEnable()
    {
        player.OnPlayerDied += PlayDeathSound;
    }

    private void OnDisable()
    {
        player.OnPlayerDied -= PlayDeathSound;
    }

    private void PlayDeathSound()
    {
        Debug.Log("音效:播放玩家惨叫声!");
    }
}

你看,通过这种方式,PlayerHealth 根本不知道 UIManagerAudioManager 的存在。你可以随时删掉音效管理器,或者新增一个成就管理器,玩家的代码一行都不用改!这就叫高内聚,低耦合


二、 适合策划/美术配置的:UnityEvent

如果你希望让不懂代码的策划或美术也能在 Inspector 面板中连线配置事件,你可以使用 UnityEngine.Events.UnityEvent。这在 Unity 官方的 UI 系统(比如 Button 的 OnClick)中被广泛使用。

using UnityEngine;
using UnityEngine.Events; // 引入命名空间

public class Door : MonoBehaviour
{
    // 定义一个可以在 Inspector 面板中显示的事件
    public UnityEvent OnDoorOpened;

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            Debug.Log("门打开了!");
            
            // 触发事件
            OnDoorOpened?.Invoke();
        }
    }
}

如何使用?
写完这段代码后,回到 Unity 编辑器点击这扇门,你会发现它的 Inspector 面板上多了一个 On Door Opened () 的列表。你可以点击 + 号,把播放开门音效的方法、增加玩家经验值的方法直接拖拽上去,完全不需要写额外的订阅代码!


🚨 避坑指南(致命错误警告!)

作为新手,在使用 C# 的 Actiondelegate 时,极其容易犯一个导致游戏卡死或内存泄漏的错误:忘记取消订阅

如果你在 OnEnableStart 里写了 +=必须、一定、绝对要在 OnDisableOnDestroy 里写对应的 -=

为什么?
假设一个敌人订阅了全局的 GameManager.OnDayNightChanged(昼夜交替事件)。玩家把这个敌人杀死了,敌人对应的 GameObject 被 Destroy 了。
但如果这个敌人没有执行 -=GameManager 仍然保留着对它的记忆。当昼夜交替时,GameManager 试图通知一个已经被销毁的敌人,Unity 就会瞬间抛出可怕的 NullReferenceException(空引用异常),严重时会导致整个游戏逻辑中断。

总结一下:有加必有减,成对出现保安宁。 只要记住这一点,观察者模式将成为你在 Unity 开发中最得心应手的武器!