观察者模式(Observer Pattern)就是专门用来解决“对象之间怎么高效通信”的绝对王者。

在游戏开发中,它还有另外几个大家更熟悉的名字:事件驱动(Event-Driven)发布-订阅模式(Pub/Sub)。它是实现模块之间“解耦(Decoupling)”的最强武器。

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

假设你正在开发一款动作游戏,当玩家角色死亡时,游戏里需要发生一连串的事情:

  1. UIManager:弹出“胜败乃兵家常事”的死亡界面。
  2. AudioManager:停止激烈的战斗 BGM,播放悲伤的死亡音效。
  3. AchievementManager:记录死亡次数,看看是否能解锁“受苦之魂”成就。
  4. EnemyManager:通知所有敌人停止攻击并原地嘲讽。

新手的灾难写法(极度耦合):

public class PlayerHealth : MonoBehaviour
{
    // 玩家必须把所有相关的系统都认识一遍!
    public UIManager uiManager;
    public AudioManager audioManager;
    public AchievementManager achievementManager;
    public EnemyManager enemyManager;

    public void Die()
    {
        uiManager.ShowGameOver();
        audioManager.PlaySadMusic();
        achievementManager.AddDeathCount();
        enemyManager.StopAllEnemies();
    }
}

这太糟糕了! 玩家的血量脚本凭什么要懂 UI 和音效怎么运作?如果有一天策划要求删掉成就系统,你还得跑回 PlayerHealth 里面去删代码,稍微删错一点就会引发空指针报错。

观察者模式的优雅解法:
PlayerHealth 变成一个只管大喊大叫的“广播电台(发布者)”。它死亡时,只需要向全服广播一句:“我死了!”。
至于谁想听(订阅者/观察者)?玩家根本不在乎。UI 和音效系统只需要默默收听这个频道,听到“玩家死亡”的广播后,各自执行自己的逻辑即可。


一、 C# 现代标准写法:使用 Action 事件

在传统的 Java 或 C++ 中,观察者模式需要你自己写 List<IObserver> 来管理订阅者。但在 C# 中,微软把观察者模式直接做进了语言底层,也就是委托(Delegate)和事件(Event)

我们现在最常用的是 System.Action

1. 定义发布者(被观察的对象:玩家)

using System;
using UnityEngine;

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

    // 1. 核心:定义一个事件(广播频道)
    // static 意味着这是一个全局频道,任何人都可以方便地收听
    public static event Action OnPlayerDeath; 

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        Debug.Log("玩家:啊,我死了!(开始广播)");
        
        // 2. 触发事件:向所有订阅者广播!
        // ?. 的作用是:如果没有人订阅(频道空无一人),就不发送,防止报错
        OnPlayerDeath?.Invoke(); 
        
        Destroy(gameObject);
    }
}

看!玩家的代码变得极其干净,它不再包含任何 UI 或音效的引用。

2. 定义订阅者(观察者:UI 和 音效)

using UnityEngine;

public class UIManager : MonoBehaviour
{
    // 在对象启用时,订阅(收听)事件
    private void OnEnable()
    {
        PlayerHealth.OnPlayerDeath += ShowGameOverScreen;
    }

    // 在对象禁用时,必须取消订阅(退订)!
    private void OnDisable()
    {
        PlayerHealth.OnPlayerDeath -= ShowGameOverScreen;
    }

    // 听到广播后实际要执行的动作
    private void ShowGameOverScreen()
    {
        Debug.Log("UI系统:收到死亡广播,正在弹出 Game Over 面板...");
    }
}

public class AudioManager : MonoBehaviour
{
    private void OnEnable()
    {
        PlayerHealth.OnPlayerDeath += PlaySadMusic;
    }

    private void OnDisable()
    {
        PlayerHealth.OnPlayerDeath -= PlaySadMusic;
    }

    private void PlaySadMusic()
    {
        Debug.Log("音效系统:收到死亡广播,正在播放悲伤的二胡 BGM...");
    }
}


二、 🌟 进阶:带参数的广播电台

很多时候,光喊一句“有事发生了”不够,我们还需要传递数据。比如玩家受击时,UI 需要知道扣了多少血才能更新血条。

这时可以使用 Action<T> 来传递参数。

发布者(广播带有数值):

// 广播频道:谁受伤了?扣了多少血?当前还剩多少血?
public static event Action<string, int, int> OnPlayerTakeDamage;

public void TakeDamage(int damage)
{
    health -= damage;
    // 广播:发送具体的数据给观察者
    OnPlayerTakeDamage?.Invoke("Player", damage, health);
}

订阅者(接收带数值的广播):

private void OnEnable()
{
    // 订阅的函数的参数列表,必须和 Action<string, int, int> 完全一致
    PlayerHealth.OnPlayerTakeDamage += UpdateHealthBar;
}

private void UpdateHealthBar(string targetName, int damageTaken, int currentHp)
{
    Debug.Log($"UI系统更新:{targetName} 扣除了 {damageTaken} 滴血,剩余血量 {currentHp}");
}


🚨 避坑指南:观察者模式的“致命诅咒” (内存泄漏)

这是全宇宙 Unity 新手在用观察者模式时最容易踩的终极天坑,请务必死死记住:

只要你 += 了,就必须在离开时 -=

  • 为什么? 如果你在 UIManager 里写了 PlayerHealth.OnPlayerDeath += ShowGameOver,这意味着 PlayerHealth 的事件列表里死死地抓住了 UIManager 的引用
    如果此时 UIManager 被销毁了(比如切换了场景),但你忘记了 -=。当玩家再次死亡触发事件时,事件系统会试图去寻找那个已经被销毁的 UIManager,这会直接导致 NullReferenceException(空指针异常),甚至造成严重的内存泄漏(GC 无法回收 UI 占用的内存,因为它还被事件抓着)。
  • 最佳实践: 永远成对使用 Unity 的生命周期函数:
  • OnEnable+= 订阅。
  • OnDisable-= 退订。
    (或者在 Start 中订阅,在 OnDestroy 中退订)。