延迟事件队列

现在把事件入队,稍后在你选定的 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 LogLifecycletrue 时,编辑器构建会记录入队/flush 踪迹;这些踪迹在发布构建中被剥离。默认 false
DeferredBusOptions(int maxDrainPerFlush, int initialQueueCapacity, bool logLifecycle = false)显式构造函数;数值范围在构造时被钳制。
static DeferredBusOptions Default0 / 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 抛了异常,队列会捕获它、记录它,并继续处理剩余事件。框架自带的事件总线本身已经隔离了有问题的订阅者;这道防线覆盖的是那些不做隔离的替换总线。

相关页面

  • 事件总线 — 本队列经由其发布的路由层
  • 调度器 — 在选定时间稍后运行回调,而不是在 flush 点
  • 日志服务 — flush 故障与生命周期踪迹的去处