第4章 事件系统

事件系统是游戏框架的核心组件之一,它负责管理游戏中各种状态变化的通知机制。一个设计良好的事件系统能够降低模块间的耦合度,提高代码的可维护性和可扩展性。本章将详细介绍如何在 Godot C# 环境中构建一个类型安全、高效且易于使用的事件系统。

1. 设计原理与思路

事件驱动架构是一种以事件为核心的软件架构模式,它改变了传统游戏开发中直接调用的编程范式。

1.1 事件驱动架构的核心概念

事件驱动架构(Event-Driven Architecture,EDA)建立在发布-订阅模式(Publish-Subscribe Pattern)之上。在这种架构中,系统中的组件不直接调用彼此的方法,而是通过事件总线进行间接通信。这种设计带来了以下核心概念:

发布者(Publisher):生成事件的组件,它不知道谁会接收事件 订阅者(Subscriber):对特定事件感兴趣的组件,它不知道事件由谁发布 事件总线(Event Bus):负责事件路由的中心枢纽,解耦发布者和订阅者

┌─────────────────────────────────────────────────────────────────┐
│                    事件驱动架构示意图                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌───────────┐         ┌───────────┐         ┌───────────┐    │
│   │  发布者A  │────────▶│           │────────▶│  订阅者X  │    │
│   └───────────┘         │           │         └───────────┘    │
│                         │   事件总线  │                          │
│   ┌───────────┐         │           │         ┌───────────┐    │
│   │  发布者B  │────────▶│           │────────▶│  订阅者Y  │    │
│   └───────────┘         │           │         └───────────┘    │
│                         │           │                          │
│   ┌───────────┐         │           │         ┌───────────┐    │
│   │  发布者C  │────────▶│           │────────▶│  订阅者Z  │    │
│   └───────────┘         └───────────┘         └───────────┘    │
│                                                                 │
│   优势:                                                        │
│   - 发布者和订阅者完全解耦                                      │
│   - 可以轻松添加新的订阅者                                      │
│   - 一个事件可以被多个订阅者接收                                │
└─────────────────────────────────────────────────────────────────┘

1.2 为什么要解耦组件通信

在传统的游戏开发中,组件间的通信往往通过直接引用来实现:

// 传统方式:直接引用
public class PlayerController : Node
{
    private UIManager _uiManager;  // 直接依赖
    private AchievementSystem _achievementSystem;  // 直接依赖
    private SaveManager _saveManager;  // 直接依赖

    public override void _Ready()
    {
        _uiManager = GetNode<UIManager>("/root/UIManager");
        _achievementSystem = GetNode<AchievementSystem>("/root/AchievementSystem");
        _saveManager = GetNode<SaveManager>("/root/SaveManager");
    }

    public void TakeDamage(int damage)
    {
        _currentHealth -= damage;
        // 直接调用UI更新
        _uiManager.UpdateHealthBar(_currentHealth);
        // 直接调用成就检查
        _achievementSystem.CheckDamageTaken(damage);
        // 直接调用存档
        _saveManager.AutoSave();
    }
}

这种方式的问题在于:

  1. 耦合度高:PlayerController需要知道所有相关系统的存在
  2. 可测试性差:难以单独测试PlayerController,需要构造所有依赖
  3. 扩展困难:添加新系统需要修改PlayerController代码
  4. 灵活性低:无法在运行时动态添加或移除功能

通过事件系统解耦后,代码变得更加灵活:

// 事件驱动方式:通过事件总线通信
public class PlayerController : EventBusNode
{
    public void TakeDamage(int damage)
    {
        _currentHealth -= damage;
        // 发布事件,不关心谁接收
        Publish(new PlayerHealthChangedEvent(this, new PlayerHealthChangeData
        {
            CurrentHealth = _currentHealth,
            Delta = -damage
        }));
    }
}

1.3 事件总线与直接引用的对比

特性直接引用事件总线
耦合度高(编译时依赖)低(运行时依赖)
可测试性差(需要构造所有依赖)好(可以单独测试)
扩展性差(需要修改现有代码)好(添加订阅者即可)
性能高(直接调用)中等(委托调用开销)
代码清晰度直观需要理解事件流向
调试难度简单(调用栈清晰)较难(需要追踪事件)

为什么这样设计?

  • 泛型委托:使用泛型而非object类型,避免装箱拆箱,确保类型安全
  • 生命周期管理:通过EventBusNode自动处理订阅和取消订阅,防止内存泄漏
  • 线程安全:使用lock关键字保护事件字典,支持多线程环境
  • 延迟移除:在事件发布期间取消订阅时延迟处理,避免遍历时的集合修改异常

2. 使用场景分析

2.1 适用场景

模块间通信:当两个模块位于不同的命名空间或层级,且不需要强耦合时

// 场景:成就系统监听多个模块的事件
public class AchievementSystem : Node
{
    public override void _Ready()
    {
        // 订阅各种游戏事件
        EventBus.Instance.Subscribe<PlayerDeathEvent>(OnPlayerDeath);
        EventBus.Instance.Subscribe<QuestCompletedEvent>(OnQuestCompleted);
        EventBus.Instance.Subscribe<InventoryChangedEvent>(OnInventoryChanged);
    }
}

UI更新:当游戏状态变化需要更新多个UI元素时

游戏状态变更:关卡开始/结束、暂停/恢复等全局状态变化

跨系统通知:存档、音效、统计等辅助系统需要响应游戏事件

2.2 不适用场景

高频实时通信:如每帧更新的物理计算、动画插值等

// 不推荐:高频事件
public override void _PhysicsProcess(double delta)
{
    // 每帧都发布事件会造成性能问题
    EventBus.Instance.Publish(new PositionChangedEvent(this, transform.Position));
}

// 推荐:直接调用
public override void _PhysicsProcess(double delta)
{
    // 直接更新位置
    _sprite.Position = transform.Position;
}

需要同步返回值的场景:事件系统是异步的,无法直接获取返回值

// 不推荐:尝试通过事件获取返回值
public int GetPlayerLevel()
{
    int level = 0;
    // 事件处理器无法返回数据给发布者
    EventBus.Instance.Publish(new QueryPlayerLevelEvent(this, (l) => level = l));
    return level; // 总是0,因为事件是异步处理的
}

2.3 典型案例

成就系统:当玩家完成特定条件时解锁成就 任务系统:任务进度更新时通知UI和奖励系统 UI事件:生命值变化时更新血条、技能冷却时更新图标

成就系统事件流:
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ 玩家击杀敌人  │────▶│ 事件总线      │────▶│ 成就系统     │
└──────────────┘     └──────────────┘     └──────────────┘
                            │
                            ▼
                     ┌──────────────┐
                     │ 统计系统      │
                     └──────────────┘
                            │
                            ▼
                     ┌──────────────┐
                     │ UI显示弹窗   │
                     └──────────────┘

3. 实现细节讲解

3.1 为什么使用泛型委托

我们使用Action<TEvent>而非Action<object>Action有两个主要原因:

类型安全:编译时检查事件类型匹配,避免运行时错误

// 使用泛型:编译时类型检查
public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
{
    // 如果传入的handler参数类型不匹配,编译器会报错
}

// 使用非泛型:运行时可能出错
public void Subscribe(Type eventType, Action<object> handler)
{
    // 运行时可能接收到错误类型的事件
    handler.Invoke(wrongTypeEvent); // 运行时异常
}

性能优化:避免装箱拆箱操作

// 值类型事件数据
public struct PlayerHealthChangeData
{
    public int CurrentHealth;
    public int MaxHealth;
    public int Delta;
}

// 使用泛型委托:无装箱
Action<PlayerHealthChangedEvent> handler = (evt) => { /* ... */ };

