Files
ShrinkEventBus/README.md
T
cneicy a6011cd01d
Publish UPM package / publish (push) Successful in 2s
feat: add opt-in MonoBehaviour lifecycle weaving
2026-08-28 04:31:00 +08:00

183 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ShrinkEventBus 2.0
ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。
## 核心契约
```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 事实事件到 NativeQueuePlayback 在托管/ECS 系统阶段进入相同 Bus。取消、结果或等待异步完成的事件不应从 Burst Job 直接发布。
## 队列与关闭
`ShrinkBusOptions` 配置队列容量、溢出策略、最大并发、关闭排空和超时。关键业务默认使用 `Reject``DropNewest/DropOldest` 只适用于明确允许丢失的数据;`Wait` 只由 `PostAsync` 使用。
## UI Toolkit 调试器
菜单:`ShrinkSDK/事件总线/事件查看器`
窗口使用密集的 Bus 导航、事件流和事件详情三段布局,提供 Bus/Scheduler/DispatchMode/队列/溢出策略/handler 数量、实时速率、平均分发耗时、同步或异步执行方式、实际线程、`ShrinkPostResult`、问题筛选、跟随、搜索、CSV 导出与最多 5000 条环形保留。详细采样只在窗口开启且 `Capture` 为启用状态时生效,暂停或关闭窗口后不计时、不保留事件对象。
它只观察静态 `EventBus` 宿主管理的 Bus,不捕获独立 `ShrinkEventBusHost`;性能基准仍使用独立 Host,避免 UI 采样进入被测热路径。
## 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 诊断是可选观察层,不参与正式无诊断热路径。
架构审计与来源说明见 `Docs/JustAnyProjectArchitectureAudit.md` 和根目录 `DESIGN.md`