280 lines
11 KiB
Markdown
280 lines
11 KiB
Markdown
# ShrinkEventBus
|
||
|
||
ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。
|
||
|
||
Godot 和普通 .NET 项目安装 `ShrinkSDK.EventBus`;可打包源码位于 `DotNet~`,订阅注册由 `ShrinkSDK.CodeGen` 在构建时织入。
|
||
|
||
## Unity 安装
|
||
|
||
要求 Unity 2022.3 或更新版本。在 Package Manager 中使用 **Add package from git URL**,依次安装:
|
||
|
||
```text
|
||
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b
|
||
https://github.com/cneicy/ShrinkEventBus.git
|
||
```
|
||
|
||
第一个地址固定 UniTask 2.5.10。Mono.Cecil 1.11.4 由 Unity 官方 registry 自动解析;无需配置 ShrinkSDK registry。需要锁定 EventBus 版本时,在 Git URL 后添加 `#<commit>`。
|
||
|
||
### 可选 ShrinkSDK 集成
|
||
|
||
| 已安装的共享包 | 行为 |
|
||
|---|---|
|
||
| 无 | 使用包内调度接口和 EventBus 专用 ILPostProcessor |
|
||
| Runtime Abstractions 0.2.0+ | 额外启用共享主线程调度器适配程序集,并支持显式延后发布 |
|
||
| Shared CodeGen 0.2.1+ | 停用包内织入,交给共享 CodeGen 统一生成订阅 |
|
||
| 两者都有 | 同时启用上述两项集成 |
|
||
|
||
通过 asmdef `versionDefines` 自动生成 `SHRINK_EVENTBUS_HAS_SHARED_RUNTIME` 和 `SHRINK_EVENTBUS_HAS_SHARED_CODEGEN`,不需要手工设置宏。两个共享包由完整工程自行安装,EventBus 不再强制拉取它们。
|
||
|
||
Unity 自定义调度器现在实现 `IShrinkEventBusMainThreadDispatcher`。已有的共享 `IShrinkMainThreadDispatcher` 实现使用以下适配方式:
|
||
|
||
```csharp
|
||
using ShrinkEventBus;
|
||
using ShrinkEventBus.Integration;
|
||
|
||
ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(
|
||
new ShrinkSharedMainThreadDispatcher(sharedDispatcher));
|
||
```
|
||
|
||
使用自定义 asmdef 的业务程序集还需引用 `ShrinkEventBus.Integration.Runtime`。默认 UniTask PlayerLoop 调度无需配置;请在创建需要自定义调度的 Bus 前完成配置。此调整只影响 Unity,现有 .NET/Godot 配置接口保持不变。
|
||
|
||
### 最小使用示例
|
||
|
||
业务 asmdef 引用 `ShrinkEventBus.Runtime`,使用 UniTask 时再引用 `UniTask`。无需 `partial`、反射注册或手工生成绑定:
|
||
|
||
```csharp
|
||
using ShrinkEventBus;
|
||
using UnityEngine;
|
||
|
||
public readonly struct PlayerJoined : IShrinkEvent { }
|
||
|
||
[ShrinkEventSubscriber]
|
||
public sealed class PlayerHandlers
|
||
{
|
||
[ShrinkSubscribe]
|
||
private void OnJoined(PlayerJoined value) => Debug.Log("Player joined");
|
||
}
|
||
|
||
// 在 Unity 主线程执行;binding 应由实际宿主持有并在生命周期结束时释放。
|
||
using (var binding = EventBus.Attach(new PlayerHandlers()))
|
||
{
|
||
EventBus.Post(new PlayerJoined());
|
||
}
|
||
```
|
||
|
||
包内 `Development~/UnityProject` 是独立验证工程,包含外部消费程序集的同步/异步订阅测试。使用 Unity Test Runner 运行 EditMode 和 PlayMode 测试;包本身的测试通过 manifest 的 `testables` 开启。
|
||
|
||
## 核心契约
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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 包括 `Game`、`Server`、`Scene(id)`、`Mod(id)` 和 `World(id)`。调度器在 Bus 创建时确定,之后不可修改:
|
||
|
||
| Scheduler | 语义 |
|
||
|---|---|
|
||
| `Inline` | 在发布线程立即执行 |
|
||
| `MainThread` | Unity PlayerLoop 主线程;已在主线程时直调 |
|
||
| `DedicatedThread` | 有界队列、专属线程、Ordered 串行 |
|
||
| `TaskPool` | 有界队列、显式最大并发;Parallel 忽略优先级顺序 |
|
||
|
||
`Post` 表示投递:同线程同步完成,跨线程入队后返回。`PostAsync` 等待全部 handler 完成,并传播取消与异常。需要结果或最终取消状态时使用 `PostAsync`。
|
||
|
||
## 唯一订阅入口
|
||
|
||
```csharp
|
||
[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`、类型 `DefaultBus`、`Attach` 传入默认 Bus、否则 `game`。
|
||
|
||
普通对象由宿主管理生命周期:
|
||
|
||
```csharp
|
||
using var binding = EventBus.Attach(new PlayerHandlers());
|
||
```
|
||
|
||
MonoBehaviour 不需要继承 SDK 基类。需要从 `Awake` 持续订阅到 `OnDestroy` 时,可以显式选择编译期生命周期织入:
|
||
|
||
```csharp
|
||
[ShrinkEventSubscriber(Lifetime = ShrinkSubscriberLifetime.AwakeToDestroy)]
|
||
public sealed class PlayerHandlers : MonoBehaviour
|
||
{
|
||
[ShrinkSubscribe]
|
||
private void OnJoined(PlayerJoinedEvent value) { }
|
||
}
|
||
```
|
||
|
||
默认 `Lifetime` 为 `Manual`,普通对象与由 Context、Scene 或 Mod 宿主管理的实例继续自行持有 `EventBus.Attach(...)` 返回的绑定。需要随启用状态反复订阅的 MonoBehaviour 可以自行在 `OnEnable/OnDisable` 中 Attach/Dispose,也可以在 GameObject 上添加 `ShrinkMonoEventScope`,由它统一绑定同对象或子层级中的生成 subscriber。
|
||
|
||
静态类型同样只使用特性:
|
||
|
||
```csharp
|
||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||
public static class GlobalHandlers
|
||
{
|
||
[ShrinkSubscribe]
|
||
private static void OnStartup(GameStartupEvent value) { }
|
||
}
|
||
```
|
||
|
||
Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 `partial`,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。
|
||
|
||
## Unity 与纯 .NET
|
||
|
||
Unity 包依赖 UniTask:
|
||
|
||
```csharp
|
||
UniTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||
```
|
||
|
||
纯 .NET 实现在 `Tools~/DotNet/ShrinkEventBus.Core`:
|
||
|
||
```csharp
|
||
ValueTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||
```
|
||
|
||
两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。
|
||
|
||
## ECS/Burst
|
||
|
||
`com.cneicy.shrink-eventbus-entities` 提供:
|
||
|
||
```csharp
|
||
ShrinkEcsEventWriter<TEvent>
|
||
ShrinkEcsEventQueue<TEvent>.Playback(IShrinkEventBus bus)
|
||
```
|
||
|
||
Burst Job 只写 unmanaged 事实事件到 NativeQueue;Playback 在托管/ECS 系统阶段进入相同 Bus。取消、结果或等待异步完成的事件不应从 Burst Job 直接发布。
|
||
|
||
## 队列与关闭
|
||
|
||
`ShrinkBusOptions` 配置队列容量、溢出策略、最大并发、关闭排空和超时。关键业务默认使用 `Reject`;`DropNewest/DropOldest` 只适用于明确允许丢失的数据;`Wait` 只由 `PostAsync` 使用。
|
||
|
||
## 延后发布与诊断
|
||
|
||
MainThread Bus 可以用 `PostDeferred` 把事件推迟到下一次主线程泵送:
|
||
|
||
```csharp
|
||
var result = EventBus.PostDeferred(gameBus, new PlayerReadyEvent());
|
||
if (!result.Accepted)
|
||
Debug.LogWarning($"deferred post rejected: {result.Failure}");
|
||
```
|
||
|
||
延后发布只接受 `MainThread` 调度且宿主实现了 `IShrinkDeferredMainThreadDispatcher` 的 Bus;其他调度器返回 `DeferredUnsupported`,不会退化成同步分发。队列仍遵守 `QueueCapacity` 与 `OverflowPolicy`,重入发布的事件留到下一次泵送。
|
||
|
||
需要外部工具观察分发时使用公开诊断订阅:
|
||
|
||
```csharp
|
||
using var subscription = EventBus.ObserveDiagnostics(diagnostic =>
|
||
{
|
||
Debug.Log($"{diagnostic.BusKey} {diagnostic.EventType.Name} {diagnostic.Result.Failure}");
|
||
});
|
||
```
|
||
|
||
`ShrinkEventDiagnostic` 提供时间、事件类型、Bus、调度器、分发模式、线程、耗时、发布结果和同步标记,不暴露事件对象。观察者抛出的异常会记录并隔离,不影响业务分发或其他观察者。
|
||
|
||
## UI Toolkit 调试器
|
||
|
||
菜单:`ShrinkSDK/事件总线/事件查看器`。
|
||
|
||
窗口使用密集的 Bus 导航、事件流和事件详情三段布局,提供 Bus/Scheduler/DispatchMode/队列/溢出策略/handler 数量、实时速率、平均分发耗时、同步或异步执行方式、实际线程、`ShrinkPostResult`、问题筛选、跟随、搜索、CSV 导出与最多 5000 条环形保留。详细采样只在窗口开启且 `Capture` 为启用状态时生效,暂停或关闭窗口后不计时、不保留事件对象。
|
||
|
||
它只观察静态 `EventBus` 宿主管理的 Bus,不捕获独立 `ShrinkEventBusHost`;性能基准仍使用独立 Host,避免 UI 采样进入被测热路径。
|
||
|
||
Godot 项目安装 `ShrinkSDK.EventBus.Godot` 后可在编辑器中使用同一套诊断数据的独立调试窗口。
|
||
|
||
## Benchmark
|
||
|
||
实现位于 `Benchmark/ShrinkEventBusBenchmark.cs`,入口:
|
||
|
||
```csharp
|
||
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 诊断是可选观察层,不参与正式无诊断热路径。
|
||
|
||
Unity 独立安装不包含 Entities/Burst 适配包;需要时由宿主额外安装。
|
||
|
||
## 可复用能力
|
||
|
||
<!-- shrink:capabilities -->
|
||
Capability: 按类型发布事实和请求,支持同步、异步、优先级和可撤回订阅
|
||
Aliases: 事件 事件未触发 消息 订阅 通知 event bus
|
||
Limits: EventBus 不等于业务状态机或资源计量;并行派发须由业务明确选择
|
||
Extension: IShrinkEvent、ShrinkSubscribe、BusOptions 和拥有者 Attach/Dispose
|
||
Evidence: [EventBus](Runtime/EventBus.cs); [ShrinkSubscribeAttribute](Runtime/ShrinkEventAttributes.cs); [ShrinkGeneratedBinding](Runtime/ShrinkEventBindings.cs)
|
||
<!-- /shrink:capabilities -->
|