// 使用非泛型委托:需要装箱
Action<object> handler = (obj) =>
{
    var evt = (PlayerHealthChangedEvent)obj; // 拆箱
    /* ... */
};

3.2 事件过滤机制设计思路

事件过滤允许订阅者只接收感兴趣的事件子集:

// 实现事件过滤
public class EventFilter<TEvent> where TEvent : IGameEvent
{
    private readonly Func<TEvent, bool> _filter;
    private readonly Action<TEvent> _handler;

    public EventFilter(Func<TEvent, bool> filter, Action<TEvent> handler)
    {
        _filter = filter;
        _handler = handler;
    }

    public void Handle(TEvent evt)
    {
        if (_filter(evt))
        {
            _handler(evt);
        }
    }
}

// 使用示例:只接收特定来源的伤害事件
EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(
    new EventFilter<PlayerHealthChangedEvent>(
        evt => evt.Data.DamageSource?.Name == "BossEnemy",
        OnBossDamage
    ).Handle
);

3.3 内存泄漏防范策略

事件系统最常见的内存泄漏原因是忘记取消订阅:

// 错误示例:内存泄漏
public class UIController : Node
{
    public override void _Ready()
    {
        // 订阅事件
        EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(OnHealthChanged);
    }

    // 忘记在_ExitTree中取消订阅!
    // 当UIController被销毁时,事件总线仍然持有对它的引用
}

防范策略:

1. 使用EventBusNode基类:自动处理取消订阅

2. 使用弱引用

public class WeakEventHandler<TEvent> where TEvent : IGameEvent
{
    private readonly WeakReference _target;
    private readonly Action<TEvent> _handler;

    public void Invoke(TEvent evt)
    {
        if (_target.IsAlive)
        {
            _handler(evt);
        }
        else
        {
            // 目标已被GC,自动取消订阅
            EventBus.Instance.Unsubscribe(evt.GetType(), this);
        }
    }
}

3. 定期清理

public void CleanupDeadHandlers()
{
    foreach (var kvp in _handlers.ToList())
    {
        var handlers = kvp.Value.GetInvocationList();
        foreach (var handler in handlers)
        {
            var target = handler.Target;
            if (target is Node node && !IsInstanceValid(node))
            {
                // 节点已被销毁,移除处理器
                Delegate.Remove(kvp.Value, handler);
            }
        }
    }
}

4. 性能与权衡

4.1 事件分发的时间复杂度

事件系统的主要操作时间复杂度:

操作时间复杂度说明
订阅事件O(1)字典查找 + 委托组合
取消订阅O(n)需要在委托链中查找
发布事件O(1)字典查找 + 委托调用
获取订阅者数量O(n)遍历委托链
事件发布流程:
1. 根据事件类型查找处理器字典: O(1)
   _handlers.TryGetValue(typeof(TEvent), out Delegate handlers)

2. 调用处理器委托: O(1) ~ O(n)
   handlers.Invoke(eventData)  // 委托内部遍历所有订阅者

总复杂度:O(1) 平均,O(n) 最坏情况(n为订阅者数量)

4.2 订阅者过多时的性能影响

当订阅者数量过多时,需要考虑:

委托链遍历开销:每个订阅者都会增加调用开销

// 如果有100个订阅者
// 每次发布事件需要遍历100个处理器
// 即使99个处理器什么都不做,遍历开销仍然存在

内存占用:每个订阅都存储一个委托引用

// 假设每个委托引用约32字节
// 1000个订阅者 = 32KB内存
// 10000个订阅者 = 320KB内存

优化策略:

  • 按优先级分组,只在必要时调用低优先级处理器
  • 实现批量发布,一帧内多次相同事件合并处理
  • 定期清理无效的订阅者

4.3 与信号系统的性能对比

方面C# 事件/委托Godot 信号
调用开销较低(直接委托调用)较高(需要遍历信号连接)
内存占用较小(每个委托)较大(信号对象+连接列表)
编辑器支持完整(可视化连接)
动态连接完全支持支持但开销大
跨语言C# onlyC# + GDScript
类型安全完全类型安全需要运行时类型检查

5. 实战指南

5.1 逐步实施步骤

步骤1:定义事件类型

// 为游戏中的主要状态变化定义事件
public class PlayerLevelUpEvent : GameEvent<PlayerLevelUpData>
{
    public PlayerLevelUpEvent(object sender, PlayerLevelUpData data) : base(sender, data) { }
}

public struct PlayerLevelUpData
{
    public int OldLevel;
    public int NewLevel;
    public int ExperienceGained;
}

步骤2:创建订阅者基类

// 创建特定功能的订阅者基类
public abstract class AchievementSubscriber : EventBusNode
{
    protected abstract void OnPlayerLevelUp(PlayerLevelUpEvent evt);

    public override void _Ready()
    {
        base._Ready();
        Subscribe<PlayerLevelUpEvent>(OnPlayerLevelUp);
    }
}

步骤3:配置全局事件总线

// 在Autoload中初始化
public partial class GameBootstrap : Node
{
    public override void _Ready()
    {
        // 确保GlobalEventBus存在
        if (GlobalEventBus.Instance == null)
        {
            var bus = new GlobalEventBus();
            Engine.RegisterSingleton("GlobalEventBus", bus);
            GetTree().Root.AddChild(bus);
        }
    }
}

步骤4:迁移现有代码

// 原代码
public void LevelUp()
{
    _level++;
    _uiManager.UpdateLevel(_level);
    _achievementManager.CheckLevelAchievement(_level);
    _audioManager.PlaySound("levelup");
}

// 新代码
public void LevelUp()
{
    int oldLevel = _level;
    _level++;

    EventBus.Instance.Publish(new PlayerLevelUpEvent(this, new PlayerLevelUpData
    {
        OldLevel = oldLevel,
        NewLevel = _level,
        ExperienceGained = _currentExp
    }));
}

5.2 常见错误:忘记取消订阅

这是事件系统最常见的错误,会导致内存泄漏和崩溃。

问题代码

public class TemporaryPopup : Node
{
    public override void _Ready()
    {
        // 订阅全局事件
        EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(OnHealthChanged);

        // 5秒后自动关闭
        GetTree().CreateTimer(5.0).Timeout += QueueFree;
    }

    private void OnHealthChanged(PlayerHealthChangedEvent evt)
    {
        // 更新UI
    }

    // 忘记在销毁时取消订阅!
}

解决方案

public class TemporaryPopup : EventBusNode  // 使用EventBusNode
{
    public override void _Ready()
    {
        base._Ready();
        Subscribe<PlayerHealthChangedEvent>(OnHealthChanged);

        GetTree().CreateTimer(5.0).Timeout += QueueFree;
    }

    // EventBusNode会自动处理取消订阅
}

// 或者手动处理
public class TemporaryPopup : Node
{
    public override void _Ready()
    {
        EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(OnHealthChanged);
        GetTree().CreateTimer(5.0).Timeout += QueueFree;
    }

    public override void _ExitTree()
    {
        // 手动取消订阅
        EventBus.Instance.Unsubscribe<PlayerHealthChangedEvent>(OnHealthChanged);
        base._ExitTree();
    }
}

5.3 测试建议:如何测试事件系统

单元测试事件处理器

[Test]
public void TestHealthChangedEvent()
{
    // Arrange
    var player = new PlayerController();
    var healthUI = new HealthUIComponent();
    bool eventReceived = false;

    EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(evt =>
    {
        eventReceived = true;
        Assert.AreEqual(80, evt.Data.CurrentHealth);
    });

    // Act
    player.TakeDamage(20);

    // Assert
    Assert.IsTrue(eventReceived);
}

