延迟事件队列
现在把事件入队,稍后在你选定的 Flush 时机按入队顺序发布。
在危险位置先把事件排队,然后在你选定的安全点把整批事件按序发布。
| 接口 | IDeferredBus |
| 关闭开关 | NullDeferredBus |
| 程序集 | CommonGameSystem.Core |
| 启动 | 启动时自动注册(23 个服务之一)——无需任何设置 |
它做什么
延迟事件队列是事件总线(IEventBus)之上的一层薄封装。你不再立即发布事件,而是调用 Enqueue 把它入队。队列会保留一切,直到你调用 Flush()——它把整批事件经由事件总线发布出去,且跨所有事件类型严格按入队顺序进行。
用它把事件发布从危险位置挪走——物理回调里、对一个你同时也在修改的集合的遍历中、输入处理的中途——挪到你选定的安全点。队列只负责保管与排序;路由和订阅者处理仍归事件总线。没有自动 flush:整批何时触发由你决定。仅限主线程。
快速上手
把事件定义为普通类:
public sealed class EnemyDiedEvent
{
public int EnemyId { get; }
public EnemyDiedEvent(int enemyId) { EnemyId = enemyId; }
}
然后现在入队、稍后发布:
using CommonGameSystem.Core;
using UnityEngine;
public class MyDeferredPublisher : MonoBehaviour
{
private IDeferredBus _bus;
private void Awake() => _bus = ServiceLocator.Resolve<IDeferredBus>();
public void OnEnemyDied(int enemyId)
{
_bus.Enqueue(new EnemyDiedEvent(enemyId)); // queued — NOT published yet
}
private void LateUpdate()
{
_bus.Flush(); // publishes the batch through the Event Bus, in queue order
}
}
Bootstrap 会在启动时自动注册该服务。如上所示,在 Awake() 中解析并缓存一次。
API 参考
IDeferredBus
| 成员 | 说明 |
|---|---|
void Enqueue<TEvent>(TEvent evt) where TEvent : class | 现在把 evt 入队;在 Flush() 之前不会发布。null 事件立即抛出 ArgumentNullException,让堆栈跟踪直指调用方。class 约束与事件总线完全一致——不支持结构体事件。在 IL2CPP/AOT 构建下安全;不会发生装箱。 |
void Flush() | 按入队顺序发布整批排队事件,跨所有事件类型,每个事件对应一次事件总线的 Publish。flush 运行期间入队的事件进入下一批。在进行中的 flush 内部再调用 Flush 不做任何事。 |
int PendingCount { get; } | 已入队但尚未 flush 的事件数。诊断用。 |
DeferredBusOptions — 构造期的 readonly struct
| 成员 | 说明 |
|---|---|
int MaxDrainPerFlush | 每次 Flush 发布事件数的上限。0(默认)表示不限。非零值(钳制到 1–1,000,000)发布该数量的事件,把其余挪到下一批的最前面,并记录一条节流的警告。 |
int InitialQueueCapacity | 内部缓冲区的初始大小。默认 16;钳制到 0–4096。 |
bool LogLifecycle | 为 true 时,编辑器构建会记录入队/flush 踪迹;这些踪迹在发布构建中被剥离。默认 false。 |
DeferredBusOptions(int maxDrainPerFlush, int initialQueueCapacity, bool logLifecycle = false) | 显式构造函数;数值范围在构造时被钳制。 |
static DeferredBusOptions Default | 0 / 16 / false。注意它不同于 default(DeferredBusOptions)——后者是 0 / 0 / false。 |
DeferredBusOptions With(int? maxDrainPerFlush = null, int? initialQueueCapacity = null, bool? logLifecycle = null) | 带具名覆盖项的不可变副本。 |
示例
using CommonGameSystem.Core;
using UnityEngine;
public sealed class DamageDealtEvent
{
public int EnemyId { get; }
public int Amount { get; }
public DamageDealtEvent(int enemyId, int amount) { EnemyId = enemyId; Amount = amount; }
}
public class DamageResolver : MonoBehaviour
{
private IDeferredBus _deferred;
private void Awake() => _deferred = ServiceLocator.Resolve<IDeferredBus>();
// Called from inside a physics callback — publishing immediately here could
// re-enter the collection we are iterating. Queue it instead.
public void OnHit(int enemyId, int amount)
{
_deferred.Enqueue(new DamageDealtEvent(enemyId, amount));
}
// Publish at a known-safe point: the end of the fixed-update step.
private void FixedUpdate()
{
if (_deferred.PendingCount > 0)
_deferred.Flush(); // every queued event publishes here, in queue order
}
}
关闭它
ServiceLocator.Replace<IDeferredBus>(new NullDeferredBus());
这会彻底静默延迟机制。Enqueue 静默丢弃事件(不会转投任何真实总线),Flush 不做任何事,PendingCount 保持 0。按真实延迟队列接线的游戏在延迟被关闭的情况下照常运行。与事件总线那个静默的 NullEventBus 不同,此替换实现构造时会记录一条警告。被静默丢弃的队列("事件似乎入了队却永远不到")很难诊断,所以意外的替换被做成了可见的。null 事件仍会抛异常,与真实服务完全一致。
常见陷阱
- 不调用
Flush就什么都不会触发。 没有自动 flush。只入队、从不 flush,事件就永远不会发布,PendingCount无限增长。选定一个可预期的排空点——FixedUpdate末尾、帧末——并始终在那里 flush。 - flush 期间入队的事件等待下一次。 批次在
Flush开始的那一刻就固定了。订阅者在批次发布过程中入队的任何事件都被留到下一次Flush。这是有意为之:它保证每一次 flush 都有界。 MaxDrainPerFlush只会拆分批次——绝不丢弃事件。 达到上限时,剩余事件被挪到下一批的最前面,因此它们仍会先于此后入队的一切发布。顺序被保留,并记录一条节流的警告。它是泄压阀,不是丢弃机制。- flush 绝不抛异常,一个坏事件挡不住其余事件。 如果某次
Publish抛了异常,队列会捕获它、记录它,并继续处理剩余事件。框架自带的事件总线本身已经隔离了有问题的订阅者;这道防线覆盖的是那些不做隔离的替换总线。