【Unity3D脚本与系统设计4】事件分发器EventDispatcher

以下是一个基于 组件化设计 的 Unity EventDispatcher 实现方案,支持对象级事件管理和泛型参数传递:

EventDispatcher 完整实现代码

using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;

[DisallowMultipleComponent]
public class EventDispatcher : MonoBehaviour
{
    
    
    // 基础事件容器
    private Dictionary<string, UnityEvent> _eventTable = new Dictionary<string, UnityEvent>();
    
    // 泛型事件容器(支持带参数的事件)
    private Dictionary<string, System.Object> _genericEventTable = new Dictionary<string, System.Object>();

    //=== 基础事件方法 ===//
    
    /// <summary>
    /// 订阅无参数事件
    /// </summary>
    public void AddListener(string eventName, UnityAction handler)
    {
    
    
        if (string.IsNullOrEmpty(eventName))
        {
    
    
            Debug.LogError("事件名不能为空", this);
            return;
        }

        if (!_eventTable.TryGetValue(eventName, out UnityEvent unityEvent))
        {
    
    
            unityEvent = new UnityEvent();
            _eventTable.Add(eventName, unityEvent);
        }

        unityEvent.AddListener(handler);
    }

    /// <summary>
    /// 触发无参数事件
    /// </summary>
    public void Dispatch(string eventName)
    {
    
    
        if (_eventTable.TryGetValue(eventName, out UnityEvent unityEvent))
        {
    
    
            unityEvent.Invoke();
        }
        else
        {
    
    
            Debug.LogWarning($"未注册的事件: {
      
      eventName}", this);
        }
    }

    //=== 泛型事件方法 ===//
    
    /// <summary>
    /// 订阅带参数事件
    /// </summary>
    public void AddListener<T>(string eventName, UnityAction<T> handler)
    {
    
    
        if (!_genericEventTable.TryGetValue(eventName, out System.Object objEvent))
        {
    
    
            var genericEvent = new UnityEvent<T>();
            genericEvent.AddListener(handler);
            _genericEventTable.Add(eventName, genericEvent);
        }
        else
        {
    
    
            if (objEvent is UnityEvent<T> genericEvent)
            {
    
    
                genericEvent.AddListener(handler);
            }
            else
            {
    
    
                Debug.LogError($"事件类型不匹配: {
      
      eventName}", this);
            }
        }
    }

    /// <summary>
    /// 触发带参数事件
    /// </summary>
    public void Dispatch<T>(string eventName, T eventData)
    {
    
    
        if (_genericEventTable.TryGetValue(eventName, out System.Object objEvent))
        {
    
    
            if (objEvent is UnityEvent<T> genericEvent)
            {
    
    
                genericEvent.Invoke(eventData);
            }
            else
            {
    
    
                Debug.LogError($"事件参数类型不匹配: {
      
      eventName}", this);
            }
        }
        else
        {
    
    
            Debug.LogWarning($"未注册的泛型事件: {
      
      eventName}", this);
        }
    }

    //=== 清理方法 ===//
    
    /// <summary>
    /// 移除指定事件的所有监听
    /// </summary>
    public void RemoveEvent(string eventName)
    {
    
    
        _eventTable.Remove(eventName);
        _genericEventTable.Remove(eventName);
    }

    /// <summary>
    /// 清空所有事件监听
    /// </summary>
    public void ClearAllListeners()
    {
    
    
        _eventTable.Clear();
        _genericEventTable.Clear();
    }

    void OnDestroy()
    {
    
    
        ClearAllListeners(); // 对象销毁时自动清理
    }
}

1. 无参数事件(如按钮点击)

// 订阅方
public class UIButton : MonoBehaviour
{
    
    
    private EventDispatcher _dispatcher;

    void Start()
    {
    
    
        _dispatcher = GetComponent<EventDispatcher>();
        _dispatcher.AddListener("OnClick", HandleClick);
    }

    private void HandleClick()
    {
    
    
        Debug.Log("按钮被点击!");
    }
}