测试订阅/取消订阅

[Test]
public void TestUnsubscribe()
{
    int callCount = 0;
    Action<TestEvent> handler = evt => callCount++;

    // Subscribe and publish
    EventBus.Instance.Subscribe<TestEvent>(handler);
    EventBus.Instance.Publish(new TestEvent(this));
    Assert.AreEqual(1, callCount);

    // Unsubscribe and publish again
    EventBus.Instance.Unsubscribe<TestEvent>(handler);
    EventBus.Instance.Publish(new TestEvent(this));
    Assert.AreEqual(1, callCount); // Should not increase
}

集成测试

[Test]
public void TestEventFlow_PlayerDamageToUI()
{
    // Setup complete scene
    var player = new PlayerController();
    var healthUI = new HealthUI();

    AddChild(player);
    AddChild(healthUI);

    // Act
    player.TakeDamage(30);

    // Wait for event propagation
    yield return new WaitForSeconds(0.1f);

    // Assert
    Assert.AreEqual("70/100", healthUI.HealthLabel.Text);
}

6. 类型安全的事件总线实现

事件总线(Event Bus)是一种发布-订阅模式(Publish-Subscribe Pattern)的实现,它允许系统中的各个组件通过统一的事件通道进行通信,而无需直接引用彼此。

1.1 EventBus 设计

我们的事件总线设计基于以下核心原则:

  1. 类型安全:利用 C# 的泛型机制确保事件类型和处理器类型匹配
  2. 生命周期管理:自动处理订阅者的订阅和取消订阅,防止内存泄漏
  3. 线程安全:在必要时提供线程安全的事件分发机制
  4. 性能优化:避免装箱拆箱,使用强类型委托

首先,我们定义事件接口和基础实现:

// 文件路径: Scripts/Core/Events/IGameEvent.cs

using Godot;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 游戏事件基础接口
    /// 所有自定义事件都必须实现此接口
    /// </summary>
    public interface IGameEvent
    {
        /// <summary>
        /// 事件发送者,用于追踪事件来源
        /// </summary>
        object Sender { get; }

        /// <summary>
        /// 事件创建时间戳
        /// </summary>
        double Timestamp { get; }

        /// <summary>
        /// 是否阻止事件继续传播
        /// </summary>
        bool IsHandled { get; set; }
    }

    /// <summary>
    /// 带泛型参数的事件接口
    /// 用于传递具体的数据类型
    /// </summary>
    /// <typeparam name="T">事件数据类型</typeparam>
    public interface IGameEvent<T> : IGameEvent
    {
        /// <summary>
        /// 事件携带的数据
        /// </summary>
        T Data { get; }
    }
}
// 文件路径: Scripts/Core/Events/GameEvent.cs

using Godot;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 游戏事件基础实现
    /// 提供通用的事件属性和构造方法
    /// </summary>
    public abstract class GameEvent : IGameEvent
    {
        /// <summary>
        /// 事件发送者
        /// </summary>
        public object Sender { get; }

        /// <summary>
        /// 事件创建时间戳(使用 Godot 的时间系统)
        /// </summary>
        public double Timestamp { get; }

        /// <summary>
        /// 是否已处理(阻止冒泡)
        /// </summary>
        public bool IsHandled { get; set; }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="sender">事件发送者</param>
        protected GameEvent(object sender)
        {
            Sender = sender;
            // 使用 Godot 引擎的时间获取当前时间戳
            Timestamp = Time.GetTicksMsec() / 1000.0;
        }
    }

    /// <summary>
    /// 泛型游戏事件实现
    /// 可以携带具体的数据类型
    /// </summary>
    /// <typeparam name="T">事件数据类型</typeparam>
    public class GameEvent<T> : GameEvent, IGameEvent<T>
    {
        /// <summary>
        /// 事件数据
        /// </summary>
        public T Data { get; }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="sender">事件发送者</param>
        /// <param name="data">事件数据</param>
        public GameEvent(object sender, T data) : base(sender)
        {
            Data = data;
        }
    }
}

现在实现核心的事件总线类:

// 文件路径: Scripts/Core/Events/EventBus.cs

using System;
using System.Collections.Generic;
using Godot;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 类型安全的事件总线
    /// 管理所有事件的订阅、取消订阅和发布
    /// </summary>
    public class EventBus
    {
        // 单例实例,用于全局事件总线
        private static EventBus _instance;
        public static EventBus Instance => _instance ??= new EventBus();

        // 存储事件处理器,使用字典实现 O(1) 查找
        // 键:事件类型,值:该事件类型的处理器委托列表
        private readonly Dictionary<Type, Delegate> _handlers = new();

        // 用于线程安全的锁对象
        private readonly object _lock = new();

        // 标记是否正在发布事件,用于延迟移除
        private bool _isPublishing;

        // 待移除的处理器队列(在发布期间使用)
        private readonly Queue<(Type eventType, Delegate handler)> _pendingRemovals = new();

        /// <summary>
        /// 订阅指定类型的事件
        /// </summary>
        /// <typeparam name="TEvent">事件类型</typeparam>
        /// <param name="handler">事件处理器</param>
        public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            // 参数检查
            if (handler == null)
            {
                GD.PushError("[EventBus] 处理器不能为空");
                return;
            }

            lock (_lock)
            {
                Type eventType = typeof(TEvent);

                // 如果字典中已存在该事件类型的处理器,组合新的处理器
                if (_handlers.TryGetValue(eventType, out Delegate existingHandler))
                {
                    _handlers[eventType] = Delegate.Combine(existingHandler, handler);
                }
                else
                {
                    // 否则添加新的处理器
                    _handlers[eventType] = handler;
                }

                GD.Print($"[EventBus] 已订阅事件: {eventType.Name}");
            }
        }

        /// <summary>
        /// 取消订阅指定类型的事件
        /// </summary>
        /// <typeparam name="TEvent">事件类型</typeparam>
        /// <param name="handler">要移除的事件处理器</param>
        public void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            if (handler == null)
            {
                return;
            }

            lock (_lock)
            {
                Type eventType = typeof(TEvent);

                // 如果正在发布事件,延迟移除
                if (_isPublishing)
                {
                    _pendingRemovals.Enqueue((eventType, handler));
                    return;
                }

                PerformUnsubscribe(eventType, handler);
            }
        }

        /// <summary>
        /// 执行实际的取消订阅操作
        /// </summary>
        private void PerformUnsubscribe<TEvent>(Type eventType, Action<TEvent> handler) where TEvent : IGameEvent
        {
            if (_handlers.TryGetValue(eventType, out Delegate existingHandler))
            {
                // 移除指定处理器
                var newHandler = Delegate.Remove(existingHandler, handler);

                if (newHandler == null)
                {
                    // 如果没有处理器了,从字典中移除该事件类型
                    _handlers.Remove(eventType);
                }
                else
                {
                    _handlers[eventType] = newHandler;
                }

                GD.Print($"[EventBus] 已取消订阅事件: {eventType.Name}");
            }
        }

        /// <summary>
        /// 发布事件到所有订阅者
        /// </summary>
        /// <typeparam name="TEvent">事件类型</typeparam>
        /// <param name="eventData">事件数据</param>
        public void Publish<TEvent>(TEvent eventData) where TEvent : IGameEvent
        {
            if (eventData == null)
            {
                GD.PushError("[EventBus] 事件数据不能为空");
                return;
            }

            lock (_lock)
            {
                Type eventType = typeof(TEvent);

                // 检查是否有处理器订阅了该事件
                if (!_handlers.TryGetValue(eventType, out Delegate handlers))
                {
                    return; // 没有订阅者,直接返回
                }

                // 标记正在发布
                _isPublishing = true;

                try
                {
                    // 转换为具体类型并调用
                    if (handlers is Action<TEvent> typedHandler)
                    {
                        typedHandler.Invoke(eventData);
                    }
                    else
                    {
                        // 如果不是预期的类型,尝试动态调用
                        foreach (Delegate handler in handlers.GetInvocationList())
                        {
                            try
                            {
                                handler.DynamicInvoke(eventData);
                            }
                            catch (Exception ex)
                            {
                                GD.PushError($"[EventBus] 调用处理器时出错: {ex.Message}");
                            }
                        }
                    }
                }
                finally
                {
                    _isPublishing = false;

                    // 处理延迟移除的订阅
                    ProcessPendingRemovals();
                }
            }
        }

        /// <summary>
        /// 处理延迟移除的订阅
        /// </summary>
        private void ProcessPendingRemovals()
        {
            while (_pendingRemovals.Count > 0)
            {
                var (eventType, handler) = _pendingRemovals.Dequeue();

                // 使用反射调用泛型方法
                var method = typeof(EventBus).GetMethod(nameof(PerformUnsubscribe),
                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                var genericMethod = method.MakeGenericMethod(eventType);
                genericMethod.Invoke(this, new object[] { eventType, handler });
            }
        }

        /// <summary>
        /// 清除所有订阅
        /// </summary>
        public void ClearAllSubscriptions()
        {
            lock (_lock)
            {
                _handlers.Clear();
                _pendingRemovals.Clear();
                GD.Print("[EventBus] 已清除所有订阅");
            }
        }

        /// <summary>
        /// 获取指定事件的订阅者数量
        /// </summary>
        public int GetSubscriberCount<TEvent>() where TEvent : IGameEvent
        {
            lock (_lock)
            {
                if (_handlers.TryGetValue(typeof(TEvent), out Delegate handler))
                {
                    return handler.GetInvocationList().Length;
                }
                return 0;
            }
        }
    }
}

