Files

7.1 KiB
Raw Permalink Blame History

ShrinkEventBus 2.0

ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。

Godot 和普通 .NET 项目安装 ShrinkSDK.EventBus;可打包源码位于 DotNet~,订阅注册由 ShrinkSDK.CodeGen 在构建时织入。

核心契约

public interface IShrinkEvent { }

public interface IShrinkCancelableEvent : IShrinkEvent
{
    bool IsCanceled { get; }
    void SetCanceled(bool value);
}

public interface IShrinkResultEvent<TResult> : IShrinkEvent
{
    TResult Result { get; }
    void SetResult(TResult result);
}

事件可以是 class、struct 或 unmanaged struct,不需要继承 SDK 基类。

多 Bus

var gameBus = EventBus.GetOrCreateBus(
    ShrinkBusKey.Game,
    ShrinkBusOptions.MainThread());

var modBus = EventBus.CreateBus(
    ShrinkBusKey.Mod("com.example.mod"),
    ShrinkBusOptions.DedicatedThread());

var workerBus = EventBus.CreateBus(
    new ShrinkBusKey("worker", "pathfinding"),
    ShrinkBusOptions.TaskPool(maxConcurrency: 4));

标准 Key 包括 GameServerScene(id)Mod(id)World(id)。调度器在 Bus 创建时确定,之后不可修改:

Scheduler 语义
Inline 在发布线程立即执行
MainThread Unity PlayerLoop 主线程;已在主线程时直调
DedicatedThread 有界队列、专属线程、Ordered 串行
TaskPool 有界队列、显式最大并发;Parallel 忽略优先级顺序

Post 表示投递:同线程同步完成,跨线程入队后返回。PostAsync 等待全部 handler 完成,并传播取消与异常。需要结果或最终取消状态时使用 PostAsync

唯一订阅入口

[ShrinkEventSubscriber(
    OwnerId = "com.example.mod",
    DefaultBus = "mod:com.example.mod")]
public sealed class PlayerHandlers
{
    [ShrinkSubscribe(Priority = ShrinkEventPriority.High)]
    private void OnJoined(PlayerJoinedEvent value)
    {
    }

    [ShrinkSubscribe]
    private async UniTask OnLoaded(
        PlayerLoadedEvent value,
        CancellationToken cancellationToken)
    {
        await UniTask.Yield(cancellationToken);
    }
}

Bus 解析顺序:方法 Bus、类型 DefaultBusAttach 传入默认 Bus、否则 game

普通对象由宿主管理生命周期:

using var binding = EventBus.Attach(new PlayerHandlers());

MonoBehaviour 不需要继承 SDK 基类。需要从 Awake 持续订阅到 OnDestroy 时,可以显式选择编译期生命周期织入:

[ShrinkEventSubscriber(Lifetime = ShrinkSubscriberLifetime.AwakeToDestroy)]
public sealed class PlayerHandlers : MonoBehaviour
{
    [ShrinkSubscribe]
    private void OnJoined(PlayerJoinedEvent value) { }
}

默认 LifetimeManual,普通对象与由 Context、Scene 或 Mod 宿主管理的实例继续自行持有 EventBus.Attach(...) 返回的绑定。需要随启用状态反复订阅的 MonoBehaviour 可以自行在 OnEnable/OnDisable 中 Attach/Dispose,也可以在 GameObject 上添加 ShrinkMonoEventScope,由它统一绑定同对象或子层级中的生成 subscriber。

静态类型同样只使用特性:

[ShrinkEventSubscriber(DefaultBus = "game")]
public static class GlobalHandlers
{
    [ShrinkSubscribe]
    private static void OnStartup(GameStartupEvent value) { }
}

Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 partial,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 MethodInfo.InvokeDynamicInvokeobject[] 参数。

Unity 与纯 .NET

Unity 包依赖 UniTask

UniTask<ShrinkPostResult> PostAsync<TEvent>(...)

纯 .NET 实现在 Tools~/DotNet/ShrinkEventBus.Core

ValueTask<ShrinkPostResult> PostAsync<TEvent>(...)

两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。

ECS/Burst

com.cneicy.shrink-eventbus-entities 提供:

ShrinkEcsEventWriter<TEvent>
ShrinkEcsEventQueue<TEvent>.Playback(IShrinkEventBus bus)

Burst Job 只写 unmanaged 事实事件到 NativeQueuePlayback 在托管/ECS 系统阶段进入相同 Bus。取消、结果或等待异步完成的事件不应从 Burst Job 直接发布。

队列与关闭

ShrinkBusOptions 配置队列容量、溢出策略、最大并发、关闭排空和超时。关键业务默认使用 RejectDropNewest/DropOldest 只适用于明确允许丢失的数据;Wait 只由 PostAsync 使用。

UI Toolkit 调试器

菜单:ShrinkSDK/事件总线/事件查看器

窗口使用密集的 Bus 导航、事件流和事件详情三段布局,提供 Bus/Scheduler/DispatchMode/队列/溢出策略/handler 数量、实时速率、平均分发耗时、同步或异步执行方式、实际线程、ShrinkPostResult、问题筛选、跟随、搜索、CSV 导出与最多 5000 条环形保留。详细采样只在窗口开启且 Capture 为启用状态时生效,暂停或关闭窗口后不计时、不保留事件对象。

它只观察静态 EventBus 宿主管理的 Bus,不捕获独立 ShrinkEventBusHost;性能基准仍使用独立 Host,避免 UI 采样进入被测热路径。

Benchmark

实现位于 Benchmark/ShrinkEventBusBenchmark.cs,入口:

ShrinkEventBusBenchmark.Run(
    iterations: 1_000_000,
    massIterations: 10_000,
    registrationIterations: 5_000);

2026-08-24 在 Unity 2022.3.62f3、Windows Editor Play Mode、独立 Inline Host 下三轮中位数(全局调试采样不观察该 Host):

场景 2.0 ops/s 1.3.0 基线
0 handler 129,282,649 8,971,378
1 handler 67,249,586 6,132,242
8 handlers, struct 35,024,798 无同口径数据
8 handlers, class 25,321,199 MessagePipe 图示 25,639,260
30 handlers 10,823,612 258,805
canceled skips 10 9,029,517 1,894,776
1 async handler 5,909,446 1,168,634
generated Attach + Dispose 674,291 instance scan 76,353

同步 struct Post 热路径实测为 0 B / 10,000,000 ops;全局门面在没有 Network bridge 对象观察者时也不会为 struct 事件装箱。MessagePipe 图来自不同机器和 .NET benchmark,class 结果只能说明已达到同一吞吐量级,不能作为跨设备胜负结论。当前 Windows x64 Development IL2CPP Player 已构建成功;仍应在目标平台 Player/设备上复测吞吐。

设计边界

  • handler 不单独选择线程;需要不同线程亲和性时发布到另一个 Bus。
  • Parallel 分发不承诺优先级顺序。
  • 同步 Post 遇到异步 handler 时是 fire-and-forget;需要等待使用 PostAsync
  • 外部 Mod DLL 必须携带生成合同;没有合同的生产 DLL 不自动反射注册。
  • Editor 诊断是可选观察层,不参与正式无诊断热路径。

架构审计与来源说明见 Docs/JustAnyProjectArchitectureAudit.md 和根目录 DESIGN.md