// 触发方
public class ButtonTrigger : MonoBehaviour
{
    
    
    public void OnInteract()
    {
    
    
        GetComponent<EventDispatcher>().Dispatch("OnClick");
    }
}

2. 带参数事件(如敌人受伤)

// 订阅方
public class EnemyHealth : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        GetComponent<EventDispatcher>().AddListener<float>("TakeDamage", OnDamage);
    }

    private void OnDamage(float damage)
    {
    
    
        Debug.Log($"受到伤害: {
      
      damage}");
    }
}

// 触发方
public class Weapon : MonoBehaviour
{
    
    
    public void Attack()
    {
    
    
        var dispatcher = target.GetComponent<EventDispatcher>();
        dispatcher.Dispatch("TakeDamage", 25.5f);
    }
}

高级功能扩展

方案1:多参数支持

// 添加多参数分发方法
public void Dispatch<T1, T2>(string eventName, T1 arg1, T2 arg2)
{
    
    
    if (_genericEventTable.TryGetValue(eventName, out System.Object objEvent))
    {
    
    
        if (objEvent is UnityEvent<T1, T2> genericEvent)
        {
    
    
            genericEvent.Invoke(arg1, arg2);
        }
    }
}

// 使用示例:触发位置和伤害事件
dispatcher.Dispatch("ProjectileHit", transform.position, 30f);

方案2:自动取消订阅装饰器

// 为 MonoBehaviour 添加扩展方法
public static class EventDispatcherExtensions
{
    
    
    public static void AutoUnsubscribe(this MonoBehaviour listener, 
                                     EventDispatcher dispatcher,
                                     string eventName,
                                     UnityAction handler)
    {
    
    
        dispatcher.AddListener(eventName, handler);
        listener.gameObject.AddComponent<AutoUnsubscriber>()
              .Setup(() => dispatcher.RemoveEvent(eventName));
    }
}

// 自动取消订阅组件
public class AutoUnsubscriber : MonoBehaviour
{
    
    
    private System.Action _unsubscribeAction;

    public void Setup(System.Action action) => _unsubscribeAction = action;
    
    void OnDestroy() => _unsubscribeAction?.Invoke();
}

方案3:可视化调试

[Header("Debug Settings")]
[SerializeField] private bool _showLogs = true;

public void Dispatch<T>(string eventName, T eventData)
{
    
    
    if (_showLogs)
        Debug.Log($"<color=green>[事件触发]</color> {
      
      eventName} : {
      
      eventData}", this);
    
    // ...原有触发逻辑...
}

最佳实践建议

层级化命名规范

使用分类前缀避免命名冲突:

// UI系统事件
dispatcher.Dispatch("UI.Menu.Open");

// 战斗系统事件
dispatcher.Dispatch("Combat.Enemy.Spawn", enemyData);

结合 ScriptableObject

创建可配置的事件资产:

[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    
    
    public UnityEvent OnRaised;
    
    public void Raise() => OnRaised?.Invoke();
}

// 使用方式
[SerializeField] private GameEvent _playerDeathEvent;
_playerDeathEvent.OnRaised += HandleDeath;

性能优化策略

对高频事件(如每帧触发)使用 事件节流 技术

private float _lastEventTime;

public void DispatchWithThrottle(string eventName, float interval = 0.1f)
{
    
    
    if (Time.time - _lastEventTime < interval) return;
    Dispatch(eventName);
    _lastEventTime = Time.time;
}

与 EventManager 的对比使用场景

场景 适用组件
跨场景全局事件 ✅ EventManager
玩家技能连招触发 ✅ EventDispatcher
背包物品拖拽交互 ✅ EventDispatcher
游戏暂停/继续 ✅ EventManager
NPC对话选项选择 ✅ EventDispatcher

通过这种组件化的 EventDispatcher 实现,可以实现细粒度的事件控制,特别适合以下场景:

  • UI系统:按钮点击、面板开关
  • 实体交互:角色受击、道具拾取
  • 动画事件:动画关键帧触发逻辑
  • 状态同步:多对象间的状态变更通知