1.2 强类型事件参数

为了确保类型安全,我们应该为游戏中的不同系统定义具体的事件类型:

// 文件路径: Scripts/Events/GameplayEvents.cs

using GameFramework.Core.Events;
using Godot;

namespace GameFramework.Events
{
    // ==================== 玩家相关事件 ====================

    /// <summary>
    /// 玩家生命值变化数据
    /// </summary>
    public struct PlayerHealthChangeData
    {
        /// <summary>
        /// 当前生命值
        /// </summary>
        public int CurrentHealth;

        /// <summary>
        /// 最大生命值
        /// </summary>
        public int MaxHealth;

        /// <summary>
        /// 变化量(正数为治疗,负数为伤害)
        /// </summary>
        public int Delta;

        /// <summary>
        /// 伤害来源(如果是伤害)
        /// </summary>
        public Node DamageSource;
    }

    /// <summary>
    /// 玩家生命值变化事件
    /// </summary>
    public class PlayerHealthChangedEvent : GameEvent<PlayerHealthChangeData>
    {
        public PlayerHealthChangedEvent(object sender, PlayerHealthChangeData data)
            : base(sender, data) { }
    }

    /// <summary>
    /// 玩家死亡事件数据
    /// </summary>
    public struct PlayerDeathData
    {
        /// <summary>
        /// 死亡位置
        /// </summary>
        public Vector3 DeathPosition;

        /// <summary>
        /// 击杀者
        /// </summary>
        public Node Killer;

        /// <summary>
        /// 死亡原因
        /// </summary>
        public string DeathReason;
    }

    /// <summary>
    /// 玩家死亡事件
    /// </summary>
    public class PlayerDeathEvent : GameEvent<PlayerDeathData>
    {
        public PlayerDeathEvent(object sender, PlayerDeathData data)
            : base(sender, data) { }
    }

    // ==================== 游戏状态事件 ====================

    /// <summary>
    /// 游戏状态变化类型
    /// </summary>
    public enum GameStateChangeType
    {
        Started,    // 游戏开始
        Paused,     // 游戏暂停
        Resumed,    // 游戏恢复
        Ended,      // 游戏结束
        Restarted   // 游戏重新开始
    }

    /// <summary>
    /// 游戏状态变化数据
    /// </summary>
    public struct GameStateChangeData
    {
        public GameStateChangeType ChangeType;
        public string PreviousState;
        public string NewState;
    }

    /// <summary>
    /// 游戏状态变化事件
    /// </summary>
    public class GameStateChangedEvent : GameEvent<GameStateChangeData>
    {
        public GameStateChangedEvent(object sender, GameStateChangeData data)
            : base(sender, data) { }
    }

    // ==================== 物品/库存事件 ====================

    /// <summary>
    /// 物品变化类型
    /// </summary>
    public enum InventoryChangeType
    {
        Added,      // 添加物品
        Removed,    // 移除物品
        Used,       // 使用物品
        Equipped,   // 装备物品
        Unequipped  // 卸下物品
    }

    /// <summary>
    /// 库存变化数据
    /// </summary>
    public struct InventoryChangeData
    {
        public string ItemId;
        public int Quantity;
        public int SlotIndex;
        public InventoryChangeType ChangeType;
    }

    /// <summary>
    /// 库存变化事件
    /// </summary>
    public class InventoryChangedEvent : GameEvent<InventoryChangeData>
    {
        public InventoryChangedEvent(object sender, InventoryChangeData data)
            : base(sender, data) { }
    }

    // ==================== 任务/成就事件 ====================

    /// <summary>
    /// 任务进度数据
    /// </summary>
    public struct QuestProgressData
    {
        public string QuestId;
        public string ObjectiveId;
        public int CurrentProgress;
        public int TargetProgress;
        public bool IsCompleted;
    }

    /// <summary>
    /// 任务进度更新事件
    /// </summary>
    public class QuestProgressEvent : GameEvent<QuestProgressData>
    {
        public QuestProgressEvent(object sender, QuestProgressData data)
            : base(sender, data) { }
    }

    /// <summary>
    /// 成就解锁数据
    /// </summary>
    public struct AchievementData
    {
        public string AchievementId;
        public string AchievementName;
        public string Description;
        public Texture2D Icon;
    }

    /// <summary>
    /// 成就解锁事件
    /// </summary>
    public class AchievementUnlockedEvent : GameEvent<AchievementData>
    {
        public AchievementUnlockedEvent(object sender, AchievementData data)
            : base(sender, data) { }
    }
}

1.3 订阅者生命周期管理

在 Godot 中,节点的生命周期管理非常重要。我们需要确保当节点被销毁时,自动取消订阅事件,防止内存泄漏。

// 文件路径: Scripts/Core/Events/EventBusNode.cs

