This commit is contained in:
cneicy committed 2026-03-08 00:40:13 +08:00
1 parent 3dfbccc9d6
commit 8549ba5c80
38 files changed
+2187

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8bf98940dc144bd2bffd55ce21295bbd
timeCreated: 1772878399
+473
View File
@@ -0,0 +1,473 @@
#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 ScrollView _subscribersScrollView;
private ScrollView _liveTrackerScrollView;
private VisualElement _subscribersTab;
private VisualElement _liveTrackerTab;
private Label _instanceCountLabel;
private string _searchString = "";
private const int MaxLogCount = 100;
private readonly List<VisualElement> _logElements = new();
[MenuItem("Window/Shrink EventBus/事件查看器")]
public static void ShowWindow()
{
var window = GetWindow<EventBusViewerWindow>("EventBus 控制台");
window.minSize = new Vector2(500, 600);
window.Show();
}
private void OnEnable() => EventBus.OnEventTriggeredForEditor += RecordEventLog;
private void OnDisable() => EventBus.OnEventTriggeredForEditor -= RecordEventLog;
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();
toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" });
container.Add(toolbar);
_liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } };
container.Add(_liveTrackerScrollView);
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();
}
private void RefreshSubscribersView()
{
_subscribersScrollView.Clear();
if (!Application.isPlaying)
{
_subscribersScrollView.Add(new HelpBox("请先运行游戏,系统初始化后即可查看内存中的事件树。", HelpBoxMessageType.Info));
return;
}
var field = typeof(EventBus).GetField("EventHandlers", BindingFlags.NonPublic | BindingFlags.Static);
if (field == null) return;
var dict = field.GetValue(null) as IDictionary;
if (dict == null) return;
foreach (DictionaryEntry entry in dict)
{
var eventType = entry.Key as Type;
if (!string.IsNullOrEmpty(_searchString) &&
!eventType.Name.ToLower().Contains(_searchString.ToLower())) continue;
var listenerListObj = entry.Value;
var handlers =
listenerListObj.GetType().GetMethod("GetHandlers")?.Invoke(listenerListObj, null) as Array;
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)
{
if (!EventBus.EnableDebugRecord) return;
var list = evt.GetListenerList();
var hasListeners = list != null && list.Count > 0;
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(DateTime.Now.ToString("HH:mm:ss.fff"))
{ style = { width = 80, color = new StyleColor(Color.gray) } });
headerRow.Add(new Label(eventName)
{
style = { color = new StyleColor(new Color(1f, 0.84f, 0f)), unityFontStyleAndWeight = FontStyle.Bold }
});
var senderClassName = senderInfo.Contains(".") ? senderInfo.Split('.')[0] : senderInfo;
var senderLabel = new Label($" [由 {senderInfo} 触发]")
{
style =
{
color = new StyleColor(new Color(0.5f, 0.8f, 1f)), marginLeft = 5,
unityFontStyleAndWeight = FontStyle.Italic
}
};
if (senderClassName != "未知来源" && senderClassName != "EventBus")
{
senderLabel.RegisterCallback<MouseEnterEvent>(e =>
senderLabel.style.color = new StyleColor(Color.cyan));
senderLabel.RegisterCallback<MouseLeaveEvent>(e =>
senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f)));
senderLabel.RegisterCallback<ClickEvent>(e => OpenScriptByClassName(senderClassName));
}
headerRow.Add(senderLabel);
logEntry.Add(headerRow);
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";
var statusRow = new VisualElement
{ style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } };
statusRow.Add(new Label($"可取消: {(isCancelable ? "是" : "否")}")
{ style = { color = new StyleColor(Color.gray), marginRight = 15 } });
var canceledLabel = new Label($"已取消: {(isCanceled ? "是" : "否")}") { style = { marginRight = 15 } };
canceledLabel.style.color =
isCanceled ? new StyleColor(new Color(1f, 0.3f, 0.3f)) : new StyleColor(Color.gray);
statusRow.Add(canceledLabel);
statusRow.Add(new Label($"Result: {resultStr}")
{ style = { color = new StyleColor(new Color(0.8f, 0.6f, 0.8f)), marginRight = 15 } });
statusRow.Add(new Label($"最后阶段: {phaseStr}")
{ 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 } };
var paramsList = DumpEventParams(evt);
if (paramsList.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 paramsList)
paramsContainer.Add(new Label($" • {pStr}")
{ style = { color = new StyleColor(new Color(0.6f, 0.9f, 0.6f)), marginLeft = 10 } });
}
logEntry.Add(paramsContainer);
if (hasListeners)
{
if (list.GetType().GetMethod("GetHandlers")?.Invoke(list, null) is Array handlers)
{
foreach (var h in handlers)
{
var row = CreateHandlerRow(h);
row.style.marginLeft = 80;
logEntry.Add(row);
}
}
}
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);
}
_liveTrackerScrollView.Insert(0, logEntry);
_logElements.Insert(0, logEntry);
if (_logElements.Count > MaxLogCount)
{
_liveTrackerScrollView.Remove(_logElements[^1]);
_logElements.RemoveAt(_logElements.Count - 1);
}
}
private VisualElement CreateHandlerRow(object h)
{
ParseHandlerData(h, out var targetInstance, out var targetName, out var methodName, out var priority,
out var execType);
var row = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row, marginTop = 2, marginBottom = 2, marginLeft = 40,
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 (targetInstance is MonoBehaviour mb && mb != null)
{
var pingBtn = new Button(() =>
{
EditorGUIUtility.PingObject(mb.gameObject);
Selection.activeGameObject = mb.gameObject;
}) { text = "Ping", style = { width = 50, height = 18 } };
row.Add(pingBtn);
}
else if (targetInstance == null && 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 void ParseHandlerData(object handlerObj, out object targetInstance, out string targetName,
out string methodName, out object priority, out string execType)
{
priority = GetMemberValue(handlerObj, "Priority") ?? "NORMAL";
var debugInfo = GetMemberValue(handlerObj, "DebugInfo") as string ?? "未知方法";
execType = "Sync";
if (debugInfo.Contains("(UniTask)") || debugInfo.Contains("Async")) execType = "Async";
else if (debugInfo.Contains("(Task->UniTask)") || debugInfo.Contains("(Task)")) execType = "Task";
targetInstance = null;
targetName = "未知类";
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;
}
}
}
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()
{
_liveTrackerScrollView.Clear();
_logElements.Clear();
}
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
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16e1b18e2a2748e494f2ba648b4697a8
timeCreated: 1772878417
+13
View File
@@ -0,0 +1,13 @@
{
"name": "ShrinkEventBus.Editor",
"rootNamespace": "ShrinkEventBus.Editor",
"references": [
"ShrinkEventBus.Runtime",
"UniTask"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0a89bdb25764f1f8a8e4dcc915abe39
timeCreated: 1772900825
+437
View File
@@ -0,0 +1,437 @@
# ShrinkEventBus
一个为 Unity C# 项目设计的高性能、类型安全事件总线系统。支持优先级调度、自动注册、同步/异步混合处理,以及完整的 MonoBehaviour 生命周期管理。
## ✨ 特性概览
| 特性 | 说明 |
|------|------|
| 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 |
| ⚡ **高性能热路径** | 泛型静态缓存 `EventCache<T>` 绕过字典查找,触发路径几乎零开销 |
| 🤖 **自动注册** | 标记 `[EventBusSubscriber]` 即可,无需手动注册/注销,自动绑定 GameObject 生命周期 |
| 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 |
| 🔄 **同步 & 异步** | 统一支持 `Action`、`UniTask`、`Task` 三种 handler 形式 |
| 🧵 **线程安全** | 注册/注销操作全程加锁保护 |
| 📦 **对象池** | 内置 `EventPool<T>`,高频事件零 GC |
| 🔍 **调试友好** | Editor 事件追踪、调用栈溯源、订阅者信息查询 |
## 📦 依赖
- Unity 2021.3+
- [UniTask](https://github.com/Cysharp/UniTask) `2.x`
## 🚀 快速上手
### 第一步:定义事件
所有事件必须继承 `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]` 标记处理方法,系统会在场景加载时自动完成注册,并在 GameObject 销毁时自动清理。
```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 块结束时自动归还到池中
```
---
## 📖 核心概念
### 优先级系统
`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 |
**推荐的优先级分工:**
```
HIGHEST — 权限校验、合法性检查
HIGH — 核心业务逻辑、数值计算
NORMAL — 默认行为、状态变更
LOW — UI 更新、音效、特效
LOWEST — 收尾清理
MONITOR — 日志、统计、监控(通常配合 receiveCanceled: 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 | ✅ 随 GameObject 销毁 |
| `EventBus.RegisterEvent(...)` 手动注册 | 非 MonoBehaviour 类、Lambda | ❌ 需手动调用 `UnregisterEvent` |
| `EventBus.AutoRegister(this)` | 运行时动态注册节点 | ✅ 需配合 `EventBusDestroyListener` |
**手动注册示例(非 MonoBehaviour):**
```csharp
public class InventorySystem : IDisposable
{
public InventorySystem()
{
EventBus.RegisterEvent<ItemPickupEvent>(OnItemPickup, EventPriority.NORMAL);
}
private void OnItemPickup(ItemPickupEvent evt) { /* ... */ }
public void Dispose()
{
// 必须手动清理,否则 handler 持有 this 引用会造成内存泄漏
EventBus.UnregisterAllEventsForObject(this);
}
}
```
---
## 🔧 API 参考
### EventBus(静态门面)
#### 注册 / 注销
```csharp
// 同步 handler
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, int priority);
// 异步 handler(UniTask)
EventBus.RegisterEvent<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
// 同步触发:async handler 会以 .Forget() 方式触发,不等待
bool handled = EventBus.TriggerEvent<TEvent>(TEvent eventArgs);
// 异步触发:顺序 await 每个 handler
bool handled = await EventBus.TriggerEventAsync<TEvent>(TEvent eventArgs);
```
> ⚠️ `TriggerEvent` 中遇到 async handler 时会调用 `.Forget()`,不会等待其完成。如果需要顺序等待,请使用 `TriggerEventAsync`。
#### 查询
```csharp
EventBus.IsInstanceRegistered(object target);
EventBus.GetRegisteredInstanceCount();
EventBus.GetRegisteredEventTypeCount();
EventBus.GetEventSubscribers<TEvent>(); // 返回 EventHandlerInfo[]
EventBus.GetListenerList<TEvent>();
```
### 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 列表快照(调试用)
```
---
## 🏗️ 架构说明
```
EventBus (静态门面)
├── EventCache<T> 泛型静态缓存,热路径绕过字典
├── ListenerList 线程安全的有序 handler 列表
├── EventHandlerInfo 单个 handler 的元信息(优先级、方法反射、调试信息)
├── EventBase 所有事件的基类,携带生命周期状态
├── EventPool<T> 对象池,高频事件减少 GC
├── EventBusRegHelper 反射扫描 & handler 注册逻辑
├── EventAutoRegHelper 运行时自动扫描场景内 MonoBehaviour
└── EventBusDestroyListener 挂在 GameObject 上监听 OnDestroy 自动反注册
```
**热路径(`TriggerEvent`)工作流:**
```
TriggerEvent(evt)
└─ 读取 EventCache<T>.List // 静态字段,O(1),无字典查找
└─ GetHandlers() // 返回内部数组引用,无拷贝
└─ 遍历 handlers[]
├─ 跳过已取消 & 不接收取消的 handler
├─ Action<T> → 直接调用
└─ Func<T, UniTask> → .Forget()(同步路径)
```
**自动注册流程:**
```
[RuntimeInitializeOnLoadMethod(BeforeSceneLoad)]
└─ 扫描所有 Assembly,收集 [EventBusSubscriber] 类型
[RuntimeInitializeOnLoadMethod(AfterSceneLoad)]
└─ FindObjectsByType 找到场景内已存在的 MonoBehaviour
└─ AutoRegister(mb)
├─ 反射扫描 [EventSubscribe] 方法并注册
└─ 在 GameObject 上挂载 EventBusDestroyListener
└─ OnDestroy → UnregisterInstance(target)
```
---
## ✅ 最佳实践
**事件设计:尽量让属性只读**
```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 以 `.Forget()` 触发,执行结果和异常不会传回调用方。需要等待时请使用 `TriggerEventAsync`。
- **EventPool 归还后不要再使用**:`Release` 后对象会立即 `ResetInternal()`,继续访问属性将得到默认值。
- **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 数组(虽然不会崩溃,但会产生语义上的不确定性)。
- **静态 handler 永远不会自动注销**:静态方法注册后会持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。
- **`[EventBusSubscriber]` 仅对 MonoBehaviour 生效自动注册**:非 MonoBehaviour 类标记该 Attribute 无任何效果,请使用手动注册。
---
## 🐛 常见问题排查
**事件没有被任何 handler 接收**
1. 检查订阅类是否有 `[EventBusSubscriber]`
2. 检查方法是否有 `[EventSubscribe]`,且方法签名为 `void/UniTask/Task Method(TEvent evt)`
3. 检查 GameObject 是否在场景中且 `Active`(`FindObjectsByType` 包含 Inactive,但如果节点不在场景树则不会被扫描)
4. 确认没有在 `Awake` 之前就触发事件(自动注册在 `AfterSceneLoad` 完成)
```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 下想追踪事件流**
```csharp
EventBus.EnableDebugRecord = true;
// 触发事件后检查
EventBus.TriggerEvent(evt);
foreach (var h in evt.GetSubscribers())
Debug.Log($"[{h.Priority}] {h.DisplayDeclaringType.Name}.{h.DisplayMethodName}");
```
---
## 📄 License
MIT
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e08e301c95a34847a1071cd96bba1eeb
timeCreated: 1772900157
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c410edd1d6b644b498d39ff5e1fbc98b
timeCreated: 1772900792
+42
View File
@@ -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;
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9bb9d82b04a04f3085b9e68ab4f1ba60
timeCreated: 1760098802
+136
View File
@@ -0,0 +1,136 @@
using System;
using System.Collections.Concurrent;
using System.Reflection;
using UnityEngine;
namespace ShrinkEventBus
{
public static class EventAutoRegHelper
{
private static readonly ConcurrentDictionary<Type, bool> SubscriberTypes = new();
private static readonly object InitLock = new();
public static bool IsInitialized { get; private set; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void PreInitializeReflection()
{
if (SubscriberTypes.Count == 0) ScanEventBusSubscribers();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void InitializeAfterSceneLoad()
{
EnsureInitialized();
}
public static void EnsureInitialized()
{
if (IsInitialized) return;
lock (InitLock)
{
if (IsInitialized) return;
ScanEventBusSubscribers();
ScanExistingObjects();
IsInitialized = true;
}
}
private static void ScanExistingObjects()
{
if (SubscriberTypes.Count == 0)
{
#if UNITY_2023_1_OR_NEWER
var allMonoBehaviours =
UnityEngine.Object.FindObjectsByType<MonoBehaviour>(UnityEngine.FindObjectsInactive.Include,
UnityEngine.FindObjectsSortMode.None);
#else
var allMonoBehaviours = UnityEngine.Object.FindObjectsOfType<MonoBehaviour>(true);
#endif
foreach (var mb in allMonoBehaviours) ProcessNodeRegistration(mb);
return;
}
foreach (var targetType in SubscriberTypes.Keys)
{
#if UNITY_2023_1_OR_NEWER
var objects = UnityEngine.Object.FindObjectsByType(targetType, UnityEngine.FindObjectsInactive.Include,
UnityEngine.FindObjectsSortMode.None);
#else
var objects = UnityEngine.Object.FindObjectsOfType(targetType, true);
#endif
foreach (var obj in objects) ProcessNodeRegistration(obj);
}
}
private static void ScanEventBusSubscribers()
{
if (SubscriberTypes.Count > 0) return;
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
if (IsSystemAssembly(assembly)) continue;
try
{
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<EventBusSubscriberAttribute>() != null &&
typeof(MonoBehaviour).IsAssignableFrom(type))
SubscriberTypes.TryAdd(type, true);
}
}
catch (ReflectionTypeLoadException ex)
{
foreach (var type in ex.Types)
{
if (type?.GetCustomAttribute<EventBusSubscriberAttribute>() != null &&
typeof(MonoBehaviour).IsAssignableFrom(type))
SubscriberTypes.TryAdd(type, true);
}
}
catch
{
// ignored
}
}
}
private static bool IsSystemAssembly(Assembly assembly)
{
var name = assembly.FullName ?? "";
return name.StartsWith("System.") || name.StartsWith("Microsoft.") || name.StartsWith("mscorlib") ||
name.StartsWith("UnityEngine.") || name.StartsWith("UnityEditor.");
}
private static bool ProcessNodeRegistration(object node)
{
if (node == null) return false;
var nodeType = node.GetType();
if (SubscriberTypes.Count == 0 && nodeType.GetCustomAttribute<EventBusSubscriberAttribute>() != null)
SubscriberTypes.TryAdd(nodeType, true);
if (!SubscriberTypes.ContainsKey(nodeType)) return false;
if (EventBus.IsInstanceRegistered(node)) return false;
EventBus.AutoRegister(node);
return true;
}
public static void UnregisterNode(object node)
{
if (node == null) return;
EventBus.UnregisterInstance(node);
}
public static void ForceScanCurrentScene() => ScanExistingObjects();
public static void Cleanup()
{
lock (InitLock)
{
SubscriberTypes.Clear();
IsInitialized = false;
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f4d79f65274145cab7614950a64a92f5
timeCreated: 1760099405
+84
View File
@@ -0,0 +1,84 @@
using System;
using System.Reflection;
namespace ShrinkEventBus
{
public abstract class EventBase : IDisposable
{
private readonly ListenerList _listenerList = new();
private bool _isCanceled;
private EventResult _result = EventResult.DEFAULT;
public EventHandlerInfo? CurrentHandler { get; internal set; }
public DateTime EventTime { get; private set; } = DateTime.UtcNow;
public Guid EventId { get; private set; } = Guid.NewGuid();
public bool IsCancelable { get; }
public bool HasResult { get; }
public EventPriority? Phase { get; private set; }
internal bool IsInPool { get; set; }
internal Action<EventBase> ReleaseAction { get; set; }
protected EventBase()
{
IsCancelable = GetType().GetCustomAttribute<CancelableAttribute>() != null;
HasResult = GetType().GetCustomAttribute<HasResultAttribute>() != null;
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 = Guid.NewGuid();
_listenerList.Clear();
OnReset();
}
public bool IsCanceled
{
get => _isCanceled;
set
{
if (!IsCancelable) throw new UnsupportedOperationException();
_isCanceled = value;
}
}
public EventResult Result
{
get => _result;
set
{
if (!HasResult) throw new InvalidOperationException();
_result = value;
}
}
internal void SetPhase(EventPriority value)
{
if (Phase == value) return;
if (Phase != null && Phase.Value.CompareTo(value) > 0)
throw new ArgumentException();
Phase = value;
}
public void SetCanceled(bool canceled) => IsCanceled = canceled;
public void SetResult(EventResult result) => Result = result;
public ListenerList GetListenerList() => _listenerList;
public EventHandlerInfo[] GetSubscribers() => _listenerList.GetHandlers();
public void Dispose()
{
ReleaseAction?.Invoke(this);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b978c5174cf44d893a41950e7a504e3
timeCreated: 1760098714
+451
View File
@@ -0,0 +1,451 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace ShrinkEventBus
{
public static class EventBus
{
public static bool EnableDebugRecord { get; set; }
#if UNITY_EDITOR
public static event Action<EventBase, string, string> OnEventTriggeredForEditor;
#endif
private static readonly Dictionary<Type, ListenerList> EventHandlers = new();
private static readonly HashSet<object> RegisteredInstances = new();
private static readonly object InstanceLock = new();
static EventBus()
{
EventBusRegHelper.RegStaticEventHandler();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EnsureAutoManagerInitialized() => EventAutoRegHelper.EnsureInitialized();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegisterEvent<TEvent>(Action<TEvent> handler, string methodName, Type declaringType,
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase
{
var methodInfo = declaringType.GetMethod(methodName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled,
$"Manual Sync Handler (Priority: {priority})", methodInfo);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority = 0)
where TEvent : EventBase
{
var eventPriority = PriorityHelper.ConvertToEventPriority(priority);
RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false,
$"Manual Async Handler (Priority: {priority})");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static 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 static void RegisterEvent<TEvent>(Action<TEvent> handler, int priority = 0) 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 static 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)]
internal static void RegisterEventInternal(Type eventType, Delegate handler, EventPriority priority,
int numericPriority, bool receiveCanceled, string debugInfo = "", MethodInfo originalMethod = null)
{
lock (EventHandlers)
{
if (!EventHandlers.TryGetValue(eventType, out var collection))
{
collection = new ListenerList();
EventHandlers[eventType] = collection;
var cacheType = typeof(EventCache<>).MakeGenericType(eventType);
cacheType.GetField("List", BindingFlags.Static | BindingFlags.Public)?.SetValue(null, collection);
}
collection.Add(handler, priority, numericPriority, receiveCanceled, debugInfo, originalMethod);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase
{
var eventType = typeof(TEvent);
lock (EventHandlers)
{
if (!EventHandlers.TryGetValue(eventType, out var collection)) return;
collection.Remove(handler);
if (collection.Count == 0)
{
EventHandlers.Remove(eventType);
EventCache<TEvent>.List = null;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase
{
var eventType = typeof(TEvent);
lock (EventHandlers)
{
if (!EventHandlers.TryGetValue(eventType, out var collection)) return;
collection.Remove(handler);
if (collection.Count == 0)
{
EventHandlers.Remove(eventType);
EventCache<TEvent>.List = null;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase
{
var eventType = typeof(TEvent);
lock (EventHandlers)
{
if (EventHandlers.Remove(eventType))
{
EventCache<TEvent>.List = null;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterAllEventsForObject(object targetObject)
{
if (targetObject is null) return;
lock (EventHandlers)
{
var eventTypesToRemove = new List<Type>();
foreach (var kvp in EventHandlers)
{
kvp.Value.RemoveTarget(targetObject);
if (kvp.Value.Count == 0) eventTypesToRemove.Add(kvp.Key);
}
foreach (var eventType in eventTypesToRemove)
{
EventHandlers.Remove(eventType);
var cacheType = typeof(EventCache<>).MakeGenericType(eventType);
cacheType.GetField("List", BindingFlags.Static | BindingFlags.Public)?.SetValue(null, null);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterInstance(object targetObject)
{
if (targetObject is null) return;
lock (InstanceLock)
{
RegisteredInstances.Remove(targetObject);
}
UnregisterAllEventsForObject(targetObject);
if (targetObject is not MonoBehaviour mb || !mb) return;
try
{
if (!mb.gameObject ||
!mb.gameObject.TryGetComponent<EventBusDestroyListener>(out var listener)) return;
if (listener)
{
UnityEngine.Object.Destroy(listener);
}
}
catch (MissingReferenceException)
{
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterAllEvents()
{
lock (EventHandlers)
{
foreach (var type in EventHandlers.Keys)
{
var cacheType = typeof(EventCache<>).MakeGenericType(type);
cacheType.GetField("List", BindingFlags.Static | BindingFlags.Public).SetValue(null, null);
}
EventHandlers.Clear();
}
lock (InstanceLock)
{
RegisteredInstances.Clear();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase
{
EnsureAutoManagerInitialized();
var collection = EventCache<TEvent>.List;
if (collection == null) return false;
var handlers = collection.GetHandlers();
var wasHandled = false;
#if UNITY_EDITOR
RecordEditorTrace(handlers, eventArgs, typeof(TEvent));
#endif
for (var i = 0; i < handlers.Length; i++)
{
var handlerInfo = handlers[i];
eventArgs.CurrentHandler = handlerInfo;
eventArgs.SetPhase(handlerInfo.Priority);
if (eventArgs is { IsCancelable: true, IsCanceled: true } && !handlerInfo.ReceiveCanceled) continue;
try
{
if (handlerInfo.Handler is Action<TEvent> syncHandler)
{
syncHandler(eventArgs);
wasHandled = true;
}
else if (handlerInfo.Handler is Func<TEvent, UniTask> asyncHandler)
{
await asyncHandler(eventArgs);
wasHandled = true;
}
}
catch (Exception ex)
{
Debug.LogException(ex);
Debug.LogError(
$"[EventBus] {typeof(TEvent).Name} Async Handler {handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName} Exception: {ex.Message}");
}
}
eventArgs.CurrentHandler = null;
return wasHandled;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase
{
EnsureAutoManagerInitialized();
var collection = EventCache<TEvent>.List;
if (collection == null) return false;
var handlers = collection.GetHandlers();
var wasHandled = false;
#if UNITY_EDITOR
RecordEditorTrace(handlers, eventArgs, typeof(TEvent));
#endif
for (var i = 0; i < handlers.Length; i++)
{
var handlerInfo = handlers[i];
eventArgs.CurrentHandler = handlerInfo;
eventArgs.SetPhase(handlerInfo.Priority);
if (eventArgs is { IsCancelable: true, IsCanceled: true } && !handlerInfo.ReceiveCanceled) continue;
try
{
if (handlerInfo.Handler is Action<TEvent> syncHandler)
{
syncHandler(eventArgs);
wasHandled = true;
}
else if (handlerInfo.Handler is Func<TEvent, UniTask> asyncHandler)
{
FireAndForgetSafe(() => asyncHandler(eventArgs),
$"{handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName}");
wasHandled = true;
}
}
catch (Exception ex)
{
Debug.LogException(ex);
Debug.LogError(
$"[EventBus] {typeof(TEvent).Name} Sync Handler {handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName} Exception: {ex.Message}");
}
}
eventArgs.CurrentHandler = null;
return wasHandled;
}
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}");
});
}
#if UNITY_EDITOR
private static void RecordEditorTrace<TEvent>(EventHandlerInfo[] handlers, TEvent eventArgs, Type eventType)
where TEvent : EventBase
{
if (!EnableDebugRecord) return;
for (var i = 0; i < handlers.Length; i++)
{
var handler = handlers[i];
MethodInfo originalMethod = null;
var type = handler.GetType();
var prop = type.GetProperty("OriginalMethod");
if (prop != null) originalMethod = prop.GetValue(handler) as MethodInfo;
else
{
var field = type.GetField("OriginalMethod");
if (field != null) originalMethod = field.GetValue(handler) as MethodInfo;
}
eventArgs.GetListenerList().Add(handler.Handler, handler.Priority, handler.NumericPriority,
handler.ReceiveCanceled, handler.DebugInfo, originalMethod);
}
OnEventTriggeredForEditor?.Invoke(eventArgs, eventType.Name, GetSenderInfo());
}
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;
var ns = declaringType.Namespace ?? "";
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
{
// ignored
}
return senderInfo;
}
#endif
public static EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase
{
var eventType = typeof(TEvent);
lock (EventHandlers)
{
return EventHandlers.TryGetValue(eventType, out var collection)
? collection.GetHandlers()
: Array.Empty<EventHandlerInfo>();
}
}
public static ListenerList GetListenerList<TEvent>() where TEvent : EventBase
{
var eventType = typeof(TEvent);
lock (EventHandlers)
{
if (EventHandlers.TryGetValue(eventType, out var collection)) return collection;
return new ListenerList();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AutoRegister(object target)
{
if (target == null) return;
if (IsInstanceRegistered(target)) return;
EventBusRegHelper.RegisterEventHandlers(target);
lock (InstanceLock)
{
if (!RegisteredInstances.Add(target)) return;
}
if (target is MonoBehaviour mb && mb && mb.gameObject)
{
if (!mb.gameObject.TryGetComponent<EventBusDestroyListener>(out _))
{
var listener = mb.gameObject.AddComponent<EventBusDestroyListener>();
listener.Target = target;
listener.hideFlags = HideFlags.HideAndDontSave;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInstanceRegistered(object target)
{
if (target == null) return false;
lock (InstanceLock)
{
return RegisteredInstances.Contains(target);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetRegisteredInstanceCount()
{
lock (InstanceLock)
{
return RegisteredInstances.Count;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetRegisteredEventTypeCount()
{
lock (EventHandlers)
{
return EventHandlers.Count;
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b995a65fd1324255a5ade1ae1f0adf32
timeCreated: 1760099076
+18
View File
@@ -0,0 +1,18 @@
using UnityEngine;
namespace ShrinkEventBus
{
[DisallowMultipleComponent]
public class EventBusDestroyListener : MonoBehaviour
{
public object Target { get; set; }
private void OnDestroy()
{
if (Target != null)
{
EventBus.UnregisterInstance(Target);
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0fd1c9ba32a842199e4285383d83b3b3
timeCreated: 1772889501
+162
View File
@@ -0,0 +1,162 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace ShrinkEventBus
{
public static class EventBusRegHelper
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegStaticEventHandler() => RegisterEventHandlersInternal(null);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegisterEventHandlers(object target) => RegisterEventHandlersInternal(target);
private static void RegisterEventHandlersInternal(object target)
{
var isStatic = target == null;
if (isStatic)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
continue;
ScanMethodsAndRegister(null,
type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
}
}
catch
{
// ignored
}
}
}
else
{
var type = target.GetType();
if (type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0) return;
ScanMethodsAndRegister(target,
type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
}
}
private static void ScanMethodsAndRegister(object target, MethodInfo[] methods)
{
foreach (var method in methods)
{
if (target != null && method.IsStatic) continue;
var attributes = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false);
if (attributes.Length == 0) continue;
var subscribeAttr = (EventSubscribeAttribute)attributes[0];
var parameters = method.GetParameters();
if (parameters.Length != 1) continue;
var parameterType = parameters[0].ParameterType;
if (!typeof(EventBase).IsAssignableFrom(parameterType)) continue;
ProcessMethodRegistration(target, method, subscribeAttr, parameterType);
}
}
private static void ProcessMethodRegistration(object target, MethodInfo method,
EventSubscribeAttribute subscribeAttr, Type parameterType)
{
try
{
string scope = target == null ? "Static" : "Instance";
string typeName = target == null ? method.DeclaringType?.Name : 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);
EventBus.RegisterEventInternal(parameterType, handlerDelegate, subscribeAttr.Priority,
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
$"{scope} {typeName}.{method.Name} (UniTask)", method);
}
else if (method.ReturnType == typeof(Task))
{
var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(Task));
var handlerDelegate = target == null
? Delegate.CreateDelegate(funcType, method)
: Delegate.CreateDelegate(funcType, target, method);
var wrappedHandler = WrapTaskHandlerWithMethodInfo(parameterType, handlerDelegate, method);
EventBus.RegisterEventInternal(parameterType, wrappedHandler, subscribeAttr.Priority,
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
$"{scope} {typeName}.{method.Name} (Task->UniTask)", method);
}
else if (method.ReturnType == typeof(void))
{
var actionType = typeof(Action<>).MakeGenericType(parameterType);
var actionDelegate = target == null
? Delegate.CreateDelegate(actionType, method)
: Delegate.CreateDelegate(actionType, target, method);
EventBus.RegisterEventInternal(parameterType, actionDelegate, subscribeAttr.Priority,
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
$"{scope} {typeName}.{method.Name} (Sync)", method);
}
}
catch (Exception ex)
{
#if UNITY_EDITOR
Debug.LogWarning($"[EventBus] Registration failed for {method.Name}: {ex.Message}");
#endif
}
}
private static Delegate WrapTaskHandlerWithMethodInfo(Type eventType, Delegate taskHandler,
MethodInfo originalMethod)
{
var method = typeof(EventBusRegHelper)
.GetMethod(nameof(WrapTaskActionWithMethodInfo), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(eventType);
return (Delegate)method.Invoke(null, new object[] { taskHandler, originalMethod })!;
}
private static Func<T, UniTask> WrapTaskActionWithMethodInfo<T>(Delegate actionDelegate,
MethodInfo originalMethod)
{
var typedAction = (Func<T, Task>)actionDelegate;
var wrapper = new MethodInfoPreservingTaskWrapper<T>(typedAction, originalMethod);
return wrapper.ExecuteAsync;
}
private class MethodInfoPreservingTaskWrapper<T>
{
private readonly Func<T, Task> _originalAction;
public readonly MethodInfo OriginalMethod;
public MethodInfoPreservingTaskWrapper(Func<T, Task> originalAction, MethodInfo originalMethod)
{
_originalAction = originalAction;
OriginalMethod = originalMethod;
}
public async UniTask ExecuteAsync(T arg)
{
try
{
await _originalAction(arg);
}
catch (Exception ex)
{
Debug.LogException(ex);
Debug.LogError($"[EventBus] Task {OriginalMethod.Name}: {ex.Message}");
}
}
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a237a407a3a42394eba27bc8e4dbdce2
+7
View File
@@ -0,0 +1,7 @@
namespace ShrinkEventBus
{
internal static class EventCache<TEvent> where TEvent : EventBase
{
public static ListenerList List;
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0c41a516998341adbddce082cec09938
timeCreated: 1772889485
+78
View File
@@ -0,0 +1,78 @@
using System;
using System.Reflection;
#pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
namespace ShrinkEventBus
{
public class EventHandlerInfo
{
public Delegate Handler { 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 EventHandlerInfo(Delegate handler, EventPriority priority, int numericPriority,
bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null)
{
Handler = handler;
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;
}
}
private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler)
{
if (handler.Target == null) return null;
var targetType = handler.Target.GetType();
if (!targetType.Name.Contains("MethodInfoPreserving")) return null;
try
{
var field = targetType.GetField("OriginalMethod",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null) return field.GetValue(handler.Target) as MethodInfo;
var prop = targetType.GetProperty("OriginalMethod",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (prop != null) return prop.GetValue(handler.Target) as MethodInfo;
}
catch
{
// ignored
}
return null;
}
public string DisplayMethodName => OriginalMethodName;
public Type DisplayDeclaringType => OriginalDeclaringType ?? DeclaringType;
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 19972a32cbf54c7f94d6214fde5635c2
timeCreated: 1760098840
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace ShrinkEventBus
{
public static class EventPool<T> where T : EventBase, new()
{
private static readonly Stack<T> Pool = new(32);
private static readonly object Lock = new();
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 = e => Release((T)e);
return newEvt;
}
public static void Release(T evt)
{
if (evt == null || evt.IsInPool) return;
evt.ResetInternal();
evt.IsInPool = true;
lock (Lock)
{
Pool.Push(evt);
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c59d817792dd40f08ffed2f1a8d65cb8
timeCreated: 1772891540
+27
View File
@@ -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
};
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c77c5178c66948f29a7c87c9ae203004
timeCreated: 1760098774
+9
View File
@@ -0,0 +1,9 @@
namespace ShrinkEventBus
{
public enum EventResult
{
DEFAULT,
ALLOW,
DENY
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5213a68ce8f433a98f272904a5d7bd2
timeCreated: 1760098791
+20
View File
@@ -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)
{
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 893fdc450e7a4a8e8e1b675f0ce084a7
timeCreated: 1760098825
+96
View File
@@ -0,0 +1,96 @@
#nullable enable
using System;
using System.Reflection;
namespace ShrinkEventBus
{
public class ListenerList
{
private readonly object _lock = new();
private EventHandlerInfo[] _handlers = Array.Empty<EventHandlerInfo>();
public void Add(Delegate handler, EventPriority priority, int numericPriority,
bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null)
{
lock (_lock)
{
var newInfo = new EventHandlerInfo(handler, priority, numericPriority,
receiveCanceled, debugInfo, originalMethod);
var newArray = new EventHandlerInfo[_handlers.Length + 1];
Array.Copy(_handlers, newArray, _handlers.Length);
newArray[^1] = newInfo;
Array.Sort(newArray, CompareHandlers);
_handlers = newArray;
}
}
public bool Remove(Delegate handler)
{
lock (_lock)
{
var index = -1;
for (var i = 0; i < _handlers.Length; i++)
{
if (_handlers[i].Handler.Equals(handler))
{
index = i;
break;
}
}
if (index == -1) return false;
var newArray = new EventHandlerInfo[_handlers.Length - 1];
if (index > 0) Array.Copy(_handlers, 0, newArray, 0, index);
if (index < _handlers.Length - 1)
Array.Copy(_handlers, index + 1, newArray, index, _handlers.Length - index - 1);
_handlers = newArray;
return true;
}
}
public void RemoveTarget(object target)
{
lock (_lock)
{
var removeCount = 0;
for (var i = 0; i < _handlers.Length; i++)
{
if (_handlers[i].Target == target) removeCount++;
}
if (removeCount == 0) return;
var newArray = new EventHandlerInfo[_handlers.Length - removeCount];
var dst = 0;
for (var i = 0; i < _handlers.Length; i++)
{
if (_handlers[i].Target != target)
{
newArray[dst++] = _handlers[i];
}
}
_handlers = newArray;
}
}
public EventHandlerInfo[] GetHandlers() => _handlers;
public int Count => _handlers.Length;
public void Clear()
{
lock (_lock)
{
_handlers = Array.Empty<EventHandlerInfo>();
}
}
private static int CompareHandlers(EventHandlerInfo a, EventHandlerInfo b)
{
var priorityCompare = a.Priority.CompareTo(b.Priority);
return priorityCompare != 0 ? priorityCompare : b.NumericPriority.CompareTo(a.NumericPriority);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 864eb3e7bf8f443c86472995e408c580
timeCreated: 1760098877
+12
View File
@@ -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:
+18
View File
@@ -0,0 +1,18 @@
{
"name": "com.cneicy.shrink-eventbus",
"version": "1.0.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.md",
"dependencies": {
"com.cysharp.unitask": "2.5.10"
},
"keywords": ["eventbus", "event", "messaging", "unitask"],
"author": {
"name": "cneicy",
"url": "https://github.com/cneicy"
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 09a3fe335f19475db0839afd05b0d8cb
timeCreated: 1772900717