事件总线
游戏系统之间类型安全的同步发布/订阅。
游戏系统之间类型安全的发布/订阅 · 同步、仅限主线程 ·
Subscribe返回IDisposable令牌 · 无操作替代:NullEventBus
功能概述
事件总线让各系统在互不知晓的情况下对话。发布者调用 bus.Publish(new PlayerDamagedEvent { ... });每个通过 bus.Subscribe<PlayerDamagedEvent>(handler) 注册的处理器都会同步收到它,顺序即各处理器订阅的顺序。事件类型在编译期匹配——没有反射、没有动态查找——因此该总线在 IL2CPP(AOT 编译)构建中也能安全工作。它是输入服务、场景转换、UI 面板栈与你自己的游戏代码之间的管道。
快速示例
定义一个事件类,在 OnEnable 中订阅,在 OnDisable 中退订:
using System;
using CommonGameSystem.Core;
using UnityEngine;
// Any plain class works as an event.
public class PlayerDamagedEvent
{
public int Amount;
}
[DefaultExecutionOrder(100)]
public class DamageUI : MonoBehaviour
{
private IEventBus _bus;
private IDisposable _subscription;
private void Awake()
{
_bus = ServiceLocator.Resolve<IEventBus>(); // Cache it once
}
private void OnEnable()
{
_subscription = _bus.Subscribe<PlayerDamagedEvent>(OnPlayerDamaged);
}
private void OnDisable()
{
_subscription?.Dispose();
}
private void OnPlayerDamaged(PlayerDamagedEvent ev)
{
Debug.Log($"Damage: {ev.Amount}");
// Update UI, play a sound, etc.
}
}
在主线程上的任何位置发布只需一行:
_bus.Publish(new PlayerDamagedEvent { Amount = 12 });
对于只应存活于一个作用域内的处理器,using 会自动释放令牌:
private void RunScopedListener()
{
using var token = _bus.Subscribe<PlayerDamagedEvent>(ev => Debug.Log(ev.Amount));
// The handler is active here...
} // ...and unsubscribed automatically when the scope ends.
完整 API 一览
IEventBus 接口
-
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class为TEvent类型的事件注册一个处理器。返回一个令牌,其Dispose()用于退订——重复释放是安全的,第二次不做任何事。事件类型必须是引用类型(class或record)。处理器为 null 时抛出ArgumentNullException。 -
void Publish<TEvent>(TEvent ev) where TEvent : class同步调用为精确类型TEvent注册的每个处理器,按订阅顺序(先订阅者先被调用)。没有处理器时调用会静默返回。事件为 null 时抛出ArgumentNullException。处理器抛出的异常会被捕获并记录;其余处理器仍会执行。
EventBusOptions 调优(可选)
默认总线由启动引导以默认选项注册。如果你自行构造总线(用于测试,或一个隔离的子总线),可以对它调优:
var bus = new DefaultEventBus(
EventBusOptions.Default.With(
initialListCapacity: 8,
warnSubscribersPerType: 100));
WarnSubscribersPerType(默认 50):仅限 Editor 的阈值;当某个事件类型的订阅者数量超过该值时,为该类型记录一次警告。通常是订阅泄漏的征兆。WarnPublishDepth(默认 8):仅限 Editor 的阈值;当发布调用的嵌套深度超过该值时警告一次(一个处理器又发布另一个事件,依此类推)。发布调用栈完全退栈后,该警告会重新待命。InitialListCapacity(默认 4):内存调优——每个类型的订阅者列表的初始大小。
EventBusOptions 是不可变结构体:从 EventBusOptions.Default 出发,用 .With(...) 覆盖个别值。
EventBusDiagnostics(仅限 Editor)
仅在 Editor 构建中可用,用于检视总线状态:
#if UNITY_EDITOR
int count = EventBusDiagnostics.SubscribeCount<MyEvent>(_bus);
int depth = EventBusDiagnostics.PeekPublishDepth(_bus);
long warnings = EventBusDiagnostics.WarningsEmitted;
#endif
关闭此服务
ServiceLocator.Replace<IEventBus>(new NullEventBus());
NullEventBus 静默丢弃每次发布,并从 Subscribe 返回无害的无操作令牌。用它在测试或特殊构建中禁用事件路由,而无需改动任何调用方代码。其他服务(输入、UI、场景流程)照常工作——它们只是不再收到事件。没有日志,没有副作用。
行为与边界情况
-
仅限主线程。 所有
Subscribe和Publish调用都必须来自 Unity 主线程。Debug 构建会对此做断言。Release 构建出于速度考虑跳过检查,因此在那里从后台线程调用可能破坏状态——绝对不要这样做。要从工作线程发布,先把工作排回主线程(调度器服务正好有一个在主线程执行的调度功能)。 -
派发基于快照运行。 每次
Publish都会遍历发布开始时获取的订阅者列表快照。这使得处理器在派发期间订阅或退订——甚至退订它自己——都是安全的:变更只对下一次发布生效,而不影响正在进行的这一次。快照是每次发布的一小笔分配;在极高发布频率且订阅者众多的情况下,这会表现为垃圾回收压力,因此对非常高频的更新尽量做批处理。 -
缓存总线,不要每次都 Resolve。
Resolve<IEventBus>()是一次字典查找。在Update中调用它每帧都要付出这份成本。在Awake或Start中获取一次并保留引用。 -
事件类型必须是
class或record,不能是struct。这让派发保持简单,并避免装箱分配。 -
没有基于继承的派发。 订阅基类事件类型不会收到派生事件类型。
Publish<DerivedEvent>时不会调用Subscribe<BaseEvent>的处理器。请订阅与你发布时完全相同的类型。 -
释放订阅令牌。 忘记
Dispose()会让处理器永远保持注册——这既是内存泄漏,还会让订阅对象一直存活。在OnDisable/OnDestroy中退订,或对作用域内的处理器使用using。 -
处理器异常不会中断派发。 如果某个处理器抛出异常,异常会被记录,下一个处理器仍会执行。你的发布调用总是正常完成。
-
自定义事件类在 IL2CPP 下需要
link.xml。 如果你使用 IL2CPP 构建,请把事件类加入项目的link.xml,以免构建的代码裁剪把它们移除:<assembly fullname="YourNamespace" preserve="all"> <type fullname="YourNamespace.PlayerDamagedEvent" preserve="all"/> </assembly>