using Godot;
using System;
using System.Collections.Generic;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 提供事件总线功能的节点基类
    /// 继承此类的节点会自动管理事件订阅的生命周期
    /// </summary>
    public abstract partial class EventBusNode : Node
    {
        // 存储该节点的所有订阅,用于自动取消订阅
        private readonly List<SubscriptionInfo> _subscriptions = new();

        /// <summary>
        /// 订阅事件
        /// </summary>
        /// <typeparam name="TEvent">事件类型</typeparam>
        /// <param name="handler">处理器</param>
        protected void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            _subscriptions.Add(new SubscriptionInfo
            {
                EventType = typeof(TEvent),
                Handler = handler
            });

            EventBus.Instance.Subscribe(handler);
        }

        /// <summary>
        /// 取消订阅事件
        /// </summary>
        /// <typeparam name="TEvent">事件类型</typeparam>
        /// <param name="handler">处理器</param>
        protected void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            EventBus.Instance.Unsubscribe(handler);

            // 从本地列表中移除
            for (int i = _subscriptions.Count - 1; i >= 0; i--)
            {
                if (_subscriptions[i].Handler.Equals(handler))
                {
                    _subscriptions.RemoveAt(i);
                }
            }
        }

        /// <summary>
        /// 发布事件
        /// </summary>
        /// <typeparam name="TEvent">事件类型</typeparam>
        /// <param name="eventData">事件数据</param>
        protected void Publish<TEvent>(TEvent eventData) where TEvent : IGameEvent
        {
            EventBus.Instance.Publish(eventData);
        }

        /// <summary>
        /// 节点退出场景树时自动取消所有订阅
        /// </summary>
        public override void _ExitTree()
        {
            // 自动取消所有订阅
            foreach (var subscription in _subscriptions)
            {
                // 使用反射调用 Unsubscribe 方法
                var method = typeof(EventBus).GetMethod("Unsubscribe");
                var genericMethod = method.MakeGenericMethod(subscription.EventType);
                genericMethod.Invoke(EventBus.Instance, new object[] { subscription.Handler });
            }

            _subscriptions.Clear();
            GD.Print($"[{Name}] 已清理所有事件订阅");

            base._ExitTree();
        }

        /// <summary>
        /// 订阅信息存储结构
        /// </summary>
        private struct SubscriptionInfo
        {
            public Type EventType;
            public Delegate Handler;
        }
    }
}

使用示例:

// 文件路径: Scripts/Player/PlayerController.cs

using GameFramework.Core.Events;
using GameFramework.Events;
using Godot;

namespace GameFramework.Player
{
    /// <summary>
    /// 玩家控制器示例
    /// 展示如何使用 EventBusNode 基类
    /// </summary>
    public partial class PlayerController : EventBusNode
    {
        [Export] private int _maxHealth = 100;
        private int _currentHealth;

        public override void _Ready()
        {
            _currentHealth = _maxHealth;

            // 步骤1:订阅玩家受伤事件
            Subscribe<PlayerHealthChangedEvent>(OnHealthChanged);

            // 步骤2:订阅其他系统的事件
            Subscribe<GameStateChangedEvent>(OnGameStateChanged);

            GD.Print($"[{Name}] 玩家控制器初始化完成");
        }

        /// <summary>
        /// 处理生命值变化
        /// </summary>
        private void OnHealthChanged(PlayerHealthChangedEvent evt)
        {
            GD.Print($"[{Name}] 生命值变化: {evt.Data.Delta}, 当前: {evt.Data.CurrentHealth}");

            // 如果生命值为0,触发死亡事件
            if (evt.Data.CurrentHealth <= 0)
            {
                Publish(new PlayerDeathEvent(this, new PlayerDeathData
                {
                    DeathPosition = GlobalPosition,
                    DeathReason = "Health Depleted"
                }));
            }
        }

        /// <summary>
        /// 处理游戏状态变化
        /// </summary>
        private void OnGameStateChanged(GameStateChangedEvent evt)
        {
            // 游戏暂停时禁用输入处理
            if (evt.Data.ChangeType == GameStateChangeType.Paused)
            {
                SetProcessInput(false);
            }
            else if (evt.Data.ChangeType == GameStateChangeType.Resumed)
            {
                SetProcessInput(true);
            }
        }

        /// <summary>
        /// 受到伤害
        /// </summary>
        public void TakeDamage(int damage, Node source)
        {
            int previousHealth = _currentHealth;
            _currentHealth = Mathf.Max(0, _currentHealth - damage);

            // 发布生命值变化事件
            Publish(new PlayerHealthChangedEvent(this, new PlayerHealthChangeData
            {
                CurrentHealth = _currentHealth,
                MaxHealth = _maxHealth,
                Delta = _currentHealth - previousHealth,
                DamageSource = source
            }));
        }

        /// <summary>
        /// 手动取消订阅示例(通常不需要,_ExitTree 会自动处理)
        /// </summary>
        public void StopListeningToHealthChanges()
        {
            // 注意:如果手动取消订阅,_ExitTree 中不会再次尝试取消
            Unsubscribe<PlayerHealthChangedEvent>(OnHealthChanged);
        }
    }
}

2. Godot 信号与 C# 事件的选择策略

在 Godot 中,我们有两种主要的事件机制可用:Godot 信号(Signals)和 C# 事件/委托。理解何时使用哪种机制对于构建清晰的游戏架构至关重要。

2.1 何时使用 Godot 信号

Godot 信号适用于以下场景:

  1. 编辑器配置:需要在 Godot 编辑器中通过可视化界面连接信号
  2. 节点间直接通信:父子节点或兄弟节点之间的直接交互
  3. 内置节点事件:响应 Godot 内置节点的事件(如按钮点击、区域进入等)
  4. 场景独立事件:事件处理逻辑与特定场景紧密耦合
// 文件路径: Scripts/UI/MainMenuButton.cs

using Godot;

namespace GameFramework.UI
{
    /// <summary>
    /// 主菜单按钮示例
    /// 展示何时使用 Godot 信号
    /// </summary>
    public partial class MainMenuButton : Button
    {
        [Signal]
        public delegate void MenuSelectedEventHandler(string menuName);

        [Export] private string _menuName = "Main";

        public override void _Ready()
        {
            // 步骤1:连接内置 Pressed 信号
            // 这是 Godot 节点的事件,应该使用 Godot 信号
            Pressed += OnButtonPressed;
        }

        private void OnButtonPressed()
        {
            // 步骤2:发射自定义信号
            // 这个信号可以在编辑器中连接到其他节点
            EmitSignal(SignalName.MenuSelected, _menuName);

            GD.Print($"[{Name}] 菜单被选中: {_menuName}");
        }

        /// <summary>
        /// 鼠标悬停效果
        /// 使用 Godot 内置信号
        /// </summary>
        private void OnMouseEntered()
        {
            // 修改按钮样式
            Modulate = new Color(1.2f, 1.2f, 1.2f);
        }

        private void OnMouseExited()
        {
            Modulate = Colors.White;
        }
    }
}
// 文件路径: Scripts/Gameplay/TriggerZone.cs

using Godot;

namespace GameFramework.Gameplay
{
    /// <summary>
    /// 触发区域示例
    /// 展示物理/区域检测相关的 Godot 信号使用
    /// </summary>
    public partial class TriggerZone : Area3D
    {
        [Signal]
        public delegate void PlayerEnteredEventHandler(Node3D player);

        [Signal]
        public delegate void PlayerExitedEventHandler(Node3D player);

        public override void _Ready()
        {
            // 步骤1:连接 Area3D 的内置信号
            // 这些是 Godot 引擎提供的物理检测信号
            BodyEntered += OnBodyEntered;
            BodyExited += OnBodyExited;
        }

        private void OnBodyEntered(Node body)
        {
            // 步骤2:检查是否是玩家
            if (body is CharacterBody3D player && player.IsInGroup("Player"))
            {
                GD.Print($"[{Name}] 玩家进入触发区域");

                // 步骤3:发射自定义信号
                // 可以在编辑器中连接此信号到关卡设计相关逻辑
                EmitSignal(SignalName.PlayerEntered, player);
            }
        }

        private void OnBodyExited(Node body)
        {
            if (body is CharacterBody3D player && player.IsInGroup("Player"))
            {
                GD.Print($"[{Name}] 玩家离开触发区域");
                EmitSignal(SignalName.PlayerExited, player);
            }
        }
    }
}

2.2 何时使用 C# 事件

C# 事件/委托适用于以下场景:

  1. 全局/系统级事件:跨多个场景或系统的通信
  2. 数据变化通知:需要传递复杂数据的状态变化
  3. 解耦系统间通信:不相关的系统需要相互通知
  4. 动态订阅:需要在运行时动态添加/移除监听者
  5. 类型安全要求高:需要编译时类型检查
// 文件路径: Scripts/Systems/AchievementSystem.cs

using GameFramework.Core.Events;
using GameFramework.Events;
using Godot;
using System.Collections.Generic;

namespace GameFramework.Systems
{
    /// <summary>
    /// 成就系统示例
    /// 展示何时使用 C# 事件系统
    /// </summary>
    public partial class AchievementSystem : Node
    {
        // 成就数据库
        private Dictionary<string, AchievementDefinition> _achievements = new();

        // 解锁的成就列表
        private HashSet<string> _unlockedAchievements = new();

        public override void _Ready()
        {
            // 步骤1:订阅全局事件
            // 成就系统需要监听多个不相关系统的事件
            EventBus.Instance.Subscribe<PlayerDeathEvent>(OnPlayerDeath);
            EventBus.Instance.Subscribe<QuestProgressEvent>(OnQuestProgress);
            EventBus.Instance.Subscribe<InventoryChangedEvent>(OnInventoryChanged);

            InitializeAchievements();
        }

        /// <summary>
        /// 初始化成就定义
        /// </summary>
        private void InitializeAchievements()
        {
            _achievements["first_death"] = new AchievementDefinition
            {
                Id = "first_death",
                Name = "初次阵亡",
                Description = "在游戏中第一次死亡",
                Condition = (data) => true
            };

            _achievements["collector"] = new AchievementDefinition
            {
                Id = "collector",
                Name = "收藏家",
                Description = "收集100个物品",
                Condition = (data) => GetTotalItemsCollected() >= 100
            };

            // 更多成就定义...
        }

        /// <summary>
        /// 处理玩家死亡事件
        /// 这是一个全局事件,来自玩家系统
        /// </summary>
        private void OnPlayerDeath(PlayerDeathEvent evt)
        {
            TryUnlockAchievement("first_death");
        }

        /// <summary>
        /// 处理任务进度事件
        /// 来自任务系统
        /// </summary>
        private void OnQuestProgress(QuestProgressEvent evt)
        {
            if (evt.Data.IsCompleted)
            {
                TryUnlockAchievement($"quest_{evt.Data.QuestId}");
            }
        }

        /// <summary>
        /// 处理库存变化事件
        /// 来自物品系统
        /// </summary>
        private void OnInventoryChanged(InventoryChangedEvent evt)
        {
            if (evt.Data.ChangeType == InventoryChangeType.Added)
            {
                TryUnlockAchievement("collector");
            }
        }

        /// <summary>
        /// 尝试解锁成就
        /// </summary>
        private void TryUnlockAchievement(string achievementId)
        {
            // 检查是否已解锁
            if (_unlockedAchievements.Contains(achievementId))
            {
                return;
            }

            // 检查是否满足条件
            if (_achievements.TryGetValue(achievementId, out var definition))
            {
                if (definition.Condition(null))
                {
                    UnlockAchievement(definition);
                }
            }
        }

        /// <summary>
        /// 解锁成就并发布事件
        /// </summary>
        private void UnlockAchievement(AchievementDefinition definition)
        {
            _unlockedAchievements.Add(definition.Id);

            GD.Print($"[AchievementSystem] 解锁成就: {definition.Name}");

            // 发布成就解锁事件
            // 这将通知 UI 系统显示成就弹窗
            // 通知统计系统记录成就
            // 通知存档系统保存进度
            EventBus.Instance.Publish(new AchievementUnlockedEvent(this, new AchievementData
            {
                AchievementId = definition.Id,
                AchievementName = definition.Name,
                Description = definition.Description
            }));
        }

        private int GetTotalItemsCollected() => 0; // 实际实现从存档读取

        /// <summary>
        /// 成就定义结构
        /// </summary>
        private class AchievementDefinition
        {
            public string Id;
            public string Name;
            public string Description;
            public System.Func<object, bool> Condition;
        }
    }
}

2.3 混合使用策略

在实际项目中,我们需要同时使用两种机制。以下是推荐的分层策略:

// 文件路径: Scripts/Architecture/EventStrategyGuide.md
/*
事件使用策略指南:

┌─────────────────────────────────────────────────────────────┐
│                     系统分层架构                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐                                           │
│  │   UI 层      │  ← 使用 Godot 信号                        │
│  │ (按钮、界面) │    - 按钮点击                              │
│  └──────┬───────┘    - 界面切换                             │
│         │                                                   │
│  ┌──────▼───────┐                                           │
│  │  游戏逻辑层   │  ← 使用 C# 事件总线                       │
│  │(玩家、敌人)  │    - 状态变化                              │
│  └──────┬───────┘    - 数据更新                             │
│         │                                                   │
│  ┌──────▼───────┐                                           │
│  │   系统层     │  ← 混合使用                                │
│  │(存档、音效)  │    - Godot 信号用于引擎交互                │
│  └──────────────┘    - C# 事件用于跨系统通信                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

具体规则:
1. 同一节点树内的通信 → Godot 信号
2. 跨系统的全局通信 → C# 事件总线
3. 与 Godot 引擎内置节点交互 → Godot 信号
4. 需要传递复杂数据的事件 → C# 事件
5. 需要在编辑器中配置的事件 → Godot 信号
*/
// 文件路径: Scripts/Player/PlayerHealthUI.cs

using GameFramework.Core.Events;
using GameFramework.Events;
using Godot;

namespace GameFramework.Player
{
    /// <summary>
    /// 玩家生命值 UI 组件
    /// 展示混合使用策略:
    /// - 使用 C# 事件监听全局生命值变化
    /// - 使用 Godot 信号更新 UI 节点
    /// </summary>
    public partial class PlayerHealthUI : Control
    {
        [Signal]
        public delegate void HealthCriticalEventHandler();

        [Export] private ProgressBar _healthBar;
        [Export] private Label _healthLabel;
        [Export] private AnimationPlayer _animationPlayer;

        // 生命值警告阈值
        private const float CriticalHealthThreshold = 0.2f;

        public override void _Ready()
        {
            // 步骤1:订阅全局 C# 事件
            // 这是跨系统通信,使用 EventBus
            EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(OnHealthChanged);

            // 步骤2:初始化 UI
            UpdateHealthDisplay(100, 100);
        }

        /// <summary>
        /// 处理生命值变化事件
        /// 来自 EventBus 的全局事件
        /// </summary>
        private void OnHealthChanged(PlayerHealthChangedEvent evt)
        {
            var data = evt.Data;

            // 步骤3:更新 UI
            UpdateHealthDisplay(data.CurrentHealth, data.MaxHealth);

            // 步骤4:检查是否处于危险状态
            float healthPercent = (float)data.CurrentHealth / data.MaxHealth;
            if (healthPercent <= CriticalHealthThreshold)
            {
                // 步骤5:发射 Godot 信号
                // 这个信号可以在编辑器中连接到其他 UI 效果
                EmitSignal(SignalName.HealthCritical);

                // 播放危险状态动画
                _animationPlayer?.Play("health_critical");
            }
        }

        /// <summary>
        /// 更新生命值显示
        /// </summary>
        private void UpdateHealthDisplay(int current, int max)
        {
            if (_healthBar != null)
            {
                _healthBar.MaxValue = max;
                _healthBar.Value = current;
            }

            if (_healthLabel != null)
            {
                _healthLabel.Text = $"{current} / {max}";
            }
        }

        /// <summary>
        /// Godot 信号回调示例
        /// 可以在编辑器中连接此函数
        /// </summary>
        public void OnHealthCriticalEffect()
        {
            // 屏幕红色闪烁效果
            GD.Print("[{Name}] 触发生命值危急效果");
        }

        public override void _ExitTree()
        {
            // 取消订阅 C# 事件
            EventBus.Instance.Unsubscribe<PlayerHealthChangedEvent>(OnHealthChanged);

            base._ExitTree();
        }
    }
}

3. 全局事件与局部事件分离

为了构建可维护的大型项目,我们需要明确区分全局事件和局部事件的作用域。

3.1 GlobalEventBus

全局事件总线用于跨场景、跨系统的通信。

// 文件路径: Scripts/Core/Events/GlobalEventBus.cs

using Godot;
using System;
using System.Collections.Generic;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 全局事件总线
    /// 用于跨场景、跨系统的全局事件通信
    /// 在游戏整个生命周期内保持存在
    /// </summary>
    public partial class GlobalEventBus : Node
    {
        // 单例实例
        private static GlobalEventBus _instance;
        public static GlobalEventBus Instance
        {
            get
            {
                if (_instance == null)
                {
                    GD.PushError("[GlobalEventBus] 实例尚未初始化。请确保在场景树中添加了 GlobalEventBus 节点");
                }
                return _instance;
            }
        }

        // 内部使用 EventBus 实现
        private readonly EventBus _eventBus = new();

        // 场景加载完成时重置标记
        private bool _shouldResetOnSceneChange = false;

        public override void _EnterTree()
        {
            // 步骤1:确保只有一个实例
            if (_instance != null)
            {
                GD.PushWarning("[GlobalEventBus] 存在多个实例,销毁重复实例");
                QueueFree();
                return;
            }

            _instance = this;

            // 步骤2:设置为持久节点(切换场景时不销毁)
            ProcessMode = ProcessModeEnum.Always;

            GD.Print("[GlobalEventBus] 全局事件总线已初始化");
        }

        /// <summary>
        /// 订阅全局事件
        /// </summary>
        public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            _eventBus.Subscribe(handler);
        }

        /// <summary>
        /// 取消订阅全局事件
        /// </summary>
        public void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            _eventBus.Unsubscribe(handler);
        }

        /// <summary>
        /// 发布全局事件
        /// </summary>
        public void Publish<TEvent>(TEvent eventData) where TEvent : IGameEvent
        {
            _eventBus.Publish(eventData);
        }

        /// <summary>
        /// 设置是否在场景切换时重置
        /// </summary>
        public void SetResetOnSceneChange(bool shouldReset)
        {
            _shouldResetOnSceneChange = shouldReset;
        }

        /// <summary>
        /// 清除所有订阅
        /// </summary>
        public void ClearAllSubscriptions()
        {
            _eventBus.ClearAllSubscriptions();
            GD.Print("[GlobalEventBus] 已清除所有订阅");
        }

        public override void _ExitTree()
        {
            if (_instance == this)
            {
                _instance = null;
            }

            base._ExitTree();
        }
    }
}

3.2 LocalEventBus

局部事件总线用于场景内部的模块间通信,随场景销毁而清理。

// 文件路径: Scripts/Core/Events/LocalEventBus.cs

using Godot;
using System;
using System.Collections.Generic;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 局部事件总线
    /// 用于单个场景内部的模块间通信
    /// 场景切换时自动清理
    /// </summary>
    public partial class LocalEventBus : Node
    {
        // 存储当前场景中所有的局部事件总线
        private static readonly Dictionary<string, LocalEventBus> _instances = new();

        /// <summary>
        /// 获取或创建指定作用域的局部事件总线
        /// </summary>
        /// <param name="scope">作用域名称</param>
        public static LocalEventBus GetOrCreate(string scope = "default")
        {
            if (_instances.TryGetValue(scope, out var existing))
            {
                return existing;
            }

            // 创建新的实例
            var instance = new LocalEventBus(scope);

            // 添加到场景树根节点
            var tree = Engine.GetMainLoop() as SceneTree;
            tree?.Root.CallDeferred("add_child", instance);

            return instance;
        }

        // 内部使用 EventBus 实现
        private readonly EventBus _eventBus = new();

        // 此总线的作用域
        public string Scope { get; private set; }

        // 构造函数
        private LocalEventBus(string scope)
        {
            Scope = scope;
            _instances[scope] = this;

            GD.Print($"[LocalEventBus] 创建新的局部事件总线: {scope}");
        }

        public override void _Ready()
        {
            Name = $"LocalEventBus_{Scope}";
        }

        /// <summary>
        /// 订阅局部事件
        /// </summary>
        public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            _eventBus.Subscribe(handler);
        }

        /// <summary>
        /// 取消订阅局部事件
        /// </summary>
        public void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : IGameEvent
        {
            _eventBus.Unsubscribe(handler);
        }

        /// <summary>
        /// 发布局部事件
        /// </summary>
        public void Publish<TEvent>(TEvent eventData) where TEvent : IGameEvent
        {
            _eventBus.Publish(eventData);
        }

        /// <summary>
        /// 获取订阅者数量
        /// </summary>
        public int GetSubscriberCount<TEvent>() where TEvent : IGameEvent
        {
            return _eventBus.GetSubscriberCount<TEvent>();
        }

        public override void _ExitTree()
        {
            // 从字典中移除
            _instances.Remove(Scope);

            // 清理所有订阅
            _eventBus.ClearAllSubscriptions();

            GD.Print($"[LocalEventBus] 销毁局部事件总线: {Scope}");

            base._ExitTree();
        }
    }
}

3.3 事件作用域管理

为了更好地管理事件作用域,我们提供一个管理器类:

// 文件路径: Scripts/Core/Events/EventScopeManager.cs

using Godot;
using System.Collections.Generic;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 事件作用域管理器
    /// 提供统一的事件作用域管理接口
    /// </summary>
    public static class EventScopeManager
    {
        // 记录当前激活的作用域
        private static readonly HashSet<string> _activeScopes = new();

        /// <summary>
        /// 创建新的局部事件作用域
        /// </summary>
        /// <param name="scopeName">作用域名称</param>
        public static LocalEventBus CreateScope(string scopeName)
        {
            if (_activeScopes.Contains(scopeName))
            {
                GD.PushWarning($"[EventScopeManager] 作用域 '{scopeName}' 已存在,返回现有实例");
                return LocalEventBus.GetOrCreate(scopeName);
            }

            _activeScopes.Add(scopeName);
            return LocalEventBus.GetOrCreate(scopeName);
        }

        /// <summary>
        /// 销毁指定作用域
        /// </summary>
        public static void DestroyScope(string scopeName)
        {
            if (!_activeScopes.Contains(scopeName))
            {
                return;
            }

            // 获取并销毁
            var bus = LocalEventBus.GetOrCreate(scopeName);
            bus.QueueFree();

            _activeScopes.Remove(scopeName);
        }

        /// <summary>
        /// 销毁所有局部作用域
        /// </summary>
        public static void DestroyAllScopes()
        {
            foreach (var scope in _activeScopes)
            {
                var bus = LocalEventBus.GetOrCreate(scope);
                bus.QueueFree();
            }

            _activeScopes.Clear();
            GD.Print("[EventScopeManager] 已销毁所有局部作用域");
        }

        /// <summary>
        /// 检查作用域是否存在
        /// </summary>
        public static bool ScopeExists(string scopeName)
        {
            return _activeScopes.Contains(scopeName);
        }

        /// <summary>
        /// 获取所有激活的作用域名称
        /// </summary>
        public static IEnumerable<string> GetActiveScopes()
        {
            return _activeScopes;
        }
    }
}

使用示例:

// 文件路径: Scripts/Levels/LevelManager.cs

using GameFramework.Core.Events;
using GameFramework.Events;
using Godot;

namespace GameFramework.Levels
{
    /// <summary>
    /// 关卡管理器示例
    /// 展示全局事件与局部事件的分离使用
    /// </summary>
    public partial class LevelManager : Node
    {
        // 局部事件总线引用
        private LocalEventBus _levelEventBus;

        [Export] private string _levelName = "Level_1";

        public override void _Ready()
        {
            // 步骤1:创建局部事件总线
            // 此总线只在本关卡内有效
            _levelEventBus = EventScopeManager.CreateScope($"Level_{_levelName}");

            // 步骤2:订阅局部事件
            // 这些事件只在当前关卡内传播
            _levelEventBus.Subscribe<PlayerHealthChangedEvent>(OnLocalPlayerHealthChanged);

            // 步骤3:订阅全局事件
            // 这些事件跨所有关卡传播
            GlobalEventBus.Instance.Subscribe<GameStateChangedEvent>(OnGlobalGameStateChanged);

            GD.Print($"[{Name}] 关卡管理器初始化,作用域: Level_{_levelName}");
        }

        /// <summary>
        /// 局部事件处理器
        /// 只响应当前关卡内的事件
        /// </summary>
        private void OnLocalPlayerHealthChanged(PlayerHealthChangedEvent evt)
        {
            // 只在当前关卡内处理生命值变化
            // 例如:触发关卡特定的陷阱、检查关卡完成条件等
            GD.Print($"[{Name}] 局部处理生命值变化: {evt.Data.CurrentHealth}");

            // 发布局部事件通知关卡内的其他系统
            _levelEventBus.Publish(new GameStateChangedEvent(this, new GameStateChangeData
            {
                ChangeType = GameStateChangeType.Started,
                NewState = "LevelSpecificState"
            }));
        }

        /// <summary>
        /// 全局事件处理器
        /// 响应跨关卡的全局事件
        /// </summary>
        private void OnGlobalGameStateChanged(GameStateChangedEvent evt)
        {
            // 处理全局游戏状态变化
            if (evt.Data.ChangeType == GameStateChangeType.Paused)
            {
                // 暂停关卡逻辑
                PauseLevel();
            }
            else if (evt.Data.ChangeType == GameStateChangeType.Resumed)
            {
                // 恢复关卡逻辑
                ResumeLevel();
            }
        }

        private void PauseLevel()
        {
            GetTree().Paused = true;
            GD.Print($"[{Name}] 关卡已暂停");
        }

        private void ResumeLevel()
        {
            GetTree().Paused = false;
            GD.Print($"[{Name}] 关卡已恢复");
        }

        /// <summary>
        /// 关卡完成时调用
        /// </summary>
        public void CompleteLevel()
        {
            // 发布全局事件通知游戏管理器
            GlobalEventBus.Instance.Publish(new GameStateChangedEvent(this, new GameStateChangeData
            {
                ChangeType = GameStateChangeType.Ended,
                PreviousState = _levelName,
                NewState = "LevelComplete"
            }));
        }

        public override void _ExitTree()
        {
            // 取消订阅全局事件
            GlobalEventBus.Instance.Unsubscribe<GameStateChangedEvent>(OnGlobalGameStateChanged);

            // 局部事件总线会在场景切换时自动销毁
            // 不需要手动处理

            base._ExitTree();
        }
    }
}

最佳实践总结

1. 事件命名规范

// 文件路径: Scripts/Core/Events/NamingConventions.md
/*
事件命名规范:

1. 事件类名:使用过去时态,以 "Event" 结尾
   - PlayerHealthChangedEvent (玩家生命值已改变)
   - ItemCollectedEvent (物品已收集)
   - GameStateChangedEvent (游戏状态已改变)

2. 事件数据类名:描述数据内容,以 "Data" 结尾
   - PlayerHealthChangeData
   - ItemCollectionData
   - GameStateChangeData

3. 处理器方法名:以 "On" 开头,描述事件
   - OnPlayerHealthChanged
   - OnItemCollected
   - OnGameStateChanged

4. 事件类型枚举:使用动作描述
   - Changed (已改变)
   - Started (已开始)
   - Ended (已结束)
   - Collected (已收集)
*/

2. 性能优化建议

// 文件路径: Scripts/Core/Events/PerformanceTips.md
/*
性能优化建议:

1. 避免频繁创建事件对象
   - 对于高频事件(如每帧更新),考虑使用直接调用而非事件系统
   - 重用事件对象或使用结构体作为事件数据

2. 及时取消订阅
   - 使用 EventBusNode 基类自动管理生命周期
   - 在 _ExitTree 中手动取消订阅

3. 使用对象池
   - 对于频繁触发的事件,实现事件对象池

4. 延迟处理
   - 对于非紧急事件,考虑在下一帧处理
   - 使用 Godot 的 CallDeferred 方法

5. 事件过滤
   - 在处理器中尽早检查条件,避免不必要处理
   - 使用 IsHandled 属性阻止事件传播
*/

3. 调试和监控

// 文件路径: Scripts/Core/Events/EventDebugger.cs

using Godot;
using System;
using System.Collections.Generic;

namespace GameFramework.Core.Events
{
    /// <summary>
    /// 事件调试器
    /// 用于监控事件系统的运行状态
    /// </summary>
    public partial class EventDebugger : Node
    {
        [Export] private bool _enableLogging = true;
        [Export] private bool _enableWarningOnUnsubscribedEvents = true;

        // 事件统计
        private Dictionary<string, int> _eventPublishCounts = new();
        private Dictionary<string, int> _eventHandlerCounts = new();

        public override void _Ready()
        {
            if (_enableLogging)
            {
                GD.Print("[EventDebugger] 事件调试器已启用");
            }
        }

        /// <summary>
        /// 记录事件发布
        /// </summary>
        public void LogEventPublish(string eventType)
        {
            if (!_enableLogging) return;

            if (!_eventPublishCounts.ContainsKey(eventType))
            {
                _eventPublishCounts[eventType] = 0;
            }
            _eventPublishCounts[eventType]++;

            // 如果某事件发布次数过多,发出警告
            if (_enableWarningOnUnsubscribedEvents && _eventPublishCounts[eventType] > 100)
            {
                if (!_eventHandlerCounts.ContainsKey(eventType) || _eventHandlerCounts[eventType] == 0)
                {
                    GD.PushWarning($"[EventDebugger] 事件 '{eventType}' 已发布 {_eventPublishCounts[eventType]} 次,但没有订阅者");
                }
            }
        }

        /// <summary>
        /// 记录处理器注册
        /// </summary>
        public void LogHandlerRegistered(string eventType)
        {
            if (!_eventHandlerCounts.ContainsKey(eventType))
            {
                _eventHandlerCounts[eventType] = 0;
            }
            _eventHandlerCounts[eventType]++;
        }

        /// <summary>
        /// 打印统计信息
        /// </summary>
        public void PrintStatistics()
        {
            GD.Print("========== 事件系统统计 ==========");
            GD.Print("事件发布统计:");
            foreach (var kvp in _eventPublishCounts)
            {
                GD.Print($"  {kvp.Key}: {kvp.Value} 次");
            }
            GD.Print("处理器注册统计:");
            foreach (var kvp in _eventHandlerCounts)
            {
                GD.Print($"  {kvp.Key}: {kvp.Value} 个处理器");
            }
            GD.Print("==================================");
        }
    }
}

本章介绍了事件系统的核心设计理念和实现方案。通过类型安全的事件总线、合理的事件作用域分离,以及 Godot 信号与 C# 事件的混合使用策略,我们可以构建出既灵活又易于维护的游戏架构。在实际项目中,请根据具体需求选择合适的通信机制,并始终关注事件订阅的生命周期管理,以避免内存泄漏问题。