Files
ShrinkEventBus/README.md
T
cneicy 901ad18453
Publish UPM package / publish (push) Successful in 3s
Publish NuGet packages / publish (push) Successful in 3m43s
feat: update Godot integration and coordinated package dependencies
2026-11-06 19:13:18 +08:00

280 lines
11 KiB
Markdown
Raw 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
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 -->