feat(packages): 内置 SDK 包并完善 ContextLoader 集成

- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档
- 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理
- 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言
- 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则
- 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护
- 新增 Network 语义扫描、模板保护和 App 组件生命周期测试
- 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试
- 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
This commit is contained in:
2026-08-18 18:06:34 +08:00
parent 517c4cf46e
commit d74c2f08ca
240 changed files with 13647 additions and 545 deletions
Submodule Assets/Modules/ShrinkEventBus deleted from 8acccdcc4b
+99
View File
@@ -0,0 +1,99 @@
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
.utmp/
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
*.log
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
*.blend1
*.blend1.meta
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Jetbrains Rider personal-layer settings
*.DotSettings.user
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Mono auto generated files
mono_crash.*
# Builds
*.apk
*.aab
*.unitypackage
*.unitypackage.meta
*.app
# Crashlytics generated file
crashlytics-build.properties
# TestRunner generated files
InitTestScene*.unity*
# Addressables default ignores, before user customizations
/ServerData
/[Aa]ssets/StreamingAssets/aa*
/[Aa]ssets/AddressableAssetsData/link.xml*
/[Aa]ssets/Addressables_Temp*
# By default, Addressables content builds will generate addressables_content_state.bin
# files in platform-specific subfolders, for example:
# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin
/[Aa]ssets/AddressableAssetsData/*/*.bin*
# Visual Scripting auto-generated files
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
# Auto-generated scenes by play mode tests
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
@@ -0,0 +1,83 @@
╔══════════════════════════════════════════════════╗
║ ShrinkEventBus Benchmark Report ║
║ iter= 1000000 mass= 10000 reg= 5000 scan=1000 ║
╚══════════════════════════════════════════════════╝
基准对象:独立实例 busAllowPerPhaseDispatch 已开启)
采集于 1.3.0EventBase 瘦身后;Unity 2022.3.62f3, Editor Play Mode
▶ TriggerEvent × 无订阅者(基线)
总耗时 : 111.466 ms
单次 : 0.1115 μs/op
吞吐量 : 8971378 ops/s
▶ TriggerEvent × 单订阅者同步
总耗时 : 163.073 ms
单次 : 0.1631 μs/op
吞吐量 : 6132242 ops/s
▶ TriggerEvent × 父事件监听子事件
总耗时 : 171.424 ms
单次 : 0.1714 μs/op
吞吐量 : 5833479 ops/s
▶ TriggerEvent × 30 订阅者同步
总耗时 : 38.639 ms
单次 : 3.8639 μs/op
吞吐量 : 258805 ops/s
▶ TriggerEvent(Phase=HIGH) × 3 phase 监听
总耗时 : 164.197 ms
单次 : 0.1642 μs/op
吞吐量 : 6090238 ops/s
▶ TriggerEventAsync × 单订阅者异步
总耗时 : 0.856 ms
单次 : 0.8557 μs/op
吞吐量 : 1168634 ops/s
▶ RegisterEvent(delegate)
总耗时 : 14.418 ms
单次 : 2.8836 μs/op
吞吐量 : 346791 ops/s
▶ UnregisterEvent(delegate)
总耗时 : 3.427 ms
单次 : 0.6853 μs/op
吞吐量 : 1459130 ops/s
▶ Register/Unregister(instance scan)
总耗时 : 13.097 ms
单次 : 13.0971 μs/op
吞吐量 : 76353 ops/s
▶ Register/Unregister(static type scan)
总耗时 : 9.337 ms
单次 : 9.3374 μs/op
吞吐量 : 107096 ops/s
▶ Register/Unregister(MethodInfo)
总耗时 : 6.513 ms
单次 : 6.5134 μs/op
吞吐量 : 153530 ops/s
▶ TriggerEvent × new() [GC Gen0= 1]
总耗时 : 266.536 ms
单次 : 0.2665 μs/op
吞吐量 : 3751843 ops/s
▶ TriggerEvent × Pool.Get() [GC Gen0= 0]
总耗时 : 242.015 ms
单次 : 0.2420 μs/op
吞吐量 : 4131982 ops/s
▶ TriggerEvent × 已取消事件跳过(10 订阅者)
总耗时 : 52.777 ms
单次 : 0.5278 μs/op
吞吐量 : 1894776 ops/s
══════════════════════════════════════════════════
全部测试完成
对比 1.1.x 旧样本:new() 路径 7684ms / GC Gen0=6457 → 266ms / GC Gen0=1
(EventBase 不再每实例分配内置 ListenerListnew() 路径 GC 基本归零,
对象池从“高频必需”降级为可选优化。)
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 87ccd659dba774447b9d6941761b8300
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
# Changelog
本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。
## [1.3.0] - 2026-06-12
### Breaking
- 移除 `EventBase.GetListenerList()`。事件对象不再内置 `ListenerList`,改为持有派发时的快照数组;调试请使用 `GetSubscribers()`(现在返回拷贝)。
- 数字优先级 `0` 现在映射到 `NORMAL`(原为 `LOW`),与枚举重载的默认值保持一致。
- 所有 int 数字优先级重载(`RegisterEvent` / `SubscribeEvent`)不再提供默认值 `0`,必须显式传入数字——消除与枚举重载之间的重载二义性。不带优先级的调用现在唯一解析到枚举重载(`NORMAL`)。
### Fixed
- 织入前会检查类型自身及基类链上是否存在实例 `[EventSubscribe]` 方法,没有的类型不再织入,避免在 `Awake` 中抛出注册异常。
- `AutoRegister`(含 Mod 框架反射路径)遇到没有任何匹配 `[EventSubscribe]` 方法的类型时,改为输出警告并跳过,不再抛异常;显式 `Register()` 仍保持严格抛错。
- `ListenerList` 父子链脏标记传播与快照重建之间的竞态:同一总线内所有监听列表(含父子链)现共用一把锁。
- `RegisterCore` 的 check-then-act 竞态(并发下同一实例可能被注册两次)。
- `EventPool.Release` 的双重归还竞态;对象池增加容量上限(128),超出直接丢弃。
- 父子事件同 phase 内的 handler 顺序:现在按数字优先级跨父子稳定归并(平局时子类型 handler 在前),数字优先级不再只在单层内生效。
### Changed
- 派发热路径重构:事件对象的监听者快照从“逐个带锁二分插入内置 ListenerList”改为一次数组引用赋值;`EventId` 改为懒生成;事件元数据缓存改用 `ConcurrentDictionary`。每个事件实例的构造分配显著减少。
- 删除死代码 `EventCache<T>`(README 此前描述的“泛型静态缓存热路径”自实例化总线改造后已不存在)。
- 恢复模块内 `CodeGen/` 作为可选本地织入管线:命中 `ShrinkShared.CodeGen` 覆盖范围的程序集继续由共享管线优先处理,其余只引用 `ShrinkEventBus.Runtime` 的程序集改由本地 `EventBusILPostProcessor` 兜底,避免 `[EventBusSubscriber]` 自动注册失效。
- `GetEventSubscribers` / `GetAllSubscribersSnapshot` / `EventBase.GetSubscribers` 返回内部快照的拷贝,外部修改不再影响派发顺序。
- `SetPhase` / `IsCanceled` / `Result` 的校验异常现在携带具体错误消息。
- ILPP 的诊断列表与程序集解析器改为调用内局部状态,消除共享实例并发隐患。
## [1.2.0] - 2026-05-18
### Changed
- `MonoBehaviour` 自动注册/反注册的 IL 织入已并入共享 `ShrinkShared.CodeGen` 管线;当前工作区同时保留 `ShrinkEventBus` 本地 `CodeGen/` 作为可选兜底,以兼容纯 `ShrinkEventBus.Runtime` 引用程序集。
- 保留现有 `[EventBusSubscriber]` 用法与自动生命周期行为,不要求业务层改写订阅代码。
## [1.1.6] - 2026-05-04
### Changed
- 运行时总线升级为“默认静态门面 + 可实例化 `IShrinkEventBus`/Builder”双层结构,支持异常策略、事件类型校验、按 phase 分发。
- 手动注册能力从纯 delegate 扩展为 `object / Type / MethodInfo` 三类入口,并在注册阶段严格校验签名、静态/实例模式和事件参数类型。
- `ListenerList` 改为按 phase 分桶 + 快照缓存模型,并支持父事件监听子事件。
- 事件查看器不再反射私有字典,改走公开快照接口。
- `EventBusBenchmark` 扩展为覆盖实例 bus、扫描注册、继承监听、phase 分发、对象池与取消事件跳过等关键场景。
## [1.1.5] - 2026-04-07
### Changed
- 修正 `PrepareForDispatch()`,现在只重置派发期状态,不再误清业务载荷。
- 修正同步 `TriggerEvent(...)` 遇到异步 handler 时的行为,改为向异步处理器分发脱离原对象的事件快照,避免直接复用原事件对象。
- `OnEventTriggered` 与编辑器追踪钩子改为在派发完成后触发,同时覆盖无监听者事件。
- `ShrinkNetworkEventBusBridge` 的异步转发改为先克隆事件,降低桥接层状态污染风险。
- 事件查看器的实时日志改为快照模型,支持关键词过滤、按“有监听者 / 无监听者”筛选、同类事件折叠聚合显示。
- 编辑器菜单迁移到 `ShrinkSDK/事件总线/事件查看器`
- README 同步更新新的调试入口与同步/异步派发语义说明。
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9b2928da4a2426743bd762f5ebb9c232
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c2bfff1573c10da47ae73a47b8841279
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c297548ebc9165f448111a35488d368e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,375 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Mono.Cecil.Pdb;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;
namespace ShrinkEventBus.CodeGen
{
public sealed class EventBusILPostProcessor : ILPostProcessor
{
private const string RuntimeAssemblyName = "ShrinkEventBus.Runtime";
private static readonly string[] SharedCoverageAssemblyNames =
{
"ShrinkCommand.Runtime",
"ShrinkNetwork.Runtime",
"ShrinkApp.Core.Runtime"
};
public override ILPostProcessor GetInstance() => this;
public override bool WillProcess(ICompiledAssembly compiledAssembly)
{
if (!ReferencesAssembly(compiledAssembly, RuntimeAssemblyName))
return false;
if (ShouldDeferToSharedCodeGen(compiledAssembly))
return false;
return true;
}
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
var diagnostics = new List<DiagnosticMessage>();
if (!WillProcess(compiledAssembly))
return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics);
var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
var module = assemblyDefinition.MainModule;
try
{
var subscriberType = FindType(module, "ShrinkEventBus.EventBusSubscriberAttribute", RuntimeAssemblyName);
var subscribeAttributeType = FindType(module, "ShrinkEventBus.EventSubscribeAttribute", RuntimeAssemblyName);
var eventBusType = FindType(module, "ShrinkEventBus.EventBus", RuntimeAssemblyName);
var staticRegistryCtor = FindTypeArrayConstructor(
module,
"ShrinkEventBus.EventBusStaticRegistryAttribute",
RuntimeAssemblyName);
if (subscriberType == null || subscribeAttributeType == null || eventBusType == null)
return GetResult(assemblyDefinition, diagnostics);
var eventBusTypeDef = eventBusType.Resolve();
var autoRegisterMethod = eventBusTypeDef?.Methods.FirstOrDefault(method =>
method.Name == "AutoRegister" &&
method.IsStatic &&
method.Parameters.Count == 1);
var unregisterMethod = eventBusTypeDef?.Methods.FirstOrDefault(method =>
method.Name == "UnregisterInstance" &&
method.IsStatic &&
method.Parameters.Count == 1);
if (autoRegisterMethod == null || unregisterMethod == null)
return GetResult(assemblyDefinition, diagnostics);
var autoRegisterMethodRef = module.ImportReference(autoRegisterMethod);
var unregisterMethodRef = module.ImportReference(unregisterMethod);
var subscriberTypes = module.Types
.Where(type => HasAttribute(type, subscriberType))
.Where(InheritsFromMonoBehaviour)
.Where(type => HasInstanceSubscribeMethod(type, subscribeAttributeType))
.ToArray();
foreach (var type in subscriberTypes)
{
InjectAutoRegister(type, module, autoRegisterMethodRef);
InjectAutoUnregister(type, module, unregisterMethodRef);
}
if (staticRegistryCtor != null)
{
var staticSubscriberTypes = module.Types
.Where(type => HasAttribute(type, subscriberType))
.Where(type => type.Methods.Any(method =>
method.IsStatic && HasAttribute(method, subscribeAttributeType)))
.Select(type => module.ImportReference(type))
.ToArray();
if (staticSubscriberTypes.Length > 0)
AddAssemblyTypeArrayAttribute(module, staticRegistryCtor, staticSubscriberTypes);
}
}
catch (Exception ex)
{
diagnostics.Add(new DiagnosticMessage
{
DiagnosticType = DiagnosticType.Error,
MessageData = $"[ShrinkEventBus.CodeGen] {ex.Message}"
});
}
return GetResult(assemblyDefinition, diagnostics);
}
private static bool ShouldDeferToSharedCodeGen(ICompiledAssembly compiledAssembly)
{
if (!IsSharedCodeGenAvailable())
return false;
return SharedCoverageAssemblyNames.Any(name => ReferencesAssembly(compiledAssembly, name));
}
private static bool IsSharedCodeGenAvailable()
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (string.Equals(assembly.GetName().Name, "Unity.ShrinkShared.CodeGen", StringComparison.Ordinal))
return true;
}
return Type.GetType("ShrinkShared.CodeGen.ShrinkRegistryILPostProcessor, Unity.ShrinkShared.CodeGen", false) != null;
}
private static bool ReferencesAssembly(ICompiledAssembly compiledAssembly, string assemblyName)
{
return compiledAssembly.References.Any(reference =>
string.Equals(Path.GetFileNameWithoutExtension(reference), assemblyName, StringComparison.Ordinal));
}
private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference expectedAttributeType)
{
return provider.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == expectedAttributeType.FullName);
}
private static bool HasInstanceSubscribeMethod(TypeDefinition type, TypeReference subscribeAttributeType)
{
var current = type;
while (current != null && current.Name != "MonoBehaviour")
{
if (current.Methods.Any(method => !method.IsStatic && HasAttribute(method, subscribeAttributeType)))
return true;
var baseTypeRef = current.BaseType;
if (baseTypeRef == null)
return false;
TypeDefinition? resolvedBase;
try
{
resolvedBase = baseTypeRef.Resolve();
}
catch
{
return true;
}
if (resolvedBase == null)
return true;
current = resolvedBase;
}
return false;
}
private static bool InheritsFromMonoBehaviour(TypeDefinition type)
{
var current = type.BaseType;
while (current != null)
{
if (current.Name == "MonoBehaviour")
return true;
try
{
current = current.Resolve()?.BaseType;
}
catch
{
break;
}
}
return false;
}
private static void InjectAutoRegister(TypeDefinition type, ModuleDefinition module, MethodReference autoRegisterMethodRef)
{
var awake = type.Methods.FirstOrDefault(method => method.Name == "Awake" && !method.IsStatic);
if (awake == null)
{
var baseAwakeRef = FindBaseMethodReference(type, "Awake", module);
if (baseAwakeRef != null)
{
awake = new MethodDefinition("Awake",
MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual,
module.TypeSystem.Void);
var il = awake.Body.GetILProcessor();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, baseAwakeRef);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, autoRegisterMethodRef);
il.Emit(OpCodes.Ret);
type.Methods.Add(awake);
return;
}
awake = new MethodDefinition("Awake",
MethodAttributes.Private | MethodAttributes.HideBySig,
module.TypeSystem.Void);
var retIl = awake.Body.GetILProcessor();
retIl.Emit(OpCodes.Ret);
type.Methods.Add(awake);
}
var processor = awake.Body.GetILProcessor();
var instructions = new List<Instruction>
{
processor.Create(OpCodes.Ldarg_0),
processor.Create(OpCodes.Call, autoRegisterMethodRef),
processor.Create(OpCodes.Nop)
};
instructions.Reverse();
instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction));
}
private static void InjectAutoUnregister(TypeDefinition type, ModuleDefinition module, MethodReference unregisterMethodRef)
{
var onDestroy = type.Methods.FirstOrDefault(method => method.Name == "OnDestroy" && !method.IsStatic);
if (onDestroy == null)
{
var baseOnDestroyRef = FindBaseMethodReference(type, "OnDestroy", module);
if (baseOnDestroyRef != null)
{
onDestroy = new MethodDefinition("OnDestroy",
MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual,
module.TypeSystem.Void);
var il = onDestroy.Body.GetILProcessor();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, unregisterMethodRef);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, baseOnDestroyRef);
il.Emit(OpCodes.Ret);
type.Methods.Add(onDestroy);
return;
}
onDestroy = new MethodDefinition("OnDestroy",
MethodAttributes.Private | MethodAttributes.HideBySig,
module.TypeSystem.Void);
var retIl = onDestroy.Body.GetILProcessor();
retIl.Emit(OpCodes.Ret);
type.Methods.Add(onDestroy);
}
var processor = onDestroy.Body.GetILProcessor();
var instructions = new List<Instruction>
{
processor.Create(OpCodes.Ldarg_0),
processor.Create(OpCodes.Call, unregisterMethodRef),
processor.Create(OpCodes.Nop)
};
instructions.Reverse();
instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction));
}
private static MethodReference? FindBaseMethodReference(TypeDefinition type, string methodName, ModuleDefinition module)
{
try
{
var baseTypeRef = type.BaseType;
while (baseTypeRef != null)
{
var baseTypeDef = baseTypeRef.Resolve();
if (baseTypeDef == null)
break;
var method = baseTypeDef.Methods.FirstOrDefault(candidate =>
candidate.Name == methodName && !candidate.IsStatic && candidate.IsVirtual);
if (method != null)
{
if (baseTypeRef is GenericInstanceType genericInstance)
{
var methodRef = new MethodReference(
method.Name,
module.ImportReference(method.ReturnType),
module.ImportReference(genericInstance))
{
HasThis = method.HasThis,
ExplicitThis = method.ExplicitThis,
CallingConvention = method.CallingConvention
};
return methodRef;
}
return module.ImportReference(method);
}
baseTypeRef = baseTypeDef.BaseType;
}
}
catch
{
}
return null;
}
private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName)
{
var resolved = Type.GetType($"{fullName}, {assemblyName}", false);
return resolved == null ? null : module.ImportReference(resolved);
}
private static MethodReference? FindTypeArrayConstructor(ModuleDefinition module, string fullName, string assemblyName)
{
var typeRef = FindType(module, fullName, assemblyName);
var typeDef = typeRef?.Resolve();
var ctor = typeDef?.Methods.FirstOrDefault(method =>
method.IsConstructor &&
method.Parameters.Count == 1 &&
method.Parameters[0].ParameterType.IsArray &&
method.Parameters[0].ParameterType.GetElementType().FullName == module.ImportReference(typeof(Type)).FullName);
return ctor == null ? null : module.ImportReference(ctor);
}
private static void AddAssemblyTypeArrayAttribute(ModuleDefinition module, MethodReference ctor, TypeReference[] types)
{
var attribute = new CustomAttribute(ctor);
var typeTypeRef = module.ImportReference(typeof(Type));
attribute.ConstructorArguments.Add(new CustomAttributeArgument(
module.ImportReference(typeof(Type[])),
types.Select(type => new CustomAttributeArgument(typeTypeRef, type)).ToArray()));
module.Assembly.CustomAttributes.Add(attribute);
}
private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
{
var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly);
var readerParameters = new ReaderParameters
{
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()),
SymbolReaderProvider = new PdbReaderProvider(),
AssemblyResolver = assemblyResolver,
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
ReadingMode = ReadingMode.Immediate
};
var assemblyDefinition = AssemblyDefinition.ReadAssembly(
new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()),
readerParameters);
assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition);
return assemblyDefinition;
}
private static ILPostProcessResult GetResult(AssemblyDefinition assemblyDefinition, List<DiagnosticMessage> diagnostics)
{
var pe = new MemoryStream();
var pdb = new MemoryStream();
assemblyDefinition.Write(pe, new WriterParameters
{
SymbolWriterProvider = new PdbWriterProvider(),
SymbolStream = pdb,
WriteSymbols = true
});
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5f611b284dae01a46bc990b87b8bca90
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,107 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Mono.Cecil;
using Unity.CompilationPipeline.Common.ILPostProcessing;
namespace ShrinkEventBus.CodeGen
{
internal sealed class PostProcessorAssemblyResolver : IAssemblyResolver
{
private readonly string[] _references;
private readonly Dictionary<string, AssemblyDefinition> _cache = new();
private AssemblyDefinition? _self;
public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
{
_references = compiledAssembly.References;
}
public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition)
{
_self = assemblyDefinition;
}
public AssemblyDefinition? Resolve(AssemblyNameReference name)
{
return Resolve(name, new ReaderParameters(ReadingMode.Deferred));
}
public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters)
{
lock (_cache)
{
if (name.Name == _self?.Name.Name)
return _self;
var path = FindPath(name);
if (path == null)
return null;
var key = $"{path}{File.GetLastWriteTime(path)}";
if (_cache.TryGetValue(key, out var cached))
return cached;
parameters.AssemblyResolver = this;
var ms = ReadFileWithRetry(path);
var pdbPath = Path.ChangeExtension(path, ".pdb");
if (File.Exists(pdbPath))
parameters.SymbolStream = ReadFileWithRetry(pdbPath);
var assembly = AssemblyDefinition.ReadAssembly(ms, parameters);
_cache[key] = assembly;
return assembly;
}
}
private string? FindPath(AssemblyNameReference name)
{
foreach (var reference in _references)
{
if (Path.GetFileNameWithoutExtension(reference) == name.Name)
return reference;
}
var dirs = new HashSet<string>();
foreach (var reference in _references)
{
var dir = Path.GetDirectoryName(reference);
if (dir != null)
dirs.Add(dir);
}
foreach (var dir in dirs)
{
var candidate = Path.Combine(dir, $"{name.Name}.dll");
if (File.Exists(candidate))
return candidate;
}
return null;
}
private static MemoryStream ReadFileWithRetry(string path, int retries = 5)
{
for (var i = 0; i < retries; i++)
{
try
{
return new MemoryStream(File.ReadAllBytes(path));
}
catch (IOException) when (i < retries - 1)
{
Thread.Sleep(100);
}
}
throw new IOException($"[ShrinkEventBus.CodeGen] 无法读取文件: {path}");
}
public void Dispose()
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aeea50f82bab4ee2833c671d801c95e3
timeCreated: 1772908894
@@ -0,0 +1,37 @@
#nullable enable
using System.Linq;
using System.Reflection;
using Mono.Cecil;
namespace ShrinkEventBus.CodeGen
{
internal sealed class PostProcessorReflectionImporter : DefaultReflectionImporter
{
private const string CoreLibName = "System.Private.CoreLib";
private readonly AssemblyNameReference? _corlib;
public PostProcessorReflectionImporter(ModuleDefinition module)
: base(module)
{
_corlib = module.AssemblyReferences.FirstOrDefault(
assembly => assembly.Name is "mscorlib" or "netstandard");
}
public override AssemblyNameReference ImportReference(AssemblyName reference)
{
if (_corlib != null && reference.Name == CoreLibName)
return _corlib;
return base.ImportReference(reference);
}
}
internal sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider
{
public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
{
return new PostProcessorReflectionImporter(module);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0a6c2401798b42f0bbcde72796d2fa9c
timeCreated: 1772908906
@@ -0,0 +1,23 @@
{
"name": "Unity.ShrinkEventBus.CodeGen",
"rootNamespace": "ShrinkEventBus.CodeGen",
"references": [
"ShrinkEventBus.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"Mono.Cecil.dll",
"Mono.Cecil.Mdb.dll",
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 91161d4c2ae3ae74c9184ba33d9aa52e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8bf98940dc144bd2bffd55ce21295bbd
timeCreated: 1772878399
@@ -0,0 +1,793 @@
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace ShrinkEventBus.Editor
{
public class EventBusViewerWindow : EditorWindow
{
private enum LiveLogFilterMode
{
All,
HasListeners,
NoListeners
}
private sealed class HandlerDisplayData
{
public object TargetInstance { get; set; }
public MonoBehaviour PingTarget { get; set; }
public string TargetName { get; set; }
public string MethodName { get; set; }
public object Priority { get; set; }
public string ExecType { get; set; }
public bool IsStaticHandler { get; set; }
}
private sealed class EventLogRecord
{
public DateTime Timestamp { get; set; }
public string TimestampText { get; set; }
public string EventName { get; set; }
public string SenderInfo { get; set; }
public bool HasListeners { get; set; }
public bool IsCancelable { get; set; }
public bool IsCanceled { get; set; }
public string Result { get; set; }
public string Phase { get; set; }
public List<string> Parameters { get; set; }
public List<HandlerDisplayData> Handlers { get; set; }
public string MatchText { get; set; }
}
private sealed class EventLogGroup
{
public string EventName { get; set; }
public List<EventLogRecord> Records { get; } = new();
public EventLogRecord LatestRecord => Records[0];
}
private ScrollView _subscribersScrollView;
private ScrollView _liveTrackerScrollView;
private VisualElement _subscribersTab;
private VisualElement _liveTrackerTab;
private Label _instanceCountLabel;
private Label _liveLogStatsLabel;
private string _searchString = "";
private string _liveLogSearchString = "";
private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All;
private bool _collapseSameEventType = true;
private const int MaxLogCount = 100;
private const double LiveTrackerRefreshIntervalSeconds = 0.1d;
private readonly List<EventLogRecord> _logRecords = new();
private bool _isLiveTrackerDirty;
private double _nextLiveTrackerRefreshTime;
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
public static void ShowWindow()
{
var window = GetWindow<EventBusViewerWindow>("EventBus 控制台");
window.minSize = new Vector2(500, 600);
window.Show();
}
private void OnEnable()
{
EventBus.OnEventTriggeredForEditor += RecordEventLog;
EditorApplication.update += OnEditorUpdate;
}
private void OnDisable()
{
EventBus.OnEventTriggeredForEditor -= RecordEventLog;
EditorApplication.update -= OnEditorUpdate;
}
private void CreateGUI()
{
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Column;
root.Add(CreateHeader());
root.Add(CreateTabBar());
var contentContainer = new VisualElement { style = { flexGrow = 1 } };
_subscribersTab = CreateSubscribersTab();
_liveTrackerTab = CreateLiveTrackerTab();
_subscribersTab.style.display = DisplayStyle.Flex;
_liveTrackerTab.style.display = DisplayStyle.None;
contentContainer.Add(_subscribersTab);
contentContainer.Add(_liveTrackerTab);
root.Add(contentContainer);
RefreshSubscribersView();
root.schedule.Execute(() =>
{
_instanceCountLabel.text = Application.isPlaying
? $"活跃实例数: {EventBus.GetRegisteredInstanceCount()}"
: "游戏未运行";
}).Every(500);
}
private VisualElement CreateHeader()
{
var header = new VisualElement
{
style =
{
paddingTop = 10, paddingBottom = 10, paddingLeft = 10, paddingRight = 10,
backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1f)), borderBottomWidth = 1,
borderBottomColor = new StyleColor(Color.black)
}
};
header.Add(new Label("Shrink Event Bus 控制台")
{ style = { fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold } });
var debugToggle = new Toggle("启用 Debug 调试记录 (追踪实时事件)")
{ value = EventBus.EnableDebugRecord, style = { marginTop = 5 } };
debugToggle.RegisterValueChangedCallback(evt => EventBus.EnableDebugRecord = evt.newValue);
header.Add(debugToggle);
var infoRow = new VisualElement
{ style = { flexDirection = FlexDirection.Row, marginTop = 5, justifyContent = Justify.SpaceBetween } };
_instanceCountLabel = new Label("活跃实例数: 0") { style = { color = new StyleColor(Color.cyan) } };
infoRow.Add(_instanceCountLabel);
header.Add(infoRow);
return header;
}
private VisualElement CreateTabBar()
{
var toolbar = new Toolbar();
toolbar.Add(new ToolbarButton(() => SwitchTab(0))
{ text = "订阅者全览", style = { flexGrow = 1, unityTextAlign = TextAnchor.MiddleCenter } });
toolbar.Add(new ToolbarButton(() => SwitchTab(1))
{ text = "实时触发日志", style = { flexGrow = 1, unityTextAlign = TextAnchor.MiddleCenter } });
return toolbar;
}
private VisualElement CreateSubscribersTab()
{
var container = new VisualElement { style = { flexGrow = 1 } };
var toolbar = new Toolbar();
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
searchField.RegisterValueChangedCallback(evt =>
{
_searchString = evt.newValue;
RefreshSubscribersView();
});
toolbar.Add(searchField);
toolbar.Add(new ToolbarButton(RefreshSubscribersView) { text = "刷新视图" });
container.Add(toolbar);
_subscribersScrollView = new ScrollView
{ style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5, paddingTop = 5 } };
container.Add(_subscribersScrollView);
return container;
}
private VisualElement CreateLiveTrackerTab()
{
var container = new VisualElement { style = { flexGrow = 1 } };
var toolbar = new Toolbar();
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
searchField.RegisterValueChangedCallback(evt =>
{
_liveLogSearchString = evt.newValue ?? string.Empty;
RequestLiveTrackerRefresh(true);
});
toolbar.Add(searchField);
var filterMenu = new ToolbarMenu { text = "全部事件" };
filterMenu.menu.AppendAction("全部事件", _ =>
{
_liveLogFilterMode = LiveLogFilterMode.All;
filterMenu.text = "全部事件";
RequestLiveTrackerRefresh(true);
});
filterMenu.menu.AppendAction("仅有监听者", _ =>
{
_liveLogFilterMode = LiveLogFilterMode.HasListeners;
filterMenu.text = "仅有监听者";
RequestLiveTrackerRefresh(true);
});
filterMenu.menu.AppendAction("仅无监听者", _ =>
{
_liveLogFilterMode = LiveLogFilterMode.NoListeners;
filterMenu.text = "仅无监听者";
RequestLiveTrackerRefresh(true);
});
toolbar.Add(filterMenu);
var collapseToggle = new ToolbarToggle
{
text = "折叠同类事件",
value = _collapseSameEventType
};
collapseToggle.RegisterValueChangedCallback(evt =>
{
_collapseSameEventType = evt.newValue;
RequestLiveTrackerRefresh(true);
});
toolbar.Add(collapseToggle);
toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" });
container.Add(toolbar);
_liveLogStatsLabel = new Label
{
style =
{
marginLeft = 8,
marginRight = 8,
marginTop = 4,
marginBottom = 4,
color = new StyleColor(Color.gray)
}
};
container.Add(_liveLogStatsLabel);
_liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } };
container.Add(_liveTrackerScrollView);
RequestLiveTrackerRefresh(true);
return container;
}
private void SwitchTab(int index)
{
_subscribersTab.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None;
_liveTrackerTab.style.display = index == 1 ? DisplayStyle.Flex : DisplayStyle.None;
if (index == 0)
{
RefreshSubscribersView();
return;
}
RequestLiveTrackerRefresh(true);
}
private void OnEditorUpdate()
{
if (!_isLiveTrackerDirty) return;
if (_liveTrackerTab == null || _liveTrackerTab.style.display != DisplayStyle.Flex) return;
if (EditorApplication.timeSinceStartup < _nextLiveTrackerRefreshTime) return;
RefreshLiveTrackerView();
}
private void RequestLiveTrackerRefresh(bool immediate = false)
{
_isLiveTrackerDirty = true;
if (immediate)
{
_nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup;
if (_liveTrackerTab != null && _liveTrackerTab.style.display == DisplayStyle.Flex)
RefreshLiveTrackerView();
}
}
private void RefreshSubscribersView()
{
_subscribersScrollView.Clear();
if (!Application.isPlaying)
{
_subscribersScrollView.Add(new HelpBox("请先运行游戏,系统初始化后即可查看内存中的事件树。", HelpBoxMessageType.Info));
return;
}
var snapshots = EventBus.GetAllSubscribersSnapshot();
foreach (var entry in snapshots)
{
var eventType = entry.Key;
if (!string.IsNullOrEmpty(_searchString) &&
!eventType.Name.ToLower().Contains(_searchString.ToLower())) continue;
var handlers = entry.Value;
var foldout = new Foldout
{
text = $"{eventType.Name} (挂载数: {handlers.Length})",
style =
{
backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.5f)),
marginTop = 2,
paddingBottom = 2,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f))
}
};
foreach (var h in handlers) foldout.Add(CreateHandlerRow(h));
_subscribersScrollView.Add(foldout);
}
}
private void RecordEventLog(EventBase evt, string eventName, string senderInfo, EventHandlerInfo[] handlers)
{
if (!EventBus.EnableDebugRecord) return;
_logRecords.Add(CreateEventLogRecord(evt, eventName, senderInfo, handlers));
if (_logRecords.Count > MaxLogCount)
_logRecords.RemoveAt(0);
RequestLiveTrackerRefresh();
}
private EventLogRecord CreateEventLogRecord(EventBase evt, string eventName, string senderInfo,
EventHandlerInfo[] handlers)
{
var timestamp = DateTime.Now;
var hasListeners = handlers.Length > 0;
var paramsList = DumpEventParams(evt);
var handlerRows = new List<HandlerDisplayData>(handlers.Length);
foreach (var handler in handlers)
handlerRows.Add(ExtractHandlerDisplayData(handler));
var isCancelable = (GetMemberValue(evt, "IsCancelable") as bool?) ?? false;
var isCanceled = (GetMemberValue(evt, "IsCanceled") as bool?) ?? false;
var resultStr = GetMemberValue(evt, "Result")?.ToString() ?? "DEFAULT";
var phaseStr = GetMemberValue(evt, "Phase")?.ToString() ?? "NORMAL";
return new EventLogRecord
{
Timestamp = timestamp,
TimestampText = timestamp.ToString("HH:mm:ss.fff"),
EventName = eventName,
SenderInfo = senderInfo,
HasListeners = hasListeners,
IsCancelable = isCancelable,
IsCanceled = isCanceled,
Result = resultStr,
Phase = phaseStr,
Parameters = paramsList,
Handlers = handlerRows,
MatchText = BuildLogMatchText(eventName, senderInfo, paramsList)
};
}
private string BuildLogMatchText(string eventName, string senderInfo, List<string> paramsList)
{
if (paramsList.Count == 0)
return $"{eventName}\n{senderInfo}";
return $"{eventName}\n{senderInfo}\n{string.Join("\n", paramsList)}";
}
private void RefreshLiveTrackerView()
{
if (_liveTrackerScrollView == null) return;
_isLiveTrackerDirty = false;
_nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup + LiveTrackerRefreshIntervalSeconds;
_liveTrackerScrollView.Clear();
var filteredRecords = new List<EventLogRecord>(_logRecords.Count);
for (var i = _logRecords.Count - 1; i >= 0; i--)
{
var record = _logRecords[i];
if (MatchesLiveLogFilter(record))
filteredRecords.Add(record);
}
if (_liveLogStatsLabel != null)
{
_liveLogStatsLabel.text = _collapseSameEventType
? $"显示 {filteredRecords.Count} / {_logRecords.Count} 条,按事件类型折叠"
: $"显示 {filteredRecords.Count} / {_logRecords.Count} 条";
}
if (filteredRecords.Count == 0)
{
_liveTrackerScrollView.Add(new HelpBox("当前没有符合条件的实时事件日志。", HelpBoxMessageType.Info));
return;
}
if (_collapseSameEventType)
{
foreach (var group in BuildCollapsedGroups(filteredRecords))
_liveTrackerScrollView.Add(CreateCollapsedGroupElement(group));
return;
}
foreach (var record in filteredRecords)
_liveTrackerScrollView.Add(CreateLogEntryElement(record));
}
private bool MatchesLiveLogFilter(EventLogRecord record)
{
switch (_liveLogFilterMode)
{
case LiveLogFilterMode.HasListeners:
if (!record.HasListeners) return false;
break;
case LiveLogFilterMode.NoListeners:
if (record.HasListeners) return false;
break;
}
if (string.IsNullOrWhiteSpace(_liveLogSearchString))
return true;
return record.MatchText.IndexOf(_liveLogSearchString.Trim(), StringComparison.OrdinalIgnoreCase) >= 0;
}
private List<EventLogGroup> BuildCollapsedGroups(List<EventLogRecord> records)
{
var groups = new List<EventLogGroup>();
var groupMap = new Dictionary<string, EventLogGroup>(StringComparer.Ordinal);
foreach (var record in records)
{
if (!groupMap.TryGetValue(record.EventName, out var group))
{
group = new EventLogGroup { EventName = record.EventName };
groupMap[record.EventName] = group;
groups.Add(group);
}
group.Records.Add(record);
}
return groups;
}
private VisualElement CreateCollapsedGroupElement(EventLogGroup group)
{
var latest = group.LatestRecord;
var foldout = new Foldout
{
text = $"{group.EventName} × {group.Records.Count} 最近: {latest.TimestampText}",
value = group.Records.Count == 1,
style =
{
backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.4f)),
marginTop = 2,
paddingBottom = 2,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f))
}
};
var summary = new Label(
$"最近来源: {latest.SenderInfo} | 监听者: {(latest.HasListeners ? latest.Handlers.Count : 0)} | 已取消: {(latest.IsCanceled ? "" : "")}")
{
style =
{
marginLeft = 18,
marginBottom = 4,
color = new StyleColor(Color.gray)
}
};
foldout.Add(summary);
if (foldout.value)
PopulateCollapsedGroupDetails(foldout, group, 1);
foldout.RegisterValueChangedCallback(evt =>
{
if (evt.newValue)
{
PopulateCollapsedGroupDetails(foldout, group, 1);
return;
}
ClearFoldoutChildren(foldout, 1);
});
return foldout;
}
private void PopulateCollapsedGroupDetails(Foldout foldout, EventLogGroup group, int keepChildCount)
{
if (foldout.childCount > keepChildCount) return;
foreach (var record in group.Records)
{
var row = CreateLogEntryElement(record);
row.style.marginLeft = 12;
foldout.Add(row);
}
}
private void ClearFoldoutChildren(Foldout foldout, int keepChildCount)
{
while (foldout.childCount > keepChildCount)
foldout.RemoveAt(foldout.childCount - 1);
}
private VisualElement CreateLogEntryElement(EventLogRecord record)
{
var logEntry = new VisualElement
{
style =
{
flexDirection = FlexDirection.Column,
paddingBottom = 4,
paddingTop = 4,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
}
};
var headerRow = new VisualElement
{ style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
headerRow.Add(new Label(record.TimestampText)
{ style = { width = 80, color = new StyleColor(Color.gray) } });
headerRow.Add(new Label(record.EventName)
{
style = { color = new StyleColor(new Color(1f, 0.84f, 0f)), unityFontStyleAndWeight = FontStyle.Bold }
});
var senderClassName = GetSenderClassName(record.SenderInfo);
var senderLabel = new Label($" [由 {record.SenderInfo} 触发]")
{
style =
{
color = new StyleColor(new Color(0.5f, 0.8f, 1f)),
marginLeft = 5,
unityFontStyleAndWeight = FontStyle.Italic
}
};
if (senderClassName != "未知来源" && senderClassName != "EventBus" && senderClassName != "Unknown")
{
senderLabel.RegisterCallback<MouseEnterEvent>(_ =>
senderLabel.style.color = new StyleColor(Color.cyan));
senderLabel.RegisterCallback<MouseLeaveEvent>(_ =>
senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f)));
senderLabel.RegisterCallback<ClickEvent>(_ => OpenScriptByClassName(senderClassName));
}
headerRow.Add(senderLabel);
logEntry.Add(headerRow);
var statusRow = new VisualElement
{ style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } };
statusRow.Add(new Label($"可取消: {(record.IsCancelable ? "" : "")}")
{ style = { color = new StyleColor(Color.gray), marginRight = 15 } });
var canceledLabel = new Label($"已取消: {(record.IsCanceled ? "" : "")}")
{ style = { marginRight = 15 } };
canceledLabel.style.color =
record.IsCanceled ? new StyleColor(new Color(1f, 0.3f, 0.3f)) : new StyleColor(Color.gray);
statusRow.Add(canceledLabel);
statusRow.Add(new Label($"Result: {record.Result}")
{ style = { color = new StyleColor(new Color(0.8f, 0.6f, 0.8f)), marginRight = 15 } });
statusRow.Add(new Label($"最后阶段: {record.Phase}")
{ style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } });
logEntry.Add(statusRow);
var paramsContainer = new VisualElement
{ style = { flexDirection = FlexDirection.Column, marginLeft = 80, marginTop = 2, marginBottom = 2 } };
if (record.Parameters.Count == 0)
{
paramsContainer.Add(new Label("参数: { 无 }")
{ style = { color = new StyleColor(new Color(0.5f, 0.5f, 0.5f)) } });
}
else
{
paramsContainer.Add(
new Label("携带参数:") { style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } });
foreach (var pStr in record.Parameters)
paramsContainer.Add(new Label($" • {pStr}")
{ style = { color = new StyleColor(new Color(0.6f, 0.9f, 0.6f)), marginLeft = 10 } });
}
logEntry.Add(paramsContainer);
if (record.HasListeners)
{
foreach (var handler in record.Handlers)
logEntry.Add(CreateHandlerRow(handler, 80));
}
else
{
var noHandlerRow = new VisualElement
{ style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } };
noHandlerRow.Add(new Label("(无监听者响应)") { style = { color = new StyleColor(Color.gray) } });
logEntry.Add(noHandlerRow);
}
return logEntry;
}
private string GetSenderClassName(string senderInfo)
{
if (string.IsNullOrWhiteSpace(senderInfo))
return "未知来源";
return senderInfo.Contains(".") ? senderInfo.Split('.')[0] : senderInfo;
}
private VisualElement CreateHandlerRow(object h)
{
return CreateHandlerRow(ExtractHandlerDisplayData(h), 40);
}
private VisualElement CreateHandlerRow(HandlerDisplayData handlerData, float marginLeft)
{
var targetInstance = handlerData.TargetInstance;
var targetName = handlerData.TargetName;
var methodName = handlerData.MethodName;
var priority = handlerData.Priority;
var execType = handlerData.ExecType;
var row = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row, marginTop = 2, marginBottom = 2, marginLeft = marginLeft,
alignItems = Align.Center
}
};
row.Add(new Label($"[{priority}]") { style = { width = 75, color = new StyleColor(Color.gray) } });
row.Add(new Label($"[{execType}]")
{ style = { width = 50, color = new StyleColor(new Color(0.8f, 0.6f, 0.2f)) } });
var linkLabel = new Label($"{targetName}.{methodName}()")
{ style = { color = new StyleColor(new Color(0.3f, 0.6f, 1f)) } };
linkLabel.RegisterCallback<MouseEnterEvent>(e => linkLabel.style.color = new StyleColor(Color.cyan));
linkLabel.RegisterCallback<MouseLeaveEvent>(e =>
linkLabel.style.color = new StyleColor(new Color(0.3f, 0.6f, 1f)));
linkLabel.RegisterCallback<ClickEvent>(e => OpenScriptByClassName(targetName));
linkLabel.style.flexGrow = 1;
row.Add(linkLabel);
if (handlerData.PingTarget)
{
var pingBtn = new Button(() =>
{
EditorGUIUtility.PingObject(handlerData.PingTarget.gameObject);
Selection.activeGameObject = handlerData.PingTarget.gameObject;
}) { text = "Ping", style = { width = 50, height = 18 } };
row.Add(pingBtn);
}
else if (handlerData.IsStaticHandler && methodName != "未知方法")
row.Add(new Label("(静态)")
{
style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight }
});
else
row.Add(new Label("(无对象)")
{
style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight }
});
return row;
}
private HandlerDisplayData ExtractHandlerDisplayData(object handlerObj)
{
var priority = GetMemberValue(handlerObj, "Priority") ?? "NORMAL";
var debugInfo = GetMemberValue(handlerObj, "DebugInfo") as string ?? "未知方法";
var execType = "Sync";
if (debugInfo.Contains("(UniTask)") || debugInfo.Contains("Async")) execType = "Async";
else if (debugInfo.Contains("(Task->UniTask)") || debugInfo.Contains("(Task)")) execType = "Task";
object targetInstance = null;
var targetName = "未知类";
var methodName = debugInfo;
var handlerDelegate = GetMemberValue(handlerObj, "Handler") as Delegate;
if (handlerDelegate != null)
{
targetInstance = handlerDelegate.Target;
var methodInfo = handlerDelegate.Method;
if (targetInstance != null && targetInstance.GetType().Name.Contains("Wrapper"))
{
var realDelegate = GetMemberValue(targetInstance, "_originalAction") as Delegate;
if (realDelegate != null)
{
targetInstance = realDelegate.Target;
methodInfo = realDelegate.Method;
}
}
if (methodInfo != null)
{
var rawClassName = methodInfo.DeclaringType?.Name ?? "未知类";
if (rawClassName.Contains("<>c")) rawClassName = "匿名闭包类";
targetName = rawClassName;
methodName = methodInfo.Name;
}
}
return new HandlerDisplayData
{
TargetInstance = targetInstance,
PingTarget = targetInstance as MonoBehaviour,
TargetName = targetName,
MethodName = methodName,
Priority = priority,
ExecType = execType,
IsStaticHandler = targetInstance == null
};
}
private List<string> DumpEventParams(EventBase evt)
{
var values = new List<string>();
if (evt == null) return values;
var currentType = evt.GetType();
while (currentType != null && currentType != typeof(EventBase) && currentType != typeof(object))
{
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance |
BindingFlags.DeclaredOnly;
foreach (var f in currentType.GetFields(flags))
{
if (f.Name.Contains("<") || f.Name.Contains(">")) continue;
if (f.Name is "CurrentHandler" or "Phase") continue;
values.Add($"{f.Name} = {FormatValue(f.GetValue(evt))}");
}
foreach (var p in currentType.GetProperties(flags))
{
if (p.Name is "CurrentHandler" or "Phase" or "IsCanceled" or "IsCancelable" or "Result") continue;
if (!p.CanRead || p.GetIndexParameters().Length > 0) continue;
try
{
values.Add($"{p.Name} = {FormatValue(p.GetValue(evt))}");
}
catch
{
// ignored
}
}
currentType = currentType.BaseType;
}
return values;
}
private string FormatValue(object val)
{
if (val == null) return "null";
if (val is UnityEngine.Object uObj) return uObj ? uObj.name : "null(已销毁)";
if (val is string s) return $"\"{s}\"";
if (val is Array arr) return $"[数组 Length:{arr.Length}]";
return val.ToString();
}
private object GetMemberValue(object obj, string memberName)
{
if (obj == null) return null;
var type = obj.GetType();
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var prop = type.GetProperty(memberName, flags);
if (prop != null) return prop.GetValue(obj);
var field = type.GetField(memberName, flags);
if (field != null) return field.GetValue(obj);
return null;
}
private void ClearLogs()
{
_logRecords.Clear();
RequestLiveTrackerRefresh(true);
}
private void OpenScriptByClassName(string className)
{
if (className == "匿名闭包类") return;
var guids = AssetDatabase.FindAssets($"{className} t:MonoScript");
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (path.EndsWith($"{className}.cs"))
{
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
if (script)
{
AssetDatabase.OpenAsset(script);
return;
}
}
}
}
}
}
#endif
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16e1b18e2a2748e494f2ba648b4697a8
timeCreated: 1772878417
@@ -0,0 +1,13 @@
{
"name": "ShrinkEventBus.Editor",
"rootNamespace": "ShrinkEventBus.Editor",
"references": [
"ShrinkEventBus.Runtime",
"UniTask"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0a89bdb25764f1f8a8e4dcc915abe39
timeCreated: 1772900825
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Eicy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a726eae075081a24785e6f3f4a4efe6d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+629
View File
@@ -0,0 +1,629 @@
# ShrinkEventBus
一个为 Unity C# 项目设计的高性能、类型安全事件总线系统。支持优先级调度、编译期自动注册、同步/异步混合处理,以及完整的 MonoBehaviour 生命周期管理。
## ✨ 特性概览
| 特性 | 说明 |
|------|------|
| 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 |
| ⚡ **高性能热路径** | 注册期预编译 invoker(无反射调用、无装箱),派发走缓存快照数组,监听者快照零拷贝挂接 |
| 🤖 **零侵入自动注册** | 标记 `[EventBusSubscriber]` 即可,ILPostProcessor 编译期自动织入注册与反注册逻辑,动态创建的对象也无需手写任何代码 |
| 🧾 **静态订阅清单** | 静态 `[EventBusSubscriber]` 现可通过编译期注册表收口,避免默认总线启动时全域扫描所有类型 |
| 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 |
| 🔄 **同步 & 异步** | 统一支持 `Action`(同步)与 `UniTask`(异步)两种 handler 形式 |
| 🧵 **线程安全** | 注册/注销操作全程加锁保护 |
| 📦 **对象池** | 内置 `EventPool<T>`,高频事件零 GC |
| 🔍 **调试友好** | Editor 事件查看器实时追踪订阅者与触发日志 |
| 🧩 **可实例化总线** | 除默认静态 `EventBus` 外,也可以用 Builder 创建独立 bus,并按需要配置异常策略、事件类型约束、分 phase 分发 |
| 🌳 **父事件监听** | 监听父事件类型时,子事件触发也会命中父事件监听器,便于做 Pre/Post 家族事件和统一监控 |
## 👓 Benchmark
[Benchmark结果](Benchmark.txt)
运行时仓库里自带 `EventBusBenchmark` 组件,当前会分别覆盖这些场景:
- 无订阅者 / 单订阅者 / 多订阅者同步触发
- 父事件监听子事件
-`EventPriority` 分 phase 分发
- 单订阅者异步触发
- 手工 delegate 注册 / 注销
- `object / Type / MethodInfo` 扫描注册 / 注销
- `EventPool<T>``new`
- 已取消事件跳过
如果你在评估这次 `IShrinkEventBus`、继承监听和严格注册带来的成本变化,优先看这个组件的输出,而不是只看 `Benchmark.txt` 里的旧样本。
## 📦 依赖
- Unity 2022.3+
- [UniTask](https://github.com/Cysharp/UniTask) `2.x`
## ⚙️ 安装
在项目的 `Packages/manifest.json` 中添加:
```json
{
"dependencies": {
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
"com.cneicy.shrink-eventbus": "https://github.com/cneicy/ShrinkEventBus.git"
}
}
```
或通过 Package Manager → `+``Add package from git URL` 输入:
```
https://github.com/cneicy/ShrinkEventBus.git
```
> ⚠️ **自动织入依赖说明**:当前工作区同时支持两层织入。若当前程序集命中共享管线 `ShrinkShared.CodeGen` 的覆盖范围(例如同时引用 `ShrinkCommand.Runtime` / `ShrinkNetwork.Runtime` / `ShrinkApp.Core.Runtime`),则优先由共享管线处理;其余只引用 `ShrinkEventBus.Runtime` 的程序集由模块内 `CodeGen/` 本地 ILPostProcessor 兜底。独立安装本包但未带上 `CodeGen/` 或共享管线时,没有编译期织入,请改用手动接入:MonoBehaviour 在 `Awake`/`OnDestroy` 中调用 `EventBus.AutoRegister(this)` / `EventBus.UnregisterInstance(this)`,或使用 `SubscribeEvent` 句柄。
## 🚀 快速上手
### 第一步:定义事件
所有事件必须继承 `EventBase`,通过 Attribute 声明附加能力:
```csharp
// 普通事件
public class PlayerDiedEvent : EventBase
{
public int PlayerId { get; set; }
public string Cause { get; set; }
}
// 可取消事件
[Cancelable]
public class PlayerMoveEvent : EventBase
{
public Vector3 OldPosition { get; set; }
public Vector3 NewPosition { get; set; }
}
// 有返回结果的事件
[HasResult]
public class ItemPickupEvent : EventBase
{
public string ItemId { get; set; }
public GameObject Picker { get; set; }
}
```
### 第二步:订阅事件
在 MonoBehaviour 上标记 `[EventBusSubscriber]`,用 `[EventSubscribe]` 标记处理方法。
**无论是场景初始时存在的对象,还是运行时动态 `Instantiate` 的对象,都会在 `Awake` 时自动完成注册,销毁时自动清理,无需手写任何注册代码。**
```csharp
[EventBusSubscriber]
public class UIManager : MonoBehaviour
{
// 同步处理
[EventSubscribe(EventPriority.NORMAL)]
private void OnPlayerDied(PlayerDiedEvent evt)
{
ShowDeathScreen(evt.PlayerId);
}
// 异步处理(UniTask
[EventSubscribe(EventPriority.HIGH)]
private async UniTask OnItemPickup(ItemPickupEvent evt)
{
await PlayPickupAnimation(evt.ItemId);
}
}
```
### 第三步:触发事件
```csharp
// 同步触发
EventBus.TriggerEvent(new PlayerDiedEvent { PlayerId = 1, Cause = "Fall" });
// 异步触发(顺序等待每个 handler)
await EventBus.TriggerEventAsync(new PlayerMoveEvent
{
OldPosition = transform.position,
NewPosition = targetPos
});
// 使用对象池(高频场景推荐)
using var evt = EventPool<PlayerDiedEvent>.Get();
evt.PlayerId = 1;
EventBus.TriggerEvent(evt);
// using 块结束时自动归还到池中
```
## 🖼️ 追踪图形化
菜单栏 → `ShrinkSDK``事件总线``事件查看器`
![订阅者全览](img1.png)
![实时触发日志](img2.png)
实时触发日志页现已支持关键词过滤、按“有监听者 / 无监听者”筛选,以及“折叠同类事件”聚合查看,适合排查高频事件刷屏场景。
---
## 📖 核心概念
### 自动注册机制
ShrinkEventBus 通过 **ILPostProcessor** 在编译期自动处理完整的生命周期管理。当 Unity 编译代码时,所有标记了 `[EventBusSubscriber]` 且自身或基类链上存在实例 `[EventSubscribe]` 方法的 MonoBehaviour 子类会被自动识别,并在其 `Awake``OnDestroy` 方法中分别织入注册与反注册逻辑(没有任何实例订阅方法的类型会被跳过,不织入也不报错)。
当前织入策略如下:
- 命中共享管线 `ShrinkShared.CodeGen` 覆盖范围的程序集,优先由共享管线处理。
- 其余只引用 `ShrinkEventBus.Runtime` 的程序集,由模块内 `CodeGen/Editor/EventBusILPostProcessor.cs` 本地处理。
- 因此 ShrinkSDK 工作区内的统一 CodeGen 与独立 `ShrinkEventBus` 业务程序集可以同时兼容,且不会双重织入。
织入规则如下:
- 类**自身已有** `Awake`/`OnDestroy`:在方法头部插入,用户自己负责 `base` 调用
- 类**没有**,但**基类有虚方法**:生成 `protected override` 并自动调用 `base` 方法,`Awake` 顺序为 `base.Awake() → AutoRegister``OnDestroy` 顺序为 `UnregisterInstance → base.OnDestroy()`
- 类**没有**,基类**也没有**:生成私有方法并插入
这意味着:
- 场景初始加载的对象 → `Awake` 执行时自动注册
- 运行时 `Instantiate` 的对象 → `Awake` 执行时自动注册
- GameObject 销毁时 → `OnDestroy` 执行时自动反注册,无内存泄漏
**整个过程对业务代码完全透明,类里不需要写任何注册相关的代码。**
### 实例化总线与 Builder
默认情况下,项目继续使用全局静态门面 `EventBus`。如果你需要更清晰的模块边界,也可以创建独立 bus:
```csharp
var gameplayBus = EventBus.CreateBus(builder => builder
.AllowPerPhaseDispatch()
.SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndThrow));
gameplayBus.Register(new GameplaySubscribers());
gameplayBus.TriggerEvent(new PlayerDiedEvent { PlayerId = 1, Cause = "Fall" });
```
当前 Builder 支持的重点配置:
- `SetExceptionHandlingMode(...)`
- `SetExceptionHandler(...)`
- `AllowPerPhaseDispatch()`
- `CheckTypesOnDispatch()`
- `MarkerInterface<TMarker>()`
- `ClassChecker(...)`
- `StartShutdown()`
如果你只是想继续沿用旧习惯,直接用静态 `EventBus` 即可;它内部就是一个默认的 `IShrinkEventBus` 实例。
### 手动注册的新边界
除了 `AutoRegister(this)` / `[EventBusSubscriber]` 这条 Unity 友好的自动接入路径,现在也支持更显式的手动注册:
```csharp
// 扫描实例上的 [EventSubscribe] 方法
EventBus.Register(mySubscriberInstance);
// 扫描某个类型上的 static [EventSubscribe] 方法
EventBus.Register(typeof(GlobalEventHooks));
// 只注册某一个 static [EventSubscribe] 方法
EventBus.Register(typeof(GlobalEventHooks).GetMethod("OnPlayerDied",
BindingFlags.Static | BindingFlags.NonPublic));
```
和旧版本相比,手动注册现在会更严格:
- 方法必须带 `[EventSubscribe]`
- 只能有一个参数
- 参数必须继承 `EventBase`
- 返回值只能是 `void``UniTask`
- 实例注册只接受实例方法,类型/方法注册只接受静态方法
这样做的目的是把“为什么没触发”尽量提前到注册阶段暴露,而不是静默吞掉。
### 优先级系统
`EventPriority` 枚举定义了六个优先级档位,数值越小越先执行:
```
HIGHEST(0) → HIGH(1) → NORMAL(2) → LOW(3) → LOWEST(4) → MONITOR(5)
```
同一优先级档位内,可用数字优先级进一步细排(数字越大越先执行):
```csharp
// 枚举优先级
[EventSubscribe(EventPriority.HIGH)]
private void Handler(SomeEvent evt) { }
// 数字优先级(自动映射到枚举档位;手动注册时必须显式传入数字)
EventBus.RegisterEvent<SomeEvent>(Handler, priority: 75); // 映射为 HIGH
// 手动注册时混合使用
EventBus.RegisterEvent<SomeEvent>(Handler, EventPriority.HIGH, receiveCanceled: false);
```
数字到枚举的映射规则:
| 数字范围 | 枚举档位 |
|---------|---------|
| ≥ 100 | HIGHEST |
| ≥ 50 | HIGH |
| ≥ 0 | NORMAL |
| ≥ -50 | LOW |
| < -50 | LOWEST |
> 1.3.0 起:数字 `0` 映射到 `NORMAL`(与枚举重载默认值一致);int 重载不再提供默认值,不带优先级的 `RegisterEvent(handler)` 调用唯一解析到枚举重载(NORMAL)。
**推荐的优先级分工:**
```
HIGHEST — 权限校验、合法性检查
HIGH — 核心业务逻辑、数值计算
NORMAL — 默认行为、状态变更
LOW — UI 更新、音效、特效
LOWEST — 收尾清理
MONITOR — 日志、统计、监控(通常配合 receiveCanceled: true
```
如果你创建的 bus 开启了 `AllowPerPhaseDispatch()`,也可以只分发某一个 phase
```csharp
gameplayBus.TriggerEvent(EventPriority.HIGH, evt);
await gameplayBus.TriggerEventAsync(EventPriority.MONITOR, evt);
```
这个模式主要适合做框架级流水线控制;普通业务仍推荐直接走完整分发。
### 父事件监听
现在监听父事件时,子事件触发也会命中父事件监听器:
```csharp
public class DamageEvent : EventBase
{
public int Value { get; set; }
}
public sealed class CriticalDamageEvent : DamageEvent
{
public bool IsCritical { get; set; }
}
EventBus.RegisterEvent<DamageEvent>(OnAnyDamage, EventPriority.MONITOR, receiveCanceled: true);
EventBus.TriggerEvent(new CriticalDamageEvent { Value = 42, IsCritical = true });
```
这很适合做统一日志、统一权限检查、事件族级别的监控和桥接。
### 事件取消与结果
```csharp
// 取消事件(需标记 [Cancelable]
[EventSubscribe(EventPriority.HIGHEST)]
private void ValidateMove(PlayerMoveEvent evt)
{
if (!IsValidPosition(evt.NewPosition))
evt.SetCanceled(true); // 后续未设置 receiveCanceled: true 的 handler 将跳过
}
// 监控处理器可以接收已取消的事件
[EventSubscribe(EventPriority.MONITOR, receiveCanceled: true)]
private void LogMove(PlayerMoveEvent evt)
{
Debug.Log($"移动 {(evt.IsCanceled ? "" : "")}");
}
// 触发方检查取消状态
var moveEvent = new PlayerMoveEvent { ... };
await EventBus.TriggerEventAsync(moveEvent);
if (!moveEvent.IsCanceled)
transform.position = moveEvent.NewPosition;
```
```csharp
// 设置结果(需标记 [HasResult]
[EventSubscribe(EventPriority.HIGH)]
private void CheckPermission(ItemPickupEvent evt)
{
evt.SetResult(player.HasSpace ? EventResult.ALLOW : EventResult.DENY);
}
// 触发方读取结果
var pickupEvent = new ItemPickupEvent { ... };
EventBus.TriggerEvent(pickupEvent);
bool success = pickupEvent.Result switch
{
EventResult.ALLOW => true,
EventResult.DENY => false,
EventResult.DEFAULT => DefaultPickupLogic()
};
```
### 注册方式对比
| 方式 | 适用场景 | 自动反注册 |
|------|---------|-----------|
| `[EventBusSubscriber]` + `[EventSubscribe]` | MonoBehaviour(推荐) | ✅ ILP 织入 OnDestroy,随 GameObject 销毁自动清理 |
| `EventBus.SubscribeEvent(...)` 手动订阅 | 非 MonoBehaviour 类、Lambda | ✅ `Dispose()` 即可精准清理 |
| `EventBus.RegisterEvent(...)` 手动注册 | 兼容旧代码 | ❌ 需手动调用 `UnregisterEvent` |
| `EventBus.AutoRegister(this)` | 特殊场景下手动触发 | ❌ 需手动调用 `UnregisterInstance` |
**手动注册示例(非 MonoBehaviour):**
```csharp
public class InventorySystem : IDisposable
{
private readonly IShrinkEventSubscription _itemPickupSubscription;
public InventorySystem()
{
_itemPickupSubscription = EventBus.SubscribeEvent<ItemPickupEvent>(OnItemPickup, EventPriority.NORMAL);
}
private void OnItemPickup(ItemPickupEvent evt) { /* ... */ }
public void Dispose()
{
_itemPickupSubscription.Dispose();
}
}
```
---
## 🔧 API 参考
### EventBus(静态门面)
#### 注册 / 注销
```csharp
// 同步 handler
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, int priority);
EventBus.SubscribeEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
EventBus.SubscribeEvent<TEvent>(Action<TEvent> handler, int priority);
// 异步 handlerUniTask
EventBus.RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
EventBus.SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
// 注销
EventBus.UnregisterEvent<TEvent>(Action<TEvent> handler);
EventBus.UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler);
EventBus.UnregisterAllEventsForObject(object target); // 注销某实例的全部 handler
EventBus.ClearAllSubscribersForEvent<TEvent>(); // 清空某事件的全部订阅者
EventBus.UnregisterAllEvents(); // 全部清空(谨慎使用)
```
#### 触发
```csharp
// 同步触发:只同步等待 sync handlerasync handler 会基于事件快照 fire-and-forget
bool handled = EventBus.TriggerEvent<TEvent>(TEvent eventArgs);
// 异步触发:顺序 await 每个 handler
bool handled = await EventBus.TriggerEventAsync<TEvent>(TEvent eventArgs);
```
> ⚠️ `TriggerEvent` 中遇到 async handler 时,不会等待其完成,而是对当前事件做一份快照后异步执行。如果你需要让 async handler 参与最终状态(如 `IsCanceled` / `Result` / 后续字段改写),请使用 `TriggerEventAsync`。
#### 查询
```csharp
EventBus.IsInstanceRegistered(object target);
EventBus.GetRegisteredInstanceCount();
EventBus.GetRegisteredEventTypeCount();
EventBus.GetEventSubscribers<TEvent>(); // 返回 EventHandlerInfo[]
EventBus.GetListenerList<TEvent>(); // 无订阅者时返回 null
EventBus.GetActiveSubscriptionsSnapshot();// 返回 IDisposable 订阅快照
```
### EventPool\<T\>
```csharp
// 从池中取出(自动重置状态)
var evt = EventPool<MyEvent>.Get();
// 手动归还
EventPool<MyEvent>.Release(evt);
// 推荐:配合 using 自动归还
using var evt = EventPool<MyEvent>.Get();
EventBus.TriggerEvent(evt);
// 作用域结束时调用 Dispose() → 自动归还
```
> ⚠️ 归还后不要再访问 `evt` 的属性,对象已被重置并放回池中。
### EventBase 关键成员
```csharp
evt.EventId // Guid,每次派发唯一(懒生成,首次访问时分配)
evt.EventTime // 事件创建时间(UTC
evt.IsCancelable // 是否支持取消(由 [Cancelable] 决定)
evt.HasResult // 是否支持结果(由 [HasResult] 决定)
evt.IsCanceled // 是否已被取消
evt.Result // 当前结果(EventResult 枚举)
evt.Phase // 当前执行到的优先级阶段
evt.CurrentHandler // 当前正在执行的 handler 信息
evt.GetSubscribers() // 获取本次派发的 handler 快照拷贝(调试用)
```
---
## 🏗️ 架构说明
```
ShrinkEventBus
├── Runtime/
│ ├── EventBus 静态门面,内部是一个默认 IShrinkEventBus 实例
│ ├── ShrinkEventBusInstance 总线实现:注册、派发、异常策略、phase 分发
│ ├── ShrinkEventBusBuilder 实例总线的构建与配置入口
│ ├── ListenerList 按 phase 分桶的有序 handler 列表,带快照缓存与父链合并
│ ├── EventHandlerInfo 单个 handler 的元信息(优先级、预编译 invoker、调试信息)
│ ├── EventBase 所有事件的基类,携带生命周期状态与派发快照
│ ├── EventPool<T> 对象池,高频事件减少 GC
│ ├── EventCloneUtility 同步路径上 async handler 的事件快照克隆
│ ├── EventBusRegHelper 反射扫描 & handler 注册逻辑
│ └── EventAutoRegHelper 运行时初始化,确保 IsInitialized 状态正确
├── Editor/
│ └── EventBusViewerWindow 事件查看器,实时显示订阅者与触发日志
└── (织入)ShrinkShared.CodeGen / CodeGen 共享 ILPostProcessor 优先,本地 ILPostProcessor 兜底
向 [EventBusSubscriber] 类注入 AwakeAutoRegister
与 OnDestroyUnregisterInstance
```
**热路径(`TriggerEvent`)工作流:**
```
TriggerEvent(evt)
└─ 取该事件类型的 ListenerList // 总线级字典 + 共享锁,每类型常数开销
└─ GetHandlers() // 返回缓存快照数组(脏时才重建),无拷贝
├─ 快照数组引用挂到事件对象上(一次赋值,供 GetSubscribers 调试)
└─ 遍历 handlers[]
├─ 跳过已取消 & 不接收取消的 handler
├─ Action<T> → 经预编译 invoker 直接调用
└─ Func<T, UniTask> → 克隆事件快照后 .Forget()(同步路径)
```
**自动注册完整流程:**
```
【编译期】若当前程序集命中 ShrinkShared.CodeGen 覆盖范围,则由共享 ILPostProcessor 扫描;
否则由 CodeGen/EventBusILPostProcessor.cs 本地扫描
└─ 找到标记了 [EventBusSubscriber] 且存在实例 [EventSubscribe] 方法的 MonoBehaviour 子类
├─ 在 Awake 头部织入 EventBus.AutoRegister(this)
└─ 在 OnDestroy 头部织入 EventBus.UnregisterInstance(this)
(类无对应方法时自动生成,有虚基类方法时自动调用 base)
※ 只引用 ShrinkEventBus.Runtime 的纯业务程序集目前不在织入范围内,需手动 AutoRegister
【运行时 - 默认静态总线启动】
└─ 读取编译期静态订阅清单,注册 static [EventSubscribe] 方法
【运行时 - 动态创建】Instantiate(prefab)
└─ Unity 调用新对象的 Awake(已含织入代码)→ 自动注册
【运行时 - 销毁】GameObject.Destroy
└─ OnDestroy(已含织入代码)→ UnregisterInstance → 自动反注册
```
---
## ✅ 最佳实践
**事件设计:尽量让属性只读**
```csharp
// ✅ 推荐:构造时传入,防止 handler 间意外修改输入数据
public class OrderPlacedEvent : EventBase
{
public string OrderId { get; }
public decimal Amount { get; }
public OrderPlacedEvent(string orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
// ❌ 避免:公开可写属性,handler 间耦合风险高
public class BadEvent : EventBase
{
public object Payload { get; set; }
}
```
**高频事件一定要用对象池**
```csharp
// ✅ 每帧触发的伤害/移动事件
using var dmgEvt = EventPool<DamageEvent>.Get();
dmgEvt.Value = damage;
EventBus.TriggerEvent(dmgEvt);
// ❌ 每帧 new,会产生大量 GC
EventBus.TriggerEvent(new DamageEvent { Value = damage });
```
**非 MonoBehaviour 类一定要手动清理**
```csharp
public void Dispose()
{
EventBus.UnregisterAllEventsForObject(this);
}
```
**异步 handler 中谨慎触发新事件**
`TriggerEventAsync` 的 handler 内部再次 `await TriggerEventAsync`,链条过深时调用栈难以追踪,建议把二次触发拆到外部或改用消息队列。
---
## ⚠️ 注意事项
- **`TriggerEvent` 不等待异步 handler**:同步路径中的 UniTask handler 会基于事件快照异步执行,执行结果和异常不会传回调用方,对原事件对象的改动也不会回写。需要等待并拿到最终状态时请使用 `TriggerEventAsync`
- **同步路径中的 async 快照是浅拷贝**:事件对象本身会复制一份,但如果载荷里挂着可变引用类型(如 `List<>``Dictionary<>`、自定义引用对象),内部成员仍然是共享引用。高风险数据建议改成不可变载荷,或统一走 `TriggerEventAsync`
- **EventPool 归还后不要再使用**`Release` 后对象会立即 `ResetInternal()`,继续访问属性将得到默认值。
- **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 快照,会产生语义上的不确定性。
- **静态 handler 永远不会自动注销**:静态方法注册后持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。
- **`[EventBusSubscriber]` 仅对 MonoBehaviour 生效自动注册**:非 MonoBehaviour 类标记该 Attribute 无任何效果,请使用手动注册。
- **标了 `[EventBusSubscriber]` 但没有实例 `[EventSubscribe]` 方法的类**:编译期不会织入;若通过 `AutoRegister` 手动接入,会输出警告并跳过(不抛异常)。显式 `Register()` 对此仍严格抛错。
- **int 数字优先级重载必须显式传值**:1.3.0 起 int 重载不再有默认值;数字 `0` 映射 `NORMAL`
- **ILPostProcessor 织入发生在编译期**:修改代码后需要重新编译才能使注入生效,热重载场景下请注意这一点。
- **继承泛型基类(如 `Singleton<T>`)时无需额外处理**:ILP 会正确识别泛型基类中的虚方法并生成 `protected override`,自动调用 `base.Awake()``base.OnDestroy()`
---
## 🐛 常见问题排查
**事件没有被任何 handler 接收**
1. 检查订阅类是否有 `[EventBusSubscriber]`
2. 检查方法是否有 `[EventSubscribe]`,且签名为 `void/UniTask Method(TEvent evt)`
3. 确认代码在标记 `[EventBusSubscriber]` 后重新编译过(ILPostProcessor 需要编译期运行)
4. 确认没有在 `Awake` 之前就触发事件
```csharp
// 调试:主动检查注册状态
Debug.Log(EventBus.IsInstanceRegistered(this));
Debug.Log($"订阅者数量: {EventBus.GetEventSubscribers<MyEvent>().Length}");
```
**怀疑内存泄漏**
```csharp
// 检查是否有 handler 持有意外引用
var handlers = EventBus.GetEventSubscribers<MyEvent>();
foreach (var h in handlers)
Debug.Log($"{h.DisplayDeclaringType.Name}.{h.DisplayMethodName} | target: {h.Target}");
```
**Editor 下想追踪事件流**
打开事件查看器:菜单栏 → `ShrinkSDK``事件总线``事件查看器`
也可以通过代码追踪:
```csharp
EventBus.EnableDebugRecord = true;
EventBus.TriggerEvent(evt);
foreach (var h in evt.GetSubscribers())
Debug.Log($"[{h.Priority}] {h.DisplayDeclaringType.Name}.{h.DisplayMethodName}");
```
---
## 📄 License
[MIT](LICENSE)
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e08e301c95a34847a1071cd96bba1eeb
timeCreated: 1772900157
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c410edd1d6b644b498d39ff5e1fbc98b
timeCreated: 1772900792
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43e85f78c24f1d94ba7d06a4fa37e937
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,42 @@
using System;
namespace ShrinkEventBus
{
[AttributeUsage(AttributeTargets.Class)]
public class CancelableAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class)]
public class HasResultAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class)]
public class EventBusSubscriberAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class EventSubscribeAttribute : Attribute
{
public EventPriority Priority { get; set; }
public bool ReceiveCanceled { get; set; }
public int NumericPriority { get; set; }
public EventSubscribeAttribute()
{
Priority = EventPriority.NORMAL;
ReceiveCanceled = false;
NumericPriority = 0;
}
public EventSubscribeAttribute(EventPriority priority, bool receiveCanceled = false)
{
Priority = priority;
ReceiveCanceled = receiveCanceled;
NumericPriority = 0;
}
public EventSubscribeAttribute(int priority)
{
NumericPriority = priority;
Priority = PriorityHelper.ConvertToEventPriority(priority);
ReceiveCanceled = false;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9bb9d82b04a04f3085b9e68ab4f1ba60
timeCreated: 1760098802
@@ -0,0 +1,36 @@
using UnityEngine;
namespace ShrinkEventBus
{
public static class EventAutoRegHelper
{
private static readonly object InitLock = new();
public static bool IsInitialized { get; private set; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void InitializeAfterSceneLoad()
{
EnsureInitialized();
}
public static void EnsureInitialized()
{
if (IsInitialized) return;
lock (InitLock)
{
if (IsInitialized) return;
IsInitialized = true;
}
}
public static void Cleanup()
{
lock (InitLock)
{
IsInitialized = false;
}
EventBus.UnregisterAllEvents();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f4d79f65274145cab7614950a64a92f5
timeCreated: 1760099405
@@ -0,0 +1,146 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.Serialization;
namespace ShrinkEventBus
{
public abstract class EventBase : IDisposable
{
private sealed class EventMetadata
{
public bool IsCancelable;
public bool HasResult;
}
private static readonly ConcurrentDictionary<Type, EventMetadata> MetadataCache = new();
private EventHandlerInfo[]? _dispatchSnapshot;
private Guid? _eventId;
private bool _isCanceled;
private EventResult _result = EventResult.DEFAULT;
[IgnoreDataMember]
public EventHandlerInfo? CurrentHandler { get; internal set; }
[IgnoreDataMember]
public DateTime EventTime { get; private set; } = DateTime.UtcNow;
[IgnoreDataMember]
public Guid EventId => _eventId ??= Guid.NewGuid();
[IgnoreDataMember]
public bool IsCancelable { get; }
[IgnoreDataMember]
public bool HasResult { get; }
[IgnoreDataMember]
public EventPriority? Phase { get; private set; }
internal bool IsInPool { get; set; }
internal Action<EventBase>? ReleaseAction { get; set; }
protected EventBase()
{
var metadata = GetOrCreateMetadata(GetType());
IsCancelable = metadata.IsCancelable;
HasResult = metadata.HasResult;
Setup();
}
protected virtual void Setup() { }
protected virtual void OnReset() { }
internal void ResetInternal()
{
_isCanceled = false;
_result = EventResult.DEFAULT;
CurrentHandler = null;
Phase = null;
EventTime = DateTime.UtcNow;
_eventId = null;
_dispatchSnapshot = null;
OnReset();
}
internal void PrepareForDispatch()
{
CurrentHandler = null;
Phase = null;
EventTime = DateTime.UtcNow;
_eventId = null;
_dispatchSnapshot = null;
}
internal void SetListenerSnapshot(EventHandlerInfo[]? handlers)
{
_dispatchSnapshot = handlers is { Length: > 0 } ? handlers : null;
}
[IgnoreDataMember]
public bool IsCanceled
{
get => _isCanceled;
set
{
if (!IsCancelable)
throw new UnsupportedOperationException(
$"Event {GetType().Name} is not cancelable. Mark it with [Cancelable] to allow cancellation.");
_isCanceled = value;
}
}
[IgnoreDataMember]
public EventResult Result
{
get => _result;
set
{
if (!HasResult)
throw new InvalidOperationException(
$"Event {GetType().Name} does not support results. Mark it with [HasResult] to allow setting a result.");
_result = value;
}
}
internal void SetPhase(EventPriority value)
{
if (Phase == value) return;
if (Phase != null && Phase.Value.CompareTo(value) > 0)
throw new ArgumentException(
$"Event phase cannot move backwards from {Phase.Value} to {value}.", nameof(value));
Phase = value;
}
public void SetCanceled(bool canceled) => IsCanceled = canceled;
public void SetResult(EventResult result) => Result = result;
public EventHandlerInfo[] GetSubscribers()
{
var snapshot = _dispatchSnapshot;
if (snapshot == null || snapshot.Length == 0)
return Array.Empty<EventHandlerInfo>();
var copy = new EventHandlerInfo[snapshot.Length];
Array.Copy(snapshot, copy, snapshot.Length);
return copy;
}
public void Dispose()
{
ReleaseAction?.Invoke(this);
}
private static EventMetadata GetOrCreateMetadata(Type eventType)
{
return MetadataCache.GetOrAdd(eventType, static type => new EventMetadata
{
IsCancelable = type.GetCustomAttribute<CancelableAttribute>() != null,
HasResult = type.GetCustomAttribute<HasResultAttribute>() != null
});
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b978c5174cf44d893a41950e7a504e3
timeCreated: 1760098714
@@ -0,0 +1,129 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public static class EventBus
{
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
public static IShrinkEventBus Default => DefaultBus;
public static event Action<EventBase, Type> OnEventTriggered
{
add => DefaultBus.OnEventTriggered += value;
remove => DefaultBus.OnEventTriggered -= value;
}
#if UNITY_EDITOR
public static bool EnableDebugRecord
{
get => DefaultBus.EnableDebugRecord;
set => DefaultBus.EnableDebugRecord = value;
}
public static event Action<EventBase, string, string, EventHandlerInfo[]> OnEventTriggeredForEditor
{
add => DefaultBus.OnEventTriggeredForEditor += value;
remove => DefaultBus.OnEventTriggeredForEditor -= value;
}
#endif
public static ShrinkEventBusBuilder Builder() => new();
public static IShrinkEventBus CreateBus(Action<ShrinkEventBusBuilder>? configure = null)
{
var builder = new ShrinkEventBusBuilder();
configure?.Invoke(builder);
return builder.Build();
}
public static void Start() => DefaultBus.Start();
public static void AutoRegister(object target) => DefaultBus.AutoRegister(target);
public static void Register(object target) => DefaultBus.Register(target);
public static void Unregister(object target) => DefaultBus.Unregister(target);
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
public static void RegisterEvent<TEvent>(Action<TEvent> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled);
public static void RegisterEvent<TEvent>(Action<TEvent> handler, int priority)
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled);
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled);
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority);
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled);
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority);
public static void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase =>
DefaultBus.UnregisterEvent(handler);
public static void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase =>
DefaultBus.UnregisterEvent(handler);
public static void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase =>
DefaultBus.ClearAllSubscribersForEvent<TEvent>();
public static void UnregisterAllEventsForObject(object targetObject) =>
DefaultBus.UnregisterAllEventsForObject(targetObject);
public static void UnregisterInstance(object targetObject) => DefaultBus.Unregister(targetObject);
public static void UnregisterAllEvents() => DefaultBus.UnregisterAllEvents();
public static UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase =>
DefaultBus.TriggerEventAsync(eventArgs);
public static UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs)
where TEvent : EventBase => DefaultBus.TriggerEventAsync(phase, eventArgs);
public static bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase =>
DefaultBus.TriggerEvent(eventArgs);
public static bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase =>
DefaultBus.TriggerEvent(phase, eventArgs);
public static EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase =>
DefaultBus.GetEventSubscribers<TEvent>();
public static ListenerList GetListenerList<TEvent>() where TEvent : EventBase =>
DefaultBus.GetListenerList<TEvent>();
public static IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot() =>
DefaultBus.GetAllSubscribersSnapshot();
public static IReadOnlyList<ShrinkEventSubscriptionSnapshot> GetActiveSubscriptionsSnapshot() =>
DefaultBus.GetActiveSubscriptionsSnapshot();
public static bool IsInstanceRegistered(object target) => DefaultBus.IsInstanceRegistered(target);
public static int GetRegisteredInstanceCount() => DefaultBus.GetRegisteredInstanceCount();
public static int GetRegisteredEventTypeCount() => DefaultBus.GetRegisteredEventTypeCount();
private static IShrinkEventBus CreateDefaultBus()
{
var bus = (ShrinkEventBusInstance)new ShrinkEventBusBuilder()
.SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndContinue)
.AllowPerPhaseDispatch()
.Build();
EventBusRegHelper.RegStaticEventHandler(bus);
return bus;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b995a65fd1324255a5ade1ae1f0adf32
timeCreated: 1760099076
@@ -0,0 +1,519 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace ShrinkEventBus.Runtime
{
/// <summary>
/// ShrinkEventBus 性能测试。
/// 挂在任意 GameObject 上,运行后在 Console 查看结果。
/// 在进行测试时不要打开事件查看器的大量实时追踪。
/// </summary>
public sealed class EventBusBenchmark : MonoBehaviour
{
[Header("同步触发测试")] public int iterations = 1_000_000;
[Header("大批量订阅者测试")] public int subscriberCount = 30;
public int massIterations = 10_000;
[Header("异步触发测试")] public int asyncIterations = 1_000;
[Header("注册 / 注销测试")] public int registerIterations = 5_000;
public int scannedRegisterIterations = 1_000;
[Header("取消事件测试")] public int cancelIterations = 100_000;
public class BenchmarkEvent : EventBase
{
public int Value { get; set; }
}
public sealed class DerivedBenchmarkEvent : BenchmarkEvent
{
public bool IsDerived { get; set; }
}
[Cancelable]
public class CancelableBenchmarkEvent : EventBase
{
public int Value { get; set; }
}
private sealed class BenchmarkInstanceSubscriber
{
public int Recorded;
[EventSubscribe(EventPriority.NORMAL)]
private void OnBenchmark(BenchmarkEvent evt)
{
Recorded = evt.Value;
}
}
private static class BenchmarkStaticSubscriber
{
public static int Recorded;
[EventSubscribe(EventPriority.NORMAL)]
private static void OnBenchmark(BenchmarkEvent evt)
{
Recorded = evt.Value;
}
}
private static class BenchmarkMethodSubscriber
{
public static int Recorded;
[EventSubscribe(EventPriority.NORMAL)]
private static void OnBenchmark(BenchmarkEvent evt)
{
Recorded = evt.Value;
}
}
private sealed class PhaseSubscriber
{
public int Sum;
[EventSubscribe(EventPriority.HIGH)]
private void OnHigh(BenchmarkEvent evt)
{
Sum += evt.Value + 1;
}
[EventSubscribe(EventPriority.NORMAL)]
private void OnNormal(BenchmarkEvent evt)
{
Sum += evt.Value + 10;
}
[EventSubscribe(EventPriority.LOW)]
private void OnLow(BenchmarkEvent evt)
{
Sum += evt.Value + 100;
}
}
private readonly StringBuilder _report = new();
private void Start()
{
RunAllBenchmarks().Forget();
}
private async UniTaskVoid RunAllBenchmarks()
{
_report.Clear();
_report.AppendLine("╔══════════════════════════════════════════════════╗");
_report.AppendLine("║ ShrinkEventBus Benchmark Report ║");
_report.AppendLine(
$"║ iter={iterations,8} mass={massIterations,6} reg={registerIterations,5} scan={scannedRegisterIterations,4} ║");
_report.AppendLine("╚══════════════════════════════════════════════════╝");
_report.AppendLine(" 基准对象:独立实例 busAllowPerPhaseDispatch 已开启)");
Log("开始:无订阅者触发");
Bench_TriggerEvent_NoSubscriber();
await UniTask.Yield();
Log("开始:单订阅者同步触发");
Bench_TriggerEvent_SingleSubscriber();
await UniTask.Yield();
Log("开始:父事件监听子事件");
Bench_TriggerEvent_InheritedSubscriber();
await UniTask.Yield();
Log($"开始:{subscriberCount} 订阅者同步触发");
Bench_TriggerEvent_MassSubscribers();
await UniTask.Yield();
Log("开始:按 phase 分发");
Bench_TriggerEvent_PerPhase();
await UniTask.Yield();
Log("开始:异步触发");
await Bench_TriggerEventAsync();
await UniTask.Yield();
Log("开始:手工 delegate 注册/注销");
Bench_RegisterUnregister_ManualDelegate();
await UniTask.Yield();
Log("开始:实例扫描注册/注销");
Bench_RegisterUnregister_InstanceScan();
await UniTask.Yield();
Log("开始:静态类型扫描注册/注销");
Bench_RegisterUnregister_StaticScan();
await UniTask.Yield();
Log("开始:MethodInfo 注册/注销");
Bench_RegisterUnregister_MethodScan();
await UniTask.Yield();
Log("开始:EventPool vs new");
Bench_EventPool_vs_New();
await UniTask.Yield();
Log("开始:已取消事件跳过");
Bench_CanceledEvent_Skip();
_report.AppendLine("\n══════════════════════════════════════════════════");
_report.AppendLine(" 全部测试完成");
Debug.Log(_report.ToString());
}
private void Bench_TriggerEvent_NoSubscriber()
{
var bus = CreateBenchmarkBus();
var evt = new BenchmarkEvent { Value = 1 };
Warmup(() => bus.TriggerEvent(evt), 500);
var sw = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
bus.TriggerEvent(evt);
sw.Stop();
Record("TriggerEvent × 无订阅者(基线)", sw.Elapsed.TotalMilliseconds, iterations);
Log("完成:无订阅者触发");
}
private void Bench_TriggerEvent_SingleSubscriber()
{
var bus = CreateBenchmarkBus();
var dummy = 0;
void Handler(BenchmarkEvent evt)
{
dummy = evt.Value;
}
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
var evt = new BenchmarkEvent { Value = 1 };
Warmup(() => bus.TriggerEvent(evt), 500);
var sw = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
bus.TriggerEvent(evt);
sw.Stop();
Record("TriggerEvent × 单订阅者同步", sw.Elapsed.TotalMilliseconds, iterations);
Log("完成:单订阅者同步触发");
bus.UnregisterEvent<BenchmarkEvent>(Handler);
GC.KeepAlive(dummy);
}
private void Bench_TriggerEvent_InheritedSubscriber()
{
var bus = CreateBenchmarkBus();
var dummy = 0;
void Handler(BenchmarkEvent evt)
{
dummy = evt.Value;
}
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
var evt = new DerivedBenchmarkEvent { Value = 1, IsDerived = true };
Warmup(() => bus.TriggerEvent(evt), 500);
var sw = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
bus.TriggerEvent(evt);
sw.Stop();
Record("TriggerEvent × 父事件监听子事件", sw.Elapsed.TotalMilliseconds, iterations);
Log("完成:父事件监听子事件");
bus.UnregisterEvent<BenchmarkEvent>(Handler);
GC.KeepAlive(dummy);
}
private void Bench_TriggerEvent_MassSubscribers()
{
var bus = CreateBenchmarkBus();
var dummy = 0;
var handlers = new Action<BenchmarkEvent>[subscriberCount];
for (var i = 0; i < subscriberCount; i++)
{
var captured = i;
handlers[i] = evt => { dummy = captured + evt.Value; };
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
}
var evt = new BenchmarkEvent { Value = 1 };
Warmup(() => bus.TriggerEvent(evt), 100);
var sw = Stopwatch.StartNew();
for (var i = 0; i < massIterations; i++)
bus.TriggerEvent(evt);
sw.Stop();
Record($"TriggerEvent × {subscriberCount} 订阅者同步", sw.Elapsed.TotalMilliseconds, massIterations);
Log($"完成:{subscriberCount} 订阅者同步触发");
foreach (var handler in handlers)
bus.UnregisterEvent<BenchmarkEvent>(handler);
GC.KeepAlive(dummy);
}
private void Bench_TriggerEvent_PerPhase()
{
var bus = CreateBenchmarkBus();
var subscriber = new PhaseSubscriber();
bus.Register(subscriber);
var evt = new BenchmarkEvent { Value = 1 };
Warmup(() => bus.TriggerEvent(EventPriority.HIGH, evt), 500);
var sw = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
bus.TriggerEvent(EventPriority.HIGH, evt);
sw.Stop();
Record("TriggerEvent(Phase=HIGH) × 3 phase 监听", sw.Elapsed.TotalMilliseconds, iterations);
Log("完成:按 phase 分发");
bus.Unregister(subscriber);
GC.KeepAlive(subscriber.Sum);
}
private async UniTask Bench_TriggerEventAsync()
{
var bus = CreateBenchmarkBus();
var dummy = 0;
async UniTask Handler(BenchmarkEvent evt)
{
dummy = evt.Value;
await UniTask.CompletedTask;
}
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
var evt = new BenchmarkEvent { Value = 1 };
await WarmupAsync(() => bus.TriggerEventAsync(evt), 10);
const int perFrame = 10;
var totalMs = 0.0;
var ran = 0;
while (ran < asyncIterations)
{
await UniTask.Yield();
var count = Math.Min(perFrame, asyncIterations - ran);
var sw = Stopwatch.StartNew();
for (var i = 0; i < count; i++)
await bus.TriggerEventAsync(evt);
sw.Stop();
totalMs += sw.Elapsed.TotalMilliseconds;
ran += count;
}
Record("TriggerEventAsync × 单订阅者异步", totalMs, asyncIterations);
Log("完成:异步触发");
bus.UnregisterEvent<BenchmarkEvent>(Handler);
GC.KeepAlive(dummy);
}
private void Bench_RegisterUnregister_ManualDelegate()
{
var bus = CreateBenchmarkBus();
var dummy = 0;
var handlers = new Action<BenchmarkEvent>[registerIterations];
for (var i = 0; i < registerIterations; i++)
{
var captured = i;
handlers[i] = evt => { dummy = captured; };
}
var swReg = Stopwatch.StartNew();
for (var i = 0; i < registerIterations; i++)
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
swReg.Stop();
Record("RegisterEvent(delegate)", swReg.Elapsed.TotalMilliseconds, registerIterations);
var swUnreg = Stopwatch.StartNew();
for (var i = 0; i < registerIterations; i++)
bus.UnregisterEvent<BenchmarkEvent>(handlers[i]);
swUnreg.Stop();
Record("UnregisterEvent(delegate)", swUnreg.Elapsed.TotalMilliseconds, registerIterations);
Log("完成:手工 delegate 注册/注销");
GC.KeepAlive(dummy);
}
private void Bench_RegisterUnregister_InstanceScan()
{
var bus = CreateBenchmarkBus();
var subscribers = new BenchmarkInstanceSubscriber[scannedRegisterIterations];
for (var i = 0; i < subscribers.Length; i++)
subscribers[i] = new BenchmarkInstanceSubscriber();
var sw = Stopwatch.StartNew();
for (var i = 0; i < subscribers.Length; i++)
{
var subscriber = subscribers[i];
bus.Register(subscriber);
bus.Unregister(subscriber);
}
sw.Stop();
Record("Register/Unregister(instance scan)", sw.Elapsed.TotalMilliseconds, subscribers.Length);
Log("完成:实例扫描注册/注销");
}
private void Bench_RegisterUnregister_StaticScan()
{
var bus = CreateBenchmarkBus();
var sw = Stopwatch.StartNew();
for (var i = 0; i < scannedRegisterIterations; i++)
{
bus.Register(typeof(BenchmarkStaticSubscriber));
bus.Unregister(typeof(BenchmarkStaticSubscriber));
}
sw.Stop();
Record("Register/Unregister(static type scan)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations);
Log("完成:静态类型扫描注册/注销");
}
private void Bench_RegisterUnregister_MethodScan()
{
var bus = CreateBenchmarkBus();
var method = typeof(BenchmarkMethodSubscriber).GetMethod("OnBenchmark",
BindingFlags.Static | BindingFlags.NonPublic);
if (method == null)
throw new MissingMethodException(typeof(BenchmarkMethodSubscriber).FullName, "OnBenchmark");
var sw = Stopwatch.StartNew();
for (var i = 0; i < scannedRegisterIterations; i++)
{
bus.Register(method);
bus.Unregister(method);
}
sw.Stop();
Record("Register/Unregister(MethodInfo)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations);
Log("完成:MethodInfo 注册/注销");
}
private void Bench_EventPool_vs_New()
{
var bus = CreateBenchmarkBus();
void Handler(BenchmarkEvent evt)
{
}
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
for (var i = 0; i < 64; i++)
EventPool<BenchmarkEvent>.Release(EventPool<BenchmarkEvent>.Get());
GC.Collect();
var gcBefore = GC.CollectionCount(0);
var swNew = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
{
var evt = new BenchmarkEvent { Value = i };
bus.TriggerEvent(evt);
}
swNew.Stop();
var gcNew = GC.CollectionCount(0) - gcBefore;
Record($"TriggerEvent × new() [GC Gen0={gcNew,3}]", swNew.Elapsed.TotalMilliseconds, iterations);
GC.Collect();
gcBefore = GC.CollectionCount(0);
var swPool = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
{
using var evt = EventPool<BenchmarkEvent>.Get();
evt.Value = i;
bus.TriggerEvent(evt);
}
swPool.Stop();
var gcPool = GC.CollectionCount(0) - gcBefore;
Record($"TriggerEvent × Pool.Get() [GC Gen0={gcPool,3}]", swPool.Elapsed.TotalMilliseconds, iterations);
Log("完成:EventPool vs new");
bus.UnregisterEvent<BenchmarkEvent>(Handler);
}
private void Bench_CanceledEvent_Skip()
{
var bus = CreateBenchmarkBus();
var dummy = 0;
var handlers = new Action<CancelableBenchmarkEvent>[10];
for (var i = 0; i < handlers.Length; i++)
{
handlers[i] = evt => { dummy = evt.Value; };
bus.RegisterEvent<CancelableBenchmarkEvent>(handlers[i], EventPriority.NORMAL);
}
var evt = new CancelableBenchmarkEvent { Value = 1 };
evt.SetCanceled(true);
Warmup(() => bus.TriggerEvent(evt), 500);
var sw = Stopwatch.StartNew();
for (var i = 0; i < cancelIterations; i++)
bus.TriggerEvent(evt);
sw.Stop();
Record("TriggerEvent × 已取消事件跳过(10 订阅者)", sw.Elapsed.TotalMilliseconds, cancelIterations);
Log("完成:已取消事件跳过");
foreach (var handler in handlers)
bus.UnregisterEvent<CancelableBenchmarkEvent>(handler);
GC.KeepAlive(dummy);
}
private static IShrinkEventBus CreateBenchmarkBus()
{
return EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch());
}
private static void Warmup(Action action, int count)
{
for (var i = 0; i < count; i++)
action();
}
private static async UniTask WarmupAsync(Func<UniTask<bool>> action, int count)
{
for (var i = 0; i < count; i++)
await action();
}
private static void Log(string msg)
{
Debug.Log($"[Benchmark] {msg}");
}
private void Record(string label, double totalMs, int count)
{
var perOp = totalMs / count * 1000.0;
var throughput = totalMs <= 0.0001 ? 0 : count / (totalMs / 1000.0);
_report.AppendLine($"\n ▶ {label}");
_report.AppendLine($" 总耗时 : {totalMs,10:F3} ms");
_report.AppendLine($" 单次 : {perOp,10:F4} μs/op");
_report.AppendLine($" 吞吐量 : {throughput,10:F0} ops/s");
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9619a4dc431b4b2c8268c32a9e56f72f
timeCreated: 1772913062
@@ -0,0 +1,53 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace ShrinkEventBus
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class EventBusStaticRegistryAttribute : Attribute
{
public EventBusStaticRegistryAttribute(params Type[] subscriberTypes)
{
SubscriberTypes = subscriberTypes ?? Array.Empty<Type>();
}
public Type[] SubscriberTypes { get; }
}
internal static class EventBusGeneratedRegistry
{
public static IReadOnlyList<Type> GetStaticSubscriberTypes()
{
var types = new List<Type>();
var seen = new HashSet<Type>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
object[] attributes;
try
{
attributes = assembly.GetCustomAttributes(typeof(EventBusStaticRegistryAttribute), false);
}
catch
{
continue;
}
foreach (var attribute in attributes.OfType<EventBusStaticRegistryAttribute>())
{
foreach (var subscriberType in attribute.SubscriberTypes ?? Array.Empty<Type>())
{
if (subscriberType == null || !seen.Add(subscriberType))
continue;
types.Add(subscriberType);
}
}
}
return types;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 04809187aa0ce3b43a4df12b048a1c98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,193 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace ShrinkEventBus
{
internal static class EventBusRegHelper
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegStaticEventHandler(ShrinkEventBusInstance bus)
{
if (bus == null)
throw new ArgumentNullException(nameof(bus));
RegisterStaticSubscriberTypes(bus, EventBusGeneratedRegistry.GetStaticSubscriberTypes());
}
public static void RegisterStaticSubscriberTypes(ShrinkEventBusInstance bus, IEnumerable<Type> subscriberTypes)
{
if (bus == null)
throw new ArgumentNullException(nameof(bus));
if (subscriberTypes == null)
throw new ArgumentNullException(nameof(subscriberTypes));
foreach (var type in subscriberTypes)
{
try
{
if (type == null || type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
continue;
RegisterTarget(bus, type, requireSubscriberAttribute: true, lenientWhenNoMethods: true);
}
catch (Exception ex)
{
_ = ex;
#if UNITY_EDITOR
Debug.LogWarning($"[EventBus] Static registration failed for {type?.FullName}: {ex.Message}");
#endif
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegisterTarget(ShrinkEventBusInstance bus, object target, bool requireSubscriberAttribute,
bool lenientWhenNoMethods = false)
{
if (bus == null)
throw new ArgumentNullException(nameof(bus));
if (target == null)
throw new ArgumentNullException(nameof(target));
switch (target)
{
case MethodInfo method:
RegisterMethod(bus, null, method, requireSubscriberAttribute: false, isStaticRegistration: true);
return;
case Type staticType:
RegisterDeclaredMethods(bus, null, staticType, requireSubscriberAttribute,
isStaticRegistration: true, lenientWhenNoMethods);
return;
default:
RegisterDeclaredMethods(bus, target, target.GetType(), requireSubscriberAttribute,
isStaticRegistration: false, lenientWhenNoMethods);
return;
}
}
private static void RegisterDeclaredMethods(ShrinkEventBusInstance bus, object? target, Type type,
bool requireSubscriberAttribute, bool isStaticRegistration, bool lenientWhenNoMethods)
{
if (requireSubscriberAttribute &&
type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
{
throw new InvalidOperationException(
$"Type {type.FullName} must declare [EventBusSubscriber] before it can be auto-registered.");
}
var flags = (isStaticRegistration ? BindingFlags.Static : BindingFlags.Instance) |
BindingFlags.Public | BindingFlags.NonPublic;
var methods = type.GetMethods(flags);
var foundMethods = 0;
foreach (var method in methods)
{
var hasSubscribeAttribute = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false).Length > 0;
if (!hasSubscribeAttribute)
continue;
if (method.IsStatic != isStaticRegistration)
{
var expected = isStaticRegistration ? "static" : "instance";
throw new InvalidOperationException(
$"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode.");
}
RegisterMethod(bus, target, method, requireSubscriberAttribute: false, isStaticRegistration);
foundMethods++;
}
if (foundMethods == 0)
{
var message =
$"Type {type.FullName} has no [EventSubscribe] methods for {(isStaticRegistration ? "static" : "instance")} registration.";
if (lenientWhenNoMethods)
{
Debug.LogWarning($"[EventBus] {message} Auto-registration skipped.");
return;
}
throw new InvalidOperationException(message);
}
}
private static void RegisterMethod(ShrinkEventBusInstance bus, object? target, MethodInfo method,
bool requireSubscriberAttribute, bool isStaticRegistration)
{
if (method == null)
throw new ArgumentNullException(nameof(method));
if (!method.IsDefined(typeof(EventSubscribeAttribute), false))
throw new InvalidOperationException($"Method {method} is not annotated with [EventSubscribe].");
if (method.IsStatic != isStaticRegistration)
{
var expected = isStaticRegistration ? "static" : "instance";
throw new InvalidOperationException(
$"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode.");
}
if (requireSubscriberAttribute && method.DeclaringType != null &&
method.DeclaringType.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
{
throw new InvalidOperationException(
$"Type {method.DeclaringType.FullName} must declare [EventBusSubscriber] before it can be auto-registered.");
}
var subscribeAttr = (EventSubscribeAttribute)method.GetCustomAttributes(typeof(EventSubscribeAttribute), false)[0];
var parameters = method.GetParameters();
if (parameters.Length != 1)
{
throw new InvalidOperationException(
$"Method {method} has [EventSubscribe] but declares {parameters.Length} parameters. Event handlers must declare exactly one EventBase parameter.");
}
var parameterType = parameters[0].ParameterType;
if (!typeof(EventBase).IsAssignableFrom(parameterType))
{
throw new InvalidOperationException(
$"Method {method} has [EventSubscribe] but parameter {parameterType.FullName} does not inherit from EventBase.");
}
ProcessMethodRegistration(bus, target, method, subscribeAttr, parameterType);
}
private static void ProcessMethodRegistration(ShrinkEventBusInstance bus, object? target, MethodInfo method,
EventSubscribeAttribute subscribeAttr, Type parameterType)
{
string scope = target == null ? "Static" : "Instance";
string typeName = target == null ? method.DeclaringType?.Name ?? "Unknown" : target.GetType().Name;
if (method.ReturnType == typeof(UniTask))
{
var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(UniTask));
var handlerDelegate = target == null
? Delegate.CreateDelegate(funcType, method)
: Delegate.CreateDelegate(funcType, target, method);
bus.RegisterEventInternal(parameterType, handlerDelegate, subscribeAttr.Priority,
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
$"{scope} {typeName}.{method.Name} (UniTask)", method);
return;
}
if (method.ReturnType == typeof(void))
{
var actionType = typeof(Action<>).MakeGenericType(parameterType);
var actionDelegate = target == null
? Delegate.CreateDelegate(actionType, method)
: Delegate.CreateDelegate(actionType, target, method);
bus.RegisterEventInternal(parameterType, actionDelegate, subscribeAttr.Priority,
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
$"{scope} {typeName}.{method.Name} (Sync)", method);
return;
}
throw new InvalidOperationException(
$"Method {method} has [EventSubscribe] but return type {method.ReturnType.FullName} is unsupported. Use void or UniTask.");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a237a407a3a42394eba27bc8e4dbdce2
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace ShrinkEventBus
{
internal static class EventCloneUtility
{
private static readonly object CacheLock = new();
private static readonly Dictionary<Type, FieldInfo[]> FieldCache = new();
public static TEvent CloneForDetachedDispatch<TEvent>(TEvent source) where TEvent : EventBase
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (Activator.CreateInstance(source.GetType()) is not TEvent clone)
throw new InvalidOperationException(
$"Cannot clone event type {source.GetType().FullName}. A public parameterless constructor is required.");
// 先固化 EventId,让克隆与原事件共享同一个派发标识
_ = source.EventId;
CopyFields(source, clone);
clone.ReleaseAction = null;
clone.IsInPool = false;
return clone;
}
private static void CopyFields(EventBase source, EventBase target)
{
var fields = GetCopyableFields(source.GetType());
for (var i = 0; i < fields.Length; i++)
fields[i].SetValue(target, fields[i].GetValue(source));
}
private static FieldInfo[] GetCopyableFields(Type type)
{
lock (CacheLock)
{
if (FieldCache.TryGetValue(type, out var cached))
return cached;
var fields = new List<FieldInfo>();
var currentType = type;
while (currentType != null && currentType != typeof(object))
{
var declaredFields = currentType.GetFields(BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
for (var i = 0; i < declaredFields.Length; i++)
{
var field = declaredFields[i];
if (field.IsStatic)
continue;
if (ShouldSkipField(field))
continue;
fields.Add(field);
}
currentType = currentType.BaseType;
}
cached = fields.ToArray();
FieldCache[type] = cached;
return cached;
}
}
private static bool ShouldSkipField(FieldInfo field)
{
return field.Name is "<ReleaseAction>k__BackingField"
or "<IsInPool>k__BackingField";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 876564c111495b2489870b29ab75da5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,141 @@
using System;
using System.Reflection;
using Cysharp.Threading.Tasks;
#pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
namespace ShrinkEventBus
{
public interface IMethodWrapper
{
MethodInfo OriginalMethod { get; }
}
public class EventHandlerInfo
{
public long SubscriptionId { get; }
public Delegate Handler { get; }
internal Action<EventBase>? SyncInvoker { get; }
internal Func<EventBase, UniTask>? AsyncInvoker { get; }
public EventPriority Priority { get; }
public int NumericPriority { get; }
public bool ReceiveCanceled { get; }
public object? Target { get; }
public MethodInfo Method { get; }
public string DebugInfo { get; }
public Type DeclaringType { get; }
public string MethodName { get; }
public MethodInfo? OriginalMethod { get; }
public string OriginalMethodName { get; }
public Type? OriginalDeclaringType { get; }
public DateTime RegisteredAtUtc { get; }
private EventHandlerInfo(long subscriptionId, Delegate handler, Action<EventBase>? syncInvoker,
Func<EventBase, UniTask>? asyncInvoker,
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
MethodInfo? originalMethod = null)
{
SubscriptionId = subscriptionId;
Handler = handler;
SyncInvoker = syncInvoker;
AsyncInvoker = asyncInvoker;
Priority = priority;
NumericPriority = numericPriority;
ReceiveCanceled = receiveCanceled;
Target = handler.Target;
Method = handler.Method;
DebugInfo = debugInfo;
DeclaringType = Method.DeclaringType ?? typeof(object);
MethodName = Method.Name;
OriginalMethod = ExtractOriginalMethodFromWrapper(handler) ?? originalMethod;
if (OriginalMethod != null)
{
OriginalMethodName = OriginalMethod.Name;
OriginalDeclaringType = OriginalMethod.DeclaringType;
}
else
{
OriginalMethodName = MethodName;
OriginalDeclaringType = DeclaringType;
}
RegisteredAtUtc = DateTime.UtcNow;
}
private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler)
{
if (handler.Target is IMethodWrapper wrapper)
{
return wrapper.OriginalMethod;
}
return null;
}
public string DisplayMethodName => OriginalMethodName;
public Type DisplayDeclaringType => OriginalDeclaringType ?? DeclaringType;
public bool MatchesMethod(MethodInfo method)
{
return method != null && (ReferenceEquals(Method, method) || ReferenceEquals(OriginalMethod, method));
}
public bool MatchesDeclaringType(Type declaringType)
{
return declaringType != null &&
(DeclaringType == declaringType || OriginalDeclaringType == declaringType);
}
public static EventHandlerInfo Create(long subscriptionId, Delegate handler, Type eventType,
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
MethodInfo? originalMethod = null)
{
if (handler == null)
throw new System.ArgumentNullException(nameof(handler));
if (eventType == null)
throw new System.ArgumentNullException(nameof(eventType));
Action<EventBase>? syncInvoker = null;
Func<EventBase, UniTask>? asyncInvoker = null;
if (handler.Method.ReturnType == typeof(void))
syncInvoker = CreateSyncInvoker(eventType, handler);
else if (handler.Method.ReturnType == typeof(UniTask))
asyncInvoker = CreateAsyncInvoker(eventType, handler);
else
throw new System.ArgumentException(
$"Unsupported event handler return type {handler.Method.ReturnType.FullName} for {handler.Method}.");
return new EventHandlerInfo(subscriptionId, handler, syncInvoker, asyncInvoker, priority, numericPriority,
receiveCanceled, debugInfo, originalMethod);
}
private static Action<EventBase> CreateSyncInvoker(Type eventType, Delegate handler)
{
var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateSyncInvokerGeneric),
BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType);
return (Action<EventBase>)factory.Invoke(null, new object[] { handler })!;
}
private static Func<EventBase, UniTask> CreateAsyncInvoker(Type eventType, Delegate handler)
{
var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateAsyncInvokerGeneric),
BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType);
return (Func<EventBase, UniTask>)factory.Invoke(null, new object[] { handler })!;
}
private static Action<EventBase> CreateSyncInvokerGeneric<TEvent>(Delegate handler) where TEvent : EventBase
{
var typedHandler = (Action<TEvent>)handler;
return eventArgs => typedHandler((TEvent)eventArgs);
}
private static Func<EventBase, UniTask> CreateAsyncInvokerGeneric<TEvent>(Delegate handler)
where TEvent : EventBase
{
var typedHandler = (Func<TEvent, UniTask>)handler;
return eventArgs => typedHandler((TEvent)eventArgs);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 19972a32cbf54c7f94d6214fde5635c2
timeCreated: 1760098840
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
namespace ShrinkEventBus
{
public static class EventPool<T> where T : EventBase, new()
{
private const int MaxPoolSize = 128;
private static readonly Stack<T> Pool = new(32);
private static readonly object Lock = new();
private static readonly Action<EventBase> CachedReleaseAction = e => Release((T)e);
public static T Get()
{
lock (Lock)
{
if (Pool.Count > 0)
{
var evt = Pool.Pop();
evt.IsInPool = false;
return evt;
}
}
var newEvt = new T();
newEvt.ReleaseAction = CachedReleaseAction;
return newEvt;
}
public static void Release(T evt)
{
if (evt == null)
return;
lock (Lock)
{
if (evt.IsInPool || Pool.Count >= MaxPoolSize)
return;
evt.ResetInternal();
evt.IsInPool = true;
Pool.Push(evt);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c59d817792dd40f08ffed2f1a8d65cb8
timeCreated: 1772891540
@@ -0,0 +1,27 @@
namespace ShrinkEventBus
{
public enum EventPriority
{
HIGHEST = 0,
HIGH = 1,
NORMAL = 2,
LOW = 3,
LOWEST = 4,
MONITOR = 5
}
internal static class PriorityHelper
{
public static EventPriority ConvertToEventPriority(int numericPriority)
{
return numericPriority switch
{
>= 100 => EventPriority.HIGHEST,
>= 50 => EventPriority.HIGH,
>= 0 => EventPriority.NORMAL,
>= -50 => EventPriority.LOW,
_ => EventPriority.LOWEST
};
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c77c5178c66948f29a7c87c9ae203004
timeCreated: 1760098774
@@ -0,0 +1,9 @@
namespace ShrinkEventBus
{
public enum EventResult
{
DEFAULT,
ALLOW,
DENY
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5213a68ce8f433a98f272904a5d7bd2
timeCreated: 1760098791
@@ -0,0 +1,20 @@
using System;
namespace ShrinkEventBus
{
public class UnsupportedOperationException : InvalidOperationException
{
public UnsupportedOperationException() : base()
{
}
public UnsupportedOperationException(string message) : base(message)
{
}
public UnsupportedOperationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 893fdc450e7a4a8e8e1b675f0ce084a7
timeCreated: 1760098825
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public interface IShrinkEventSubscription : IDisposable
{
long SubscriptionId { get; }
bool IsDisposed { get; }
}
public readonly struct ShrinkEventSubscriptionSnapshot
{
public ShrinkEventSubscriptionSnapshot(long subscriptionId, Type eventType, EventPriority priority,
int numericPriority, bool receiveCanceled, object target, string targetTypeName, string methodName,
string debugInfo, DateTime registeredAtUtc)
{
SubscriptionId = subscriptionId;
EventType = eventType ?? throw new ArgumentNullException(nameof(eventType));
Priority = priority;
NumericPriority = numericPriority;
ReceiveCanceled = receiveCanceled;
Target = target;
TargetTypeName = targetTypeName ?? string.Empty;
MethodName = methodName ?? string.Empty;
DebugInfo = debugInfo ?? string.Empty;
RegisteredAtUtc = registeredAtUtc;
}
public long SubscriptionId { get; }
public Type EventType { get; }
public EventPriority Priority { get; }
public int NumericPriority { get; }
public bool ReceiveCanceled { get; }
public object Target { get; }
public string TargetTypeName { get; }
public string MethodName { get; }
public string DebugInfo { get; }
public DateTime RegisteredAtUtc { get; }
public bool IsStaticHandler => Target == null;
}
public interface IShrinkEventBus
{
event Action<EventBase, Type> OnEventTriggered;
#if UNITY_EDITOR
event Action<EventBase, string, string, EventHandlerInfo[]> OnEventTriggeredForEditor;
bool EnableDebugRecord { get; set; }
#endif
bool IsStarted { get; }
void Start();
void AutoRegister(object target);
void Register(object target);
void Unregister(object target);
void RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority = EventPriority.NORMAL,
bool receiveCanceled = false) where TEvent : EventBase;
void RegisterEvent<TEvent>(Action<TEvent> handler, int priority) where TEvent : EventBase;
void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority = EventPriority.NORMAL,
bool receiveCanceled = false) where TEvent : EventBase;
void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority) where TEvent : EventBase;
IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase;
IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
where TEvent : EventBase;
IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase;
IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
where TEvent : EventBase;
void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase;
void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase;
void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase;
void UnregisterAllEventsForObject(object targetObject);
void UnregisterAllEvents();
bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase;
bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase;
UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase;
UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase;
EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase;
ListenerList GetListenerList<TEvent>() where TEvent : EventBase;
IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot();
IReadOnlyList<ShrinkEventSubscriptionSnapshot> GetActiveSubscriptionsSnapshot();
bool IsInstanceRegistered(object target);
int GetRegisteredInstanceCount();
int GetRegisteredEventTypeCount();
}
public interface IShrinkEventExceptionHandler
{
void HandleException(IShrinkEventBus bus, EventBase eventArgs, EventHandlerInfo[] listeners, int index,
Exception exception);
}
public enum ShrinkEventExceptionHandlingMode
{
LogAndContinue,
Throw,
LogAndThrow
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb69f308f5c727542ac602709b02c628
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,262 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace ShrinkEventBus
{
public class ListenerList
{
private static readonly EventPriority[] Priorities = (EventPriority[])Enum.GetValues(typeof(EventPriority));
private readonly object _lock;
private readonly List<EventHandlerInfo>[] _priorityBuckets;
private readonly ListenerList? _parent;
private List<ListenerList>? _children;
private EventHandlerInfo[] _snapshot = Array.Empty<EventHandlerInfo>();
private readonly EventHandlerInfo[]?[] _phaseSnapshots = new EventHandlerInfo[Priorities.Length][];
private bool _dirty;
public ListenerList(ListenerList? parent = null) : this(parent, null)
{
}
// 同一条父子链必须共用一把锁,否则脏标记传播与快照重建会产生竞态
internal ListenerList(ListenerList? parent, object? sharedLock)
{
_lock = sharedLock ?? parent?._lock ?? new object();
_priorityBuckets = new List<EventHandlerInfo>[Priorities.Length];
for (var i = 0; i < _priorityBuckets.Length; i++)
_priorityBuckets[i] = new List<EventHandlerInfo>();
_parent = parent;
_parent?.AddChild(this);
_dirty = true;
}
public int LocalCount
{
get
{
lock (_lock)
{
var count = 0;
foreach (var list in _priorityBuckets)
count += list.Count;
return count;
}
}
}
public int Count => GetHandlers().Length;
public void Add(EventHandlerInfo info)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
lock (_lock)
{
var list = _priorityBuckets[(int)info.Priority];
var index = BinarySearchInsertIndex(list, info.NumericPriority);
list.Insert(index, info);
MarkDirty();
}
}
public bool Remove(Delegate handler)
{
if (handler == null)
return false;
lock (_lock)
{
foreach (var list in _priorityBuckets)
{
for (var i = 0; i < list.Count; i++)
{
if (!list[i].Handler.Equals(handler))
continue;
list.RemoveAt(i);
MarkDirty();
return true;
}
}
return false;
}
}
public int RemoveWhere(Predicate<EventHandlerInfo> predicate)
{
if (predicate == null)
return 0;
lock (_lock)
{
var removedCount = 0;
foreach (var list in _priorityBuckets)
{
for (var i = list.Count - 1; i >= 0; i--)
{
if (!predicate(list[i]))
continue;
list.RemoveAt(i);
removedCount++;
}
}
if (removedCount > 0)
MarkDirty();
return removedCount;
}
}
public void RemoveTarget(object target)
{
if (target == null)
return;
lock (_lock)
{
var removed = false;
foreach (var list in _priorityBuckets)
{
for (var i = list.Count - 1; i >= 0; i--)
{
if (!ReferenceEquals(list[i].Target, target))
continue;
list.RemoveAt(i);
removed = true;
}
}
if (removed)
MarkDirty();
}
}
public EventHandlerInfo[] GetHandlers()
{
lock (_lock)
{
if (_dirty)
RebuildSnapshot();
return _snapshot;
}
}
public EventHandlerInfo[] GetHandlers(EventPriority priority)
{
lock (_lock)
{
if (_dirty)
RebuildSnapshot();
return _phaseSnapshots[(int)priority] ?? Array.Empty<EventHandlerInfo>();
}
}
public void Clear()
{
lock (_lock)
{
foreach (var list in _priorityBuckets)
list.Clear();
MarkDirty();
}
}
private void AddChild(ListenerList child)
{
if (child == null)
return;
lock (_lock)
{
_children ??= new List<ListenerList>(2);
_children.Add(child);
}
}
private void MarkDirty()
{
_dirty = true;
_snapshot = Array.Empty<EventHandlerInfo>();
Array.Clear(_phaseSnapshots, 0, _phaseSnapshots.Length);
if (_children == null)
return;
for (var i = 0; i < _children.Count; i++)
_children[i].MarkDirty();
}
private void RebuildSnapshot()
{
var merged = new List<EventHandlerInfo>();
for (var i = 0; i < Priorities.Length; i++)
{
var parentHandlers = _parent != null
? _parent.GetHandlers(Priorities[i])
: Array.Empty<EventHandlerInfo>();
var phaseSnapshot = MergeByNumericPriority(_priorityBuckets[i], parentHandlers);
_phaseSnapshots[i] = phaseSnapshot;
merged.AddRange(phaseSnapshot);
}
_snapshot = merged.ToArray();
_dirty = false;
}
// 两侧均已按 NumericPriority 降序排列;平局时本类型 handler 在前
private static EventHandlerInfo[] MergeByNumericPriority(List<EventHandlerInfo> own,
EventHandlerInfo[] parentHandlers)
{
if (parentHandlers.Length == 0)
return own.ToArray();
if (own.Count == 0)
return parentHandlers;
var result = new EventHandlerInfo[own.Count + parentHandlers.Length];
int i = 0, j = 0, k = 0;
while (i < own.Count && j < parentHandlers.Length)
{
result[k++] = parentHandlers[j].NumericPriority > own[i].NumericPriority
? parentHandlers[j++]
: own[i++];
}
while (i < own.Count)
result[k++] = own[i++];
while (j < parentHandlers.Length)
result[k++] = parentHandlers[j++];
return result;
}
private static int BinarySearchInsertIndex(List<EventHandlerInfo> list, int numericPriority)
{
var lo = 0;
var hi = list.Count;
while (lo < hi)
{
var mid = (lo + hi) >> 1;
var cmp = numericPriority.CompareTo(list[mid].NumericPriority);
if (cmp > 0)
hi = mid;
else
lo = mid + 1;
}
return lo;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 864eb3e7bf8f443c86472995e408c580
timeCreated: 1760098877
@@ -0,0 +1,12 @@
{
"name": "ShrinkEventBus.Runtime",
"rootNamespace": "ShrinkEventBus",
"references": [
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f9a7e109b2ecd4946ba9202d2288af15
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
#nullable enable
using System;
namespace ShrinkEventBus
{
public sealed class ShrinkEventBusBuilder
{
internal IShrinkEventExceptionHandler? ExceptionHandler { get; private set; }
internal ShrinkEventExceptionHandlingMode ExceptionHandlingMode { get; private set; } =
ShrinkEventExceptionHandlingMode.LogAndContinue;
internal Action<Type>? EventClassChecker { get; private set; }
internal bool StartShutdownEnabled { get; private set; }
internal bool CheckTypesOnDispatchEnabled { get; private set; }
internal bool AllowPerPhaseDispatchEnabled { get; private set; }
public ShrinkEventBusBuilder SetExceptionHandler(IShrinkEventExceptionHandler handler)
{
ExceptionHandler = handler ?? throw new ArgumentNullException(nameof(handler));
return this;
}
public ShrinkEventBusBuilder SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode mode)
{
ExceptionHandlingMode = mode;
return this;
}
public ShrinkEventBusBuilder StartShutdown()
{
StartShutdownEnabled = true;
return this;
}
public ShrinkEventBusBuilder CheckTypesOnDispatch()
{
CheckTypesOnDispatchEnabled = true;
return this;
}
public ShrinkEventBusBuilder AllowPerPhaseDispatch()
{
AllowPerPhaseDispatchEnabled = true;
return this;
}
public ShrinkEventBusBuilder ClassChecker(Action<Type> checker)
{
if (checker == null)
throw new ArgumentNullException(nameof(checker));
EventClassChecker = EventClassChecker == null
? checker
: EventClassChecker + checker;
return this;
}
public ShrinkEventBusBuilder MarkerInterface<TMarker>() where TMarker : class
{
var markerType = typeof(TMarker);
if (!markerType.IsInterface)
throw new InvalidOperationException($"Marker type {markerType.FullName} must be an interface.");
return ClassChecker(eventType =>
{
if (!markerType.IsAssignableFrom(eventType))
throw new ArgumentException(
$"This bus only accepts events assignable to {markerType.FullName}, but got {eventType.FullName}.");
});
}
public IShrinkEventBus Build()
{
return new ShrinkEventBusInstance(this);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aad8b06ecb53b8a4998fd9944673e537
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,718 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace ShrinkEventBus
{
internal sealed class ShrinkEventBusInstance : IShrinkEventBus
{
private sealed class EventSubscription : IShrinkEventSubscription
{
private readonly ShrinkEventBusInstance _owner;
private bool _disposed;
public EventSubscription(ShrinkEventBusInstance owner, long subscriptionId)
{
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
SubscriptionId = subscriptionId;
}
public long SubscriptionId { get; }
public bool IsDisposed => _disposed;
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_owner.UnregisterSubscription(SubscriptionId);
}
}
private readonly Dictionary<Type, ListenerList> _eventHandlers = new();
private readonly HashSet<object> _registeredTargets = new();
private readonly object _listenerLock = new();
private readonly object _registrationLock = new();
private long _nextSubscriptionId;
private readonly IShrinkEventExceptionHandler? _exceptionHandler;
private readonly ShrinkEventExceptionHandlingMode _exceptionHandlingMode;
private readonly Action<Type>? _eventClassChecker;
private readonly bool _checkTypesOnDispatch;
private readonly bool _allowPerPhaseDispatch;
public event Action<EventBase, Type>? OnEventTriggered;
#if UNITY_EDITOR
public bool EnableDebugRecord { get; set; }
public event Action<EventBase, string, string, EventHandlerInfo[]>? OnEventTriggeredForEditor;
#endif
public bool IsStarted { get; private set; }
public ShrinkEventBusInstance(ShrinkEventBusBuilder builder)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
_exceptionHandler = builder.ExceptionHandler;
_exceptionHandlingMode = builder.ExceptionHandlingMode;
_eventClassChecker = builder.EventClassChecker;
_checkTypesOnDispatch = builder.CheckTypesOnDispatchEnabled;
_allowPerPhaseDispatch = builder.AllowPerPhaseDispatchEnabled;
IsStarted = !builder.StartShutdownEnabled;
}
public void Start()
{
IsStarted = true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AutoRegister(object target)
{
RegisterCore(target, requireSubscriberAttribute: true, lenientWhenNoMethods: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Register(object target)
{
RegisterCore(target, requireSubscriberAttribute: false, lenientWhenNoMethods: false);
}
public void Unregister(object target)
{
if (target == null)
return;
lock (_registrationLock)
{
_registeredTargets.Remove(target);
}
switch (target)
{
case Delegate handler:
RemoveHandlers(info => Equals(info.Handler, handler));
return;
case MethodInfo method:
RemoveHandlers(info => info.MatchesMethod(method));
return;
case Type declaringType:
RemoveHandlers(info => info.Target == null && info.MatchesDeclaringType(declaringType));
return;
default:
UnregisterAllEventsForObject(target);
return;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
where TEvent : EventBase
{
var eventPriority = PriorityHelper.ConvertToEventPriority(priority);
RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false,
$"Manual Async Handler (Priority: {priority})");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority = EventPriority.NORMAL,
bool receiveCanceled = false) where TEvent : EventBase
{
RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled,
$"Manual Sync Handler (Priority: {priority})", handler.Method);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterEvent<TEvent>(Action<TEvent> handler, int priority) where TEvent : EventBase
{
var eventPriority = PriorityHelper.ConvertToEventPriority(priority);
RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false,
$"Manual Sync Handler (Priority: {priority})", handler.Method);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase
{
RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled,
$"Manual Async Handler (Priority: {priority})");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase
{
return SubscribeEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled,
$"Manual Sync Subscription (Priority: {priority})", handler.Method);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
where TEvent : EventBase
{
var eventPriority = PriorityHelper.ConvertToEventPriority(priority);
return SubscribeEventInternal(typeof(TEvent), handler, eventPriority, priority, false,
$"Manual Sync Subscription (Priority: {priority})", handler.Method);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase
{
return SubscribeEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled,
$"Manual Async Subscription (Priority: {priority})");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
where TEvent : EventBase
{
var eventPriority = PriorityHelper.ConvertToEventPriority(priority);
return SubscribeEventInternal(typeof(TEvent), handler, eventPriority, priority, false,
$"Manual Async Subscription (Priority: {priority})");
}
internal void RegisterEventInternal(Type eventType, Delegate handler, EventPriority priority,
int numericPriority, bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null)
{
if (eventType == null)
throw new ArgumentNullException(nameof(eventType));
if (handler == null)
throw new ArgumentNullException(nameof(handler));
ValidateEventType(eventType, "register");
var handlerInfo = EventHandlerInfo.Create(GetNextSubscriptionId(), handler, eventType, priority,
numericPriority, receiveCanceled,
debugInfo, originalMethod);
lock (_listenerLock)
{
GetOrCreateListenerList(eventType).Add(handlerInfo);
}
}
private IShrinkEventSubscription SubscribeEventInternal(Type eventType, Delegate handler, EventPriority priority,
int numericPriority, bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null)
{
if (eventType == null)
throw new ArgumentNullException(nameof(eventType));
if (handler == null)
throw new ArgumentNullException(nameof(handler));
ValidateEventType(eventType, "subscribe");
var subscriptionId = GetNextSubscriptionId();
var handlerInfo = EventHandlerInfo.Create(subscriptionId, handler, eventType, priority, numericPriority,
receiveCanceled, debugInfo, originalMethod);
lock (_listenerLock)
{
GetOrCreateListenerList(eventType).Add(handlerInfo);
}
return new EventSubscription(this, subscriptionId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase
{
if (handler == null)
return;
var collection = TryGetListenerList(typeof(TEvent));
collection?.Remove(handler);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase
{
if (handler == null)
return;
var collection = TryGetListenerList(typeof(TEvent));
collection?.Remove(handler);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase
{
var collection = TryGetListenerList(typeof(TEvent));
collection?.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UnregisterAllEventsForObject(object targetObject)
{
if (targetObject is null)
return;
// Keep the registration -> listener lock order used by RegisterCore.
// Removing only handlers is not enough: the target must be allowed to
// register again after a component-style teardown and reinitialization.
lock (_registrationLock)
{
_registeredTargets.Remove(targetObject);
lock (_listenerLock)
{
foreach (var kvp in _eventHandlers)
kvp.Value.RemoveTarget(targetObject);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UnregisterAllEvents()
{
// Match RegisterCore/UnregisterAllEventsForObject so teardown cannot
// deadlock with a concurrent registration.
lock (_registrationLock)
{
lock (_listenerLock)
{
foreach (var kvp in _eventHandlers)
kvp.Value.Clear();
}
_registeredTargets.Clear();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase
{
return TriggerEventAsyncInternal(eventArgs, null);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase
{
EnsurePerPhaseDispatchAllowed();
return TriggerEventAsyncInternal(eventArgs, phase);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase
{
return TriggerEventInternal(eventArgs, null);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase
{
EnsurePerPhaseDispatchAllowed();
return TriggerEventInternal(eventArgs, phase);
}
private async UniTask<bool> TriggerEventAsyncInternal(EventBase eventArgs, EventPriority? phase)
{
if (!TryPrepareDispatch(eventArgs, phase, out var eventType, out var handlers))
return false;
var wasHandled = false;
for (var i = 0; i < handlers.Length; i++)
{
var handlerInfo = handlers[i];
if (!TryPrepareHandler(eventArgs, handlerInfo))
continue;
try
{
if (handlerInfo.SyncInvoker != null)
{
handlerInfo.SyncInvoker(eventArgs);
wasHandled = true;
}
else if (handlerInfo.AsyncInvoker != null)
{
await handlerInfo.AsyncInvoker(eventArgs);
wasHandled = true;
}
}
catch (Exception ex)
{
if (HandleListenerException(eventArgs, handlers, i, ex))
throw;
}
}
eventArgs.CurrentHandler = null;
CompleteEventDispatch(eventArgs, eventType, handlers);
return wasHandled;
}
private bool TriggerEventInternal(EventBase eventArgs, EventPriority? phase)
{
if (!TryPrepareDispatch(eventArgs, phase, out var eventType, out var handlers))
return false;
var wasHandled = false;
for (var i = 0; i < handlers.Length; i++)
{
var handlerInfo = handlers[i];
if (!TryPrepareHandler(eventArgs, handlerInfo))
continue;
try
{
if (handlerInfo.SyncInvoker != null)
{
handlerInfo.SyncInvoker(eventArgs);
wasHandled = true;
}
else if (handlerInfo.AsyncInvoker != null)
{
var detachedEvent = EventCloneUtility.CloneForDetachedDispatch(eventArgs);
FireAndForgetSafe(() => handlerInfo.AsyncInvoker(detachedEvent),
$"{handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName}");
wasHandled = true;
}
}
catch (Exception ex)
{
if (HandleListenerException(eventArgs, handlers, i, ex))
throw;
}
}
eventArgs.CurrentHandler = null;
CompleteEventDispatch(eventArgs, eventType, handlers);
return wasHandled;
}
private bool TryPrepareDispatch(EventBase eventArgs, EventPriority? phase, out Type eventType,
out EventHandlerInfo[] handlers)
{
if (eventArgs == null)
throw new ArgumentNullException(nameof(eventArgs));
if (!IsStarted)
{
eventType = eventArgs.GetType();
handlers = Array.Empty<EventHandlerInfo>();
return false;
}
eventType = eventArgs.GetType();
ValidateDispatchEventType(eventType);
eventArgs.PrepareForDispatch();
lock (_listenerLock)
{
var listenerList = GetOrCreateListenerList(eventType);
handlers = phase.HasValue ? listenerList.GetHandlers(phase.Value) : listenerList.GetHandlers();
}
eventArgs.SetListenerSnapshot(handlers);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryPrepareHandler(EventBase eventArgs, EventHandlerInfo handlerInfo)
{
eventArgs.CurrentHandler = handlerInfo;
eventArgs.SetPhase(handlerInfo.Priority);
if (eventArgs.IsCancelable && eventArgs.IsCanceled && !handlerInfo.ReceiveCanceled)
return false;
return true;
}
private bool HandleListenerException(EventBase eventArgs, EventHandlerInfo[] handlers, int index, Exception exception)
{
_exceptionHandler?.HandleException(this, eventArgs, handlers, index, exception);
switch (_exceptionHandlingMode)
{
case ShrinkEventExceptionHandlingMode.Throw:
return true;
case ShrinkEventExceptionHandlingMode.LogAndThrow:
LogHandlerException(exception);
return true;
default:
LogHandlerException(exception);
return false;
}
}
private static void LogHandlerException(Exception exception)
{
Debug.LogException(exception);
Debug.LogError($"[EventBus] Handler exception: {exception.Message}");
}
private static void FireAndForgetSafe(Func<UniTask> action, string handlerName)
{
action().Forget(e =>
{
Debug.LogException(e);
Debug.LogError($"[EventBus] Async Handler {handlerName} threw exception: {e.Message}");
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CompleteEventDispatch(EventBase eventArgs, Type eventType, EventHandlerInfo[] handlers)
{
OnEventTriggered?.Invoke(eventArgs, eventType);
#if UNITY_EDITOR
var editorTraceHandler = OnEventTriggeredForEditor;
if (!EnableDebugRecord || editorTraceHandler == null)
return;
editorTraceHandler.Invoke(eventArgs, eventType.Name, GetSenderInfo(), handlers);
#endif
}
#if UNITY_EDITOR
private static string GetSenderInfo()
{
var senderInfo = "Unknown";
try
{
var trace = new System.Diagnostics.StackTrace(0, false);
for (var i = 0; i < trace.FrameCount; i++)
{
var method = trace.GetFrame(i)?.GetMethod();
var declaringType = method?.DeclaringType;
if (declaringType == null)
continue;
if (declaringType == typeof(EventBus) || declaringType.DeclaringType == typeof(EventBus))
continue;
if (declaringType == typeof(ShrinkEventBusInstance) ||
declaringType.DeclaringType == typeof(ShrinkEventBusInstance))
continue;
var ns = declaringType.Namespace ?? string.Empty;
if (ns.StartsWith("System") || ns.StartsWith("Cysharp") || ns.StartsWith("UnityEngine"))
continue;
var className = declaringType.Name;
var methodName = method!.Name;
if (declaringType.DeclaringType != null && className.StartsWith("<") && className.Contains(">"))
{
className = declaringType.DeclaringType.Name;
var startIndex = declaringType.Name.IndexOf('<') + 1;
var endIndex = declaringType.Name.IndexOf('>');
if (startIndex > 0 && endIndex > startIndex)
methodName = declaringType.Name.Substring(startIndex, endIndex - startIndex);
}
return $"{className}.{methodName}()";
}
}
catch
{
}
return senderInfo;
}
#endif
public EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase
{
var listenerList = TryGetListenerList(typeof(TEvent));
var handlers = listenerList?.GetHandlers();
if (handlers == null || handlers.Length == 0)
return Array.Empty<EventHandlerInfo>();
return (EventHandlerInfo[])handlers.Clone();
}
public ListenerList GetListenerList<TEvent>() where TEvent : EventBase
{
lock (_listenerLock)
{
return GetOrCreateListenerList(typeof(TEvent));
}
}
public IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot()
{
lock (_listenerLock)
{
var snapshot = new Dictionary<Type, EventHandlerInfo[]>(_eventHandlers.Count);
foreach (var kvp in _eventHandlers)
{
var handlers = kvp.Value.GetHandlers();
if (handlers.Length == 0)
continue;
snapshot[kvp.Key] = (EventHandlerInfo[])handlers.Clone();
}
return snapshot;
}
}
public IReadOnlyList<ShrinkEventSubscriptionSnapshot> GetActiveSubscriptionsSnapshot()
{
lock (_listenerLock)
{
var snapshot = new List<ShrinkEventSubscriptionSnapshot>();
foreach (var entry in _eventHandlers)
{
var eventType = entry.Key;
var handlers = entry.Value.GetHandlers();
for (var i = 0; i < handlers.Length; i++)
{
var handler = handlers[i];
snapshot.Add(new ShrinkEventSubscriptionSnapshot(
handler.SubscriptionId,
eventType,
handler.Priority,
handler.NumericPriority,
handler.ReceiveCanceled,
handler.Target,
handler.DisplayDeclaringType.FullName ?? handler.DisplayDeclaringType.Name,
handler.DisplayMethodName,
handler.DebugInfo,
handler.RegisteredAtUtc));
}
}
return snapshot;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsInstanceRegistered(object target)
{
if (target == null)
return false;
lock (_registrationLock)
{
return _registeredTargets.Contains(target);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRegisteredInstanceCount()
{
lock (_registrationLock)
{
return _registeredTargets.Count;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRegisteredEventTypeCount()
{
lock (_listenerLock)
{
var count = 0;
foreach (var listenerList in _eventHandlers.Values)
{
if (listenerList.LocalCount > 0)
count++;
}
return count;
}
}
private void RegisterCore(object target, bool requireSubscriberAttribute, bool lenientWhenNoMethods)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
lock (_registrationLock)
{
if (_registeredTargets.Contains(target))
return;
EventBusRegHelper.RegisterTarget(this, target, requireSubscriberAttribute, lenientWhenNoMethods);
_registeredTargets.Add(target);
}
}
private void RemoveHandlers(Predicate<EventHandlerInfo> predicate)
{
lock (_listenerLock)
{
foreach (var listenerList in _eventHandlers.Values)
listenerList.RemoveWhere(predicate);
}
}
private void UnregisterSubscription(long subscriptionId)
{
RemoveHandlers(info => info.SubscriptionId == subscriptionId);
}
private long GetNextSubscriptionId() => Interlocked.Increment(ref _nextSubscriptionId);
private ListenerList GetOrCreateListenerList(Type eventType)
{
if (_eventHandlers.TryGetValue(eventType, out var existing))
return existing;
ListenerList? parent = null;
var parentType = GetParentEventType(eventType);
if (parentType != null)
parent = GetOrCreateListenerList(parentType);
var created = new ListenerList(parent, _listenerLock);
_eventHandlers[eventType] = created;
return created;
}
private ListenerList? TryGetListenerList(Type eventType)
{
if (eventType == null)
return null;
lock (_listenerLock)
{
return _eventHandlers.TryGetValue(eventType, out var listenerList) ? listenerList : null;
}
}
private void ValidateEventType(Type eventType, string action)
{
if (!typeof(EventBase).IsAssignableFrom(eventType))
throw new ArgumentException(
$"Cannot {action} listener for {eventType.FullName}: type must inherit from EventBase.");
_eventClassChecker?.Invoke(eventType);
}
private void ValidateDispatchEventType(Type eventType)
{
if (!typeof(EventBase).IsAssignableFrom(eventType))
throw new ArgumentException($"Cannot dispatch event type {eventType.FullName}: not an EventBase.");
if (_checkTypesOnDispatch)
_eventClassChecker?.Invoke(eventType);
}
private static Type? GetParentEventType(Type eventType)
{
var current = eventType.BaseType;
while (current != null && current != typeof(object))
{
if (typeof(EventBase).IsAssignableFrom(current))
return current;
current = current.BaseType;
}
return null;
}
private void EnsurePerPhaseDispatchAllowed()
{
if (!_allowPerPhaseDispatch)
throw new InvalidOperationException(
"Per-phase event dispatch is disabled for this bus. Enable it via ShrinkEventBusBuilder.AllowPerPhaseDispatch().");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92c1c7cd96e37c8458600728c7c0c57f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f28409a6ba08444cbdbfc5eb37b1809
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusDispatchTests
{
private class PlainEvent : EventBase
{
}
private sealed class DerivedPlainEvent : PlainEvent
{
}
[Cancelable]
private sealed class CancelableEvent : EventBase
{
}
[HasResult]
private sealed class ResultEvent : EventBase
{
}
[Test]
public void TriggerEvent_ReturnsFalseWithoutSubscribers_TrueWhenHandled()
{
var bus = EventBus.CreateBus();
Assert.IsFalse(bus.TriggerEvent(new PlainEvent()));
bus.RegisterEvent<PlainEvent>(_ => { }, EventPriority.NORMAL);
Assert.IsTrue(bus.TriggerEvent(new PlainEvent()));
}
[Test]
public void CanceledEvent_SkipsHandlersUnlessReceiveCanceled()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<CancelableEvent>(evt =>
{
order.Add("canceler");
evt.SetCanceled(true);
}, EventPriority.HIGHEST);
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
bus.RegisterEvent<CancelableEvent>(evt => order.Add($"monitor:{evt.IsCanceled}"),
EventPriority.MONITOR, receiveCanceled: true);
var evt = new CancelableEvent();
bus.TriggerEvent(evt);
Assert.IsTrue(evt.IsCanceled);
CollectionAssert.AreEqual(new[] { "canceler", "monitor:True" }, order);
}
[Test]
public void SetCanceled_OnNonCancelableEvent_ThrowsWithMessage()
{
var evt = new PlainEvent();
var ex = Assert.Throws<UnsupportedOperationException>(() => evt.SetCanceled(true));
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
}
[Test]
public void SetResult_OnEventWithoutResult_ThrowsWithMessage()
{
var evt = new PlainEvent();
var ex = Assert.Throws<InvalidOperationException>(() => evt.SetResult(EventResult.ALLOW));
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
}
[Test]
public void SetResult_OnResultEvent_PersistsResult()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<ResultEvent>(evt => evt.SetResult(EventResult.DENY), EventPriority.HIGH);
var evt = new ResultEvent();
bus.TriggerEvent(evt);
Assert.AreEqual(EventResult.DENY, evt.Result);
}
[Test]
public void ParentSubscriber_ReceivesDerivedEvent()
{
var bus = EventBus.CreateBus();
var hits = 0;
bus.RegisterEvent<PlainEvent>(_ => hits++, EventPriority.NORMAL);
bus.TriggerEvent(new DerivedPlainEvent());
Assert.AreEqual(1, hits);
}
[Test]
public void ParentSubscriber_RegisteredAfterChildWasTriggered_StillReceivesChild()
{
var bus = EventBus.CreateBus();
// 先触发子事件,让子类型的监听列表先于父监听器创建(覆盖脏传播路径)
bus.TriggerEvent(new DerivedPlainEvent());
var hits = 0;
bus.RegisterEvent<PlainEvent>(_ => hits++, EventPriority.NORMAL);
bus.TriggerEvent(new DerivedPlainEvent());
Assert.AreEqual(1, hits);
}
[Test]
public void PerPhaseDispatch_OnlyRunsRequestedPhase()
{
var bus = EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch());
var order = new List<string>();
bus.RegisterEvent<PlainEvent>(_ => order.Add("high"), EventPriority.HIGH);
bus.RegisterEvent<PlainEvent>(_ => order.Add("normal"), EventPriority.NORMAL);
bus.TriggerEvent(EventPriority.HIGH, new PlainEvent());
CollectionAssert.AreEqual(new[] { "high" }, order);
}
[Test]
public void PerPhaseDispatch_ThrowsWhenNotEnabled()
{
var bus = EventBus.CreateBus();
Assert.Throws<InvalidOperationException>(() => bus.TriggerEvent(EventPriority.HIGH, new PlainEvent()));
}
[Test]
public void TriggerEventAsync_RunsHandlersSequentiallyByPriority()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
async UniTask AsyncHandler(PlainEvent evt)
{
order.Add("async-high");
await UniTask.CompletedTask;
}
bus.RegisterEvent<PlainEvent>(AsyncHandler, EventPriority.HIGH, receiveCanceled: false);
bus.RegisterEvent<PlainEvent>(_ => order.Add("sync-normal"), EventPriority.NORMAL);
var handled = bus.TriggerEventAsync(new PlainEvent()).GetAwaiter().GetResult();
Assert.IsTrue(handled);
CollectionAssert.AreEqual(new[] { "async-high", "sync-normal" }, order);
}
[Test]
public void TriggerEventAsync_AsyncHandlerCancellation_SkipsLaterHandlers()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
async UniTask CancelingHandler(CancelableEvent evt)
{
evt.SetCanceled(true);
order.Add("async-canceler");
await UniTask.CompletedTask;
}
bus.RegisterEvent<CancelableEvent>(CancelingHandler, EventPriority.HIGHEST, receiveCanceled: false);
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
bus.TriggerEventAsync(new CancelableEvent()).GetAwaiter().GetResult();
CollectionAssert.AreEqual(new[] { "async-canceler" }, order);
}
[Test]
public void GetSubscribers_ReturnsDefensiveCopyOfDispatchSnapshot()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<PlainEvent>(_ => { }, EventPriority.NORMAL);
var evt = new PlainEvent();
bus.TriggerEvent(evt);
var first = evt.GetSubscribers();
Assert.AreEqual(1, first.Length);
first[0] = null;
var second = evt.GetSubscribers();
Assert.IsNotNull(second[0]);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 11b7dddad0d215b47a7249e74a574105
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
using System.Collections.Generic;
using NUnit.Framework;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusPriorityTests
{
private class BaseEvent : EventBase
{
}
private sealed class DerivedEvent : BaseEvent
{
}
[Test]
public void EnumPriority_ExecutesPhasesInOrder()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("LOW"), EventPriority.LOW);
bus.RegisterEvent<BaseEvent>(_ => order.Add("MONITOR"), EventPriority.MONITOR);
bus.RegisterEvent<BaseEvent>(_ => order.Add("HIGHEST"), EventPriority.HIGHEST);
bus.RegisterEvent<BaseEvent>(_ => order.Add("NORMAL"), EventPriority.NORMAL);
bus.TriggerEvent(new BaseEvent());
CollectionAssert.AreEqual(new[] { "HIGHEST", "NORMAL", "LOW", "MONITOR" }, order);
}
[Test]
public void NumericPriority_HigherNumberRunsFirstWithinPhase()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
// 5 与 10 都映射到 NORMAL 档
bus.RegisterEvent<BaseEvent>(_ => order.Add("p5"), 5);
bus.RegisterEvent<BaseEvent>(_ => order.Add("p10"), 10);
bus.TriggerEvent(new BaseEvent());
CollectionAssert.AreEqual(new[] { "p10", "p5" }, order);
}
[Test]
public void NumericPriority_TieKeepsRegistrationOrder()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("first"), 0);
bus.RegisterEvent<BaseEvent>(_ => order.Add("second"), 0);
bus.TriggerEvent(new BaseEvent());
CollectionAssert.AreEqual(new[] { "first", "second" }, order);
}
[Test]
public void NumericPriorityZero_MapsToNormalPhase()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<BaseEvent>(_ => { }, 0);
var subscribers = bus.GetEventSubscribers<BaseEvent>();
Assert.AreEqual(1, subscribers.Length);
Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority);
}
[Test]
public void RegisterEvent_WithoutPriority_ResolvesToNormal()
{
var bus = EventBus.CreateBus();
void Handler(BaseEvent evt)
{
}
// 不带优先级的调用应唯一解析到枚举重载(NORMAL),不再有重载二义性
bus.RegisterEvent<BaseEvent>(Handler);
var subscribers = bus.GetEventSubscribers<BaseEvent>();
Assert.AreEqual(1, subscribers.Length);
Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority);
}
[Test]
public void ParentAndChildHandlers_MergeByNumericPriorityWithinPhase()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent10"), 10);
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child5"), 5);
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child1"), 1);
bus.TriggerEvent(new DerivedEvent());
CollectionAssert.AreEqual(new[] { "parent10", "child5", "child1" }, order);
}
[Test]
public void ParentAndChildHandlers_TiePrefersChild()
{
var bus = EventBus.CreateBus();
var order = new List<string>();
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent0"), 0);
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child0"), 0);
bus.TriggerEvent(new DerivedEvent());
CollectionAssert.AreEqual(new[] { "child0", "parent0" }, order);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc534f0e8598cab47b393ccffc90ad98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,228 @@
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusRegistrationTests
{
private class ProbeEvent : EventBase
{
}
private sealed class PooledEvent : EventBase
{
public int Value { get; set; }
protected override void OnReset()
{
Value = 0;
}
}
private sealed class InstanceSubscriber
{
public int Hits;
[EventSubscribe(EventPriority.NORMAL)]
private void OnProbe(ProbeEvent evt)
{
Hits++;
}
}
private static class StaticSubscriber
{
public static int Hits;
[EventSubscribe(EventPriority.NORMAL)]
private static void OnProbe(ProbeEvent evt)
{
Hits++;
}
}
private sealed class InvalidSignatureSubscriber
{
[EventSubscribe(EventPriority.NORMAL)]
private void OnProbe(ProbeEvent evt, int extra)
{
}
}
[EventBusSubscriber]
private sealed class EmptySubscriber
{
}
private sealed class UnattributedSubscriber
{
[EventSubscribe(EventPriority.NORMAL)]
private void OnProbe(ProbeEvent evt)
{
}
}
[Test]
public void Register_InstanceScan_RegistersAndUnregisters()
{
var bus = EventBus.CreateBus();
var subscriber = new InstanceSubscriber();
bus.Register(subscriber);
Assert.IsTrue(bus.IsInstanceRegistered(subscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, subscriber.Hits);
bus.Unregister(subscriber);
Assert.IsFalse(bus.IsInstanceRegistered(subscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, subscriber.Hits);
}
[Test]
public void Register_StaticTypeScan_RegistersAndUnregisters()
{
var bus = EventBus.CreateBus();
StaticSubscriber.Hits = 0;
bus.Register(typeof(StaticSubscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
bus.Unregister(typeof(StaticSubscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
}
[Test]
public void Register_MethodInfo_RegistersSingleHandler()
{
var bus = EventBus.CreateBus();
StaticSubscriber.Hits = 0;
var method = typeof(StaticSubscriber).GetMethod("OnProbe",
BindingFlags.Static | BindingFlags.NonPublic);
Assert.IsNotNull(method);
bus.Register(method);
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
bus.Unregister(method);
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, StaticSubscriber.Hits);
}
[Test]
public void Register_InvalidHandlerSignature_Throws()
{
var bus = EventBus.CreateBus();
Assert.Throws<InvalidOperationException>(() => bus.Register(new InvalidSignatureSubscriber()));
}
[Test]
public void AutoRegister_TypeWithoutSubscriberAttribute_Throws()
{
var bus = EventBus.CreateBus();
Assert.Throws<InvalidOperationException>(() => bus.AutoRegister(new UnattributedSubscriber()));
}
[Test]
public void AutoRegister_SubscriberWithoutHandlers_WarnsInsteadOfThrowing()
{
var bus = EventBus.CreateBus();
LogAssert.Expect(LogType.Warning, new Regex(@"has no \[EventSubscribe\] methods"));
Assert.DoesNotThrow(() => bus.AutoRegister(new EmptySubscriber()));
}
[Test]
public void GetEventSubscribers_ReturnsDefensiveCopy()
{
var bus = EventBus.CreateBus();
bus.RegisterEvent<ProbeEvent>(_ => { }, EventPriority.NORMAL);
var first = bus.GetEventSubscribers<ProbeEvent>();
Assert.AreEqual(1, first.Length);
first[0] = null;
var second = bus.GetEventSubscribers<ProbeEvent>();
Assert.IsNotNull(second[0]);
}
[Test]
public void UnregisterAllEventsForObject_RemovesScannedHandlers()
{
var bus = EventBus.CreateBus();
var subscriber = new InstanceSubscriber();
bus.Register(subscriber);
bus.UnregisterAllEventsForObject(subscriber);
Assert.IsFalse(bus.IsInstanceRegistered(subscriber));
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(0, subscriber.Hits);
bus.Register(subscriber);
bus.TriggerEvent(new ProbeEvent());
Assert.AreEqual(1, subscriber.Hits);
}
[Test]
public void EventPool_ReleaseResetsAndReusesInstance()
{
var evt = EventPool<PooledEvent>.Get();
evt.Value = 42;
EventPool<PooledEvent>.Release(evt);
var reused = EventPool<PooledEvent>.Get();
Assert.AreSame(evt, reused);
Assert.AreEqual(0, reused.Value);
EventPool<PooledEvent>.Release(reused);
}
[Test]
public void EventPool_DoubleRelease_DoesNotDuplicatePoolEntry()
{
var evt = EventPool<PooledEvent>.Get();
EventPool<PooledEvent>.Release(evt);
EventPool<PooledEvent>.Release(evt);
var first = EventPool<PooledEvent>.Get();
var second = EventPool<PooledEvent>.Get();
Assert.AreSame(evt, first);
Assert.AreNotSame(evt, second);
EventPool<PooledEvent>.Release(first);
EventPool<PooledEvent>.Release(second);
}
[Test]
public void EventPool_DisposeReturnsToPool()
{
PooledEvent captured;
using (var evt = EventPool<PooledEvent>.Get())
{
captured = evt;
evt.Value = 7;
}
var reused = EventPool<PooledEvent>.Get();
Assert.AreSame(captured, reused);
Assert.AreEqual(0, reused.Value);
EventPool<PooledEvent>.Release(reused);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4570cac2813143c40b9a8edeadf19057
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using NUnit.Framework;
using UnityEngine;
namespace ShrinkEventBus.Tests
{
[TestFixture]
public sealed class EventBusSubscriptionTests
{
private sealed class SampleEvent : EventBase
{
public int Value { get; set; }
}
[SetUp]
public void SetUp()
{
EventBus.UnregisterAllEvents();
}
[TearDown]
public void TearDown()
{
EventBus.UnregisterAllEvents();
}
[Test]
public void SubscribeEvent_Dispose_RemovesOnlyOwnedSubscription()
{
var firstHits = 0;
var secondHits = 0;
using var first = EventBus.SubscribeEvent<SampleEvent>(_ => firstHits++);
using var second = EventBus.SubscribeEvent<SampleEvent>(_ => secondHits++);
EventBus.TriggerEvent(new SampleEvent());
Assert.AreEqual(1, firstHits);
Assert.AreEqual(1, secondHits);
first.Dispose();
EventBus.TriggerEvent(new SampleEvent());
Assert.AreEqual(1, firstHits);
Assert.AreEqual(2, secondHits);
}
[Test]
public void ActiveSubscriptionSnapshot_ContainsExpectedMetadata()
{
using var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => { }, EventPriority.HIGH, true);
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
Assert.AreEqual(1, snapshots.Count);
Assert.AreEqual(subscription.SubscriptionId, snapshots[0].SubscriptionId);
Assert.AreEqual(typeof(SampleEvent), snapshots[0].EventType);
Assert.AreEqual(EventPriority.HIGH, snapshots[0].Priority);
Assert.IsTrue(snapshots[0].ReceiveCanceled);
Assert.IsFalse(string.IsNullOrWhiteSpace(snapshots[0].MethodName));
}
[Test]
public void RegisterEvent_StillWorksWithoutDisposableSubscription()
{
var hits = 0;
void Handler(SampleEvent evt)
{
hits += evt.Value;
}
EventBus.RegisterEvent<SampleEvent>(Handler);
EventBus.TriggerEvent(new SampleEvent { Value = 3 });
Assert.AreEqual(3, hits);
EventBus.UnregisterEvent<SampleEvent>(Handler);
EventBus.TriggerEvent(new SampleEvent { Value = 5 });
Assert.AreEqual(3, hits);
}
[Test]
public void ActiveSubscriptionSnapshot_EmptyAfterDispose()
{
var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => Debug.Log("noop"));
subscription.Dispose();
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
Assert.AreEqual(0, snapshots.Count);
Assert.IsTrue(subscription.IsDisposed);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5ea531619ceaeb46af4f56454624ef7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
{
"name": "ShrinkEventBus.Tests",
"rootNamespace": "ShrinkEventBus.Tests",
"references": [
"ShrinkEventBus.Runtime",
"UniTask",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8c47d36b8919bbf4d9c39817eaf577a9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+169
View File
@@ -0,0 +1,169 @@
fileFormatVersion: 2
guid: c0d34cdfe6432fc45b8e3aa75065b201
TextureImporter:
internalIDToNameTable:
- first:
213: -6576374159750988736
second: img1_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: img1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 659
height: 658
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 04cfbf97b860cb4a0800000000000000
internalID: -6576374159750988736
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
img1_0: -6576374159750988736
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+169
View File
@@ -0,0 +1,169 @@
fileFormatVersion: 2
guid: 265b481fbf28c6949a2e93bcfcdbe946
TextureImporter:
internalIDToNameTable:
- first:
213: -1076633883802439962
second: img2_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: img2_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 659
height: 658
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 6ee6ecadf270f01f0800000000000000
internalID: -1076633883802439962
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
img2_0: -1076633883802439962
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
{
"name": "com.cneicy.shrink-eventbus",
"version": "1.3.0",
"displayName": "ShrinkEventBus",
"description": "高性能、类型安全的 Unity 事件总线,支持优先级调度与自动注册。",
"unity": "2022.3",
"documentationUrl": "https://github.com/cneicy/ShrinkEventBus",
"changelogUrl": "https://github.com/cneicy/ShrinkEventBus/blob/main/CHANGELOG.md",
"licensesUrl": "https://github.com/cneicy/ShrinkEventBus/blob/main/LICENSE",
"dependencies": {
"com.cysharp.unitask": "2.5.10",
"com.unity.nuget.mono-cecil": "1.11.4"
},
"keywords": ["eventbus", "event", "messaging", "unitask"],
"author": {
"name": "cneicy",
"url": "https://github.com/cneicy"
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 09a3fe335f19475db0839afd05b0d8cb
timeCreated: 1772900717