From 8549ba5c80b69a1abc13543359f79f991a6194b6 Mon Sep 17 00:00:00 2001 From: cneicy Date: Sun, 8 Mar 2026 00:40:13 +0800 Subject: [PATCH] init --- Editor.meta | 3 + Editor/EventBusViewerWindow.cs | 473 +++++++++++++++++++++ Editor/EventBusViewerWindow.cs.meta | 3 + Editor/ShrinkEventBus.Editor.asmdef | 13 + Editor/ShrinkEventBus.Editor.asmdef.meta | 3 + README.md | 437 +++++++++++++++++++ README.md.meta | 3 + Runtime.meta | 3 + Runtime/Attributes.cs | 42 ++ Runtime/Attributes.cs.meta | 3 + Runtime/EventAutoRegHelper.cs | 136 ++++++ Runtime/EventAutoRegHelper.cs.meta | 3 + Runtime/EventBase.cs | 84 ++++ Runtime/EventBase.cs.meta | 3 + Runtime/EventBus.cs | 451 ++++++++++++++++++++ Runtime/EventBus.cs.meta | 3 + Runtime/EventBusDestroyListener.cs | 18 + Runtime/EventBusDestroyListener.cs.meta | 3 + Runtime/EventBusRegHelper.cs | 162 +++++++ Runtime/EventBusRegHelper.cs.meta | 2 + Runtime/EventCache.cs | 7 + Runtime/EventCache.cs.meta | 3 + Runtime/EventHandlerInfo.cs | 78 ++++ Runtime/EventHandlerInfo.cs.meta | 3 + Runtime/EventPool.cs | 41 ++ Runtime/EventPool.cs.meta | 3 + Runtime/EventPriority.cs | 27 ++ Runtime/EventPriority.cs.meta | 3 + Runtime/EventResult.cs | 9 + Runtime/EventResult.cs.meta | 3 + Runtime/Exceptions.cs | 20 + Runtime/Exceptions.cs.meta | 3 + Runtime/ListenerList.cs | 96 +++++ Runtime/ListenerList.cs.meta | 3 + Runtime/ShrinkEventBus.Runtime.asmdef | 12 + Runtime/ShrinkEventBus.Runtime.asmdef.meta | 7 + package.json | 18 + package.json.meta | 3 + 38 files changed, 2187 insertions(+) create mode 100644 Editor.meta create mode 100644 Editor/EventBusViewerWindow.cs create mode 100644 Editor/EventBusViewerWindow.cs.meta create mode 100644 Editor/ShrinkEventBus.Editor.asmdef create mode 100644 Editor/ShrinkEventBus.Editor.asmdef.meta create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 Runtime.meta create mode 100644 Runtime/Attributes.cs create mode 100644 Runtime/Attributes.cs.meta create mode 100644 Runtime/EventAutoRegHelper.cs create mode 100644 Runtime/EventAutoRegHelper.cs.meta create mode 100644 Runtime/EventBase.cs create mode 100644 Runtime/EventBase.cs.meta create mode 100644 Runtime/EventBus.cs create mode 100644 Runtime/EventBus.cs.meta create mode 100644 Runtime/EventBusDestroyListener.cs create mode 100644 Runtime/EventBusDestroyListener.cs.meta create mode 100644 Runtime/EventBusRegHelper.cs create mode 100644 Runtime/EventBusRegHelper.cs.meta create mode 100644 Runtime/EventCache.cs create mode 100644 Runtime/EventCache.cs.meta create mode 100644 Runtime/EventHandlerInfo.cs create mode 100644 Runtime/EventHandlerInfo.cs.meta create mode 100644 Runtime/EventPool.cs create mode 100644 Runtime/EventPool.cs.meta create mode 100644 Runtime/EventPriority.cs create mode 100644 Runtime/EventPriority.cs.meta create mode 100644 Runtime/EventResult.cs create mode 100644 Runtime/EventResult.cs.meta create mode 100644 Runtime/Exceptions.cs create mode 100644 Runtime/Exceptions.cs.meta create mode 100644 Runtime/ListenerList.cs create mode 100644 Runtime/ListenerList.cs.meta create mode 100644 Runtime/ShrinkEventBus.Runtime.asmdef create mode 100644 Runtime/ShrinkEventBus.Runtime.asmdef.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..454aa37 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8bf98940dc144bd2bffd55ce21295bbd +timeCreated: 1772878399 \ No newline at end of file diff --git a/Editor/EventBusViewerWindow.cs b/Editor/EventBusViewerWindow.cs new file mode 100644 index 0000000..ba419fe --- /dev/null +++ b/Editor/EventBusViewerWindow.cs @@ -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 _logElements = new(); + + [MenuItem("Window/Shrink EventBus/事件查看器")] + public static void ShowWindow() + { + var window = GetWindow("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(e => + senderLabel.style.color = new StyleColor(Color.cyan)); + senderLabel.RegisterCallback(e => + senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f))); + senderLabel.RegisterCallback(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(e => linkLabel.style.color = new StyleColor(Color.cyan)); + linkLabel.RegisterCallback(e => + linkLabel.style.color = new StyleColor(new Color(0.3f, 0.6f, 1f))); + linkLabel.RegisterCallback(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 DumpEventParams(EventBase evt) + { + var values = new List(); + 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(path); + if (script) + { + AssetDatabase.OpenAsset(script); + return; + } + } + } + } + } +} +#endif \ No newline at end of file diff --git a/Editor/EventBusViewerWindow.cs.meta b/Editor/EventBusViewerWindow.cs.meta new file mode 100644 index 0000000..7fd9a77 --- /dev/null +++ b/Editor/EventBusViewerWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 16e1b18e2a2748e494f2ba648b4697a8 +timeCreated: 1772878417 \ No newline at end of file diff --git a/Editor/ShrinkEventBus.Editor.asmdef b/Editor/ShrinkEventBus.Editor.asmdef new file mode 100644 index 0000000..044b09e --- /dev/null +++ b/Editor/ShrinkEventBus.Editor.asmdef @@ -0,0 +1,13 @@ +{ + "name": "ShrinkEventBus.Editor", + "rootNamespace": "ShrinkEventBus.Editor", + "references": [ + "ShrinkEventBus.Runtime", + "UniTask" + ], + "includePlatforms": ["Editor"], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": false +} \ No newline at end of file diff --git a/Editor/ShrinkEventBus.Editor.asmdef.meta b/Editor/ShrinkEventBus.Editor.asmdef.meta new file mode 100644 index 0000000..ac5f960 --- /dev/null +++ b/Editor/ShrinkEventBus.Editor.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f0a89bdb25764f1f8a8e4dcc915abe39 +timeCreated: 1772900825 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f64205 --- /dev/null +++ b/README.md @@ -0,0 +1,437 @@ +# ShrinkEventBus + +一个为 Unity C# 项目设计的高性能、类型安全事件总线系统。支持优先级调度、自动注册、同步/异步混合处理,以及完整的 MonoBehaviour 生命周期管理。 + +## ✨ 特性概览 + +| 特性 | 说明 | +|------|------| +| 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 | +| ⚡ **高性能热路径** | 泛型静态缓存 `EventCache` 绕过字典查找,触发路径几乎零开销 | +| 🤖 **自动注册** | 标记 `[EventBusSubscriber]` 即可,无需手动注册/注销,自动绑定 GameObject 生命周期 | +| 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 | +| 🔄 **同步 & 异步** | 统一支持 `Action`、`UniTask`、`Task` 三种 handler 形式 | +| 🧵 **线程安全** | 注册/注销操作全程加锁保护 | +| 📦 **对象池** | 内置 `EventPool`,高频事件零 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.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(Handler, priority: 75); // 映射为 HIGH + +// 手动注册时混合使用 +EventBus.RegisterEvent(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(OnItemPickup, EventPriority.NORMAL); + } + + private void OnItemPickup(ItemPickupEvent evt) { /* ... */ } + + public void Dispose() + { + // 必须手动清理,否则 handler 持有 this 引用会造成内存泄漏 + EventBus.UnregisterAllEventsForObject(this); + } +} +``` + +--- + +## 🔧 API 参考 + +### EventBus(静态门面) + +#### 注册 / 注销 + +```csharp +// 同步 handler +EventBus.RegisterEvent(Action handler, EventPriority priority, bool receiveCanceled); +EventBus.RegisterEvent(Action handler, int priority); + +// 异步 handler(UniTask) +EventBus.RegisterEvent(Func handler, EventPriority priority, bool receiveCanceled); + +// 注销 +EventBus.UnregisterEvent(Action handler); +EventBus.UnregisterEvent(Func handler); +EventBus.UnregisterAllEventsForObject(object target); // 注销某实例的全部 handler +EventBus.ClearAllSubscribersForEvent(); // 清空某事件的全部订阅者 +EventBus.UnregisterAllEvents(); // 全部清空(谨慎使用) +``` + +#### 触发 + +```csharp +// 同步触发:async handler 会以 .Forget() 方式触发,不等待 +bool handled = EventBus.TriggerEvent(TEvent eventArgs); + +// 异步触发:顺序 await 每个 handler +bool handled = await EventBus.TriggerEventAsync(TEvent eventArgs); +``` + +> ⚠️ `TriggerEvent` 中遇到 async handler 时会调用 `.Forget()`,不会等待其完成。如果需要顺序等待,请使用 `TriggerEventAsync`。 + +#### 查询 + +```csharp +EventBus.IsInstanceRegistered(object target); +EventBus.GetRegisteredInstanceCount(); +EventBus.GetRegisteredEventTypeCount(); +EventBus.GetEventSubscribers(); // 返回 EventHandlerInfo[] +EventBus.GetListenerList(); +``` + +### EventPool\ + +```csharp +// 从池中取出(自动重置状态) +var evt = EventPool.Get(); + +// 手动归还 +EventPool.Release(evt); + +// 推荐:配合 using 自动归还 +using var evt = EventPool.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 泛型静态缓存,热路径绕过字典 +├── ListenerList 线程安全的有序 handler 列表 +├── EventHandlerInfo 单个 handler 的元信息(优先级、方法反射、调试信息) +├── EventBase 所有事件的基类,携带生命周期状态 +├── EventPool 对象池,高频事件减少 GC +├── EventBusRegHelper 反射扫描 & handler 注册逻辑 +├── EventAutoRegHelper 运行时自动扫描场景内 MonoBehaviour +└── EventBusDestroyListener 挂在 GameObject 上监听 OnDestroy 自动反注册 +``` + +**热路径(`TriggerEvent`)工作流:** + +``` +TriggerEvent(evt) + └─ 读取 EventCache.List // 静态字段,O(1),无字典查找 + └─ GetHandlers() // 返回内部数组引用,无拷贝 + └─ 遍历 handlers[] + ├─ 跳过已取消 & 不接收取消的 handler + ├─ Action → 直接调用 + └─ Func → .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.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().Length}"); +``` + +**怀疑内存泄漏** + +```csharp +// 检查是否有 handler 持有意外引用 +var handlers = EventBus.GetEventSubscribers(); +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 \ No newline at end of file diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..c272807 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e08e301c95a34847a1071cd96bba1eeb +timeCreated: 1772900157 \ No newline at end of file diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..26d1953 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c410edd1d6b644b498d39ff5e1fbc98b +timeCreated: 1772900792 \ No newline at end of file diff --git a/Runtime/Attributes.cs b/Runtime/Attributes.cs new file mode 100644 index 0000000..81890b6 --- /dev/null +++ b/Runtime/Attributes.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Runtime/Attributes.cs.meta b/Runtime/Attributes.cs.meta new file mode 100644 index 0000000..f811159 --- /dev/null +++ b/Runtime/Attributes.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9bb9d82b04a04f3085b9e68ab4f1ba60 +timeCreated: 1760098802 \ No newline at end of file diff --git a/Runtime/EventAutoRegHelper.cs b/Runtime/EventAutoRegHelper.cs new file mode 100644 index 0000000..628548e --- /dev/null +++ b/Runtime/EventAutoRegHelper.cs @@ -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 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(UnityEngine.FindObjectsInactive.Include, + UnityEngine.FindObjectsSortMode.None); +#else + var allMonoBehaviours = UnityEngine.Object.FindObjectsOfType(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() != null && + typeof(MonoBehaviour).IsAssignableFrom(type)) + SubscriberTypes.TryAdd(type, true); + } + } + catch (ReflectionTypeLoadException ex) + { + foreach (var type in ex.Types) + { + if (type?.GetCustomAttribute() != 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() != 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; + } + } + } +} \ No newline at end of file diff --git a/Runtime/EventAutoRegHelper.cs.meta b/Runtime/EventAutoRegHelper.cs.meta new file mode 100644 index 0000000..0f2b7fc --- /dev/null +++ b/Runtime/EventAutoRegHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f4d79f65274145cab7614950a64a92f5 +timeCreated: 1760099405 \ No newline at end of file diff --git a/Runtime/EventBase.cs b/Runtime/EventBase.cs new file mode 100644 index 0000000..b562463 --- /dev/null +++ b/Runtime/EventBase.cs @@ -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 ReleaseAction { get; set; } + + protected EventBase() + { + IsCancelable = GetType().GetCustomAttribute() != null; + HasResult = GetType().GetCustomAttribute() != 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); + } + } +} \ No newline at end of file diff --git a/Runtime/EventBase.cs.meta b/Runtime/EventBase.cs.meta new file mode 100644 index 0000000..26c8a83 --- /dev/null +++ b/Runtime/EventBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0b978c5174cf44d893a41950e7a504e3 +timeCreated: 1760098714 \ No newline at end of file diff --git a/Runtime/EventBus.cs b/Runtime/EventBus.cs new file mode 100644 index 0000000..75dab1d --- /dev/null +++ b/Runtime/EventBus.cs @@ -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 OnEventTriggeredForEditor; +#endif + + private static readonly Dictionary EventHandlers = new(); + private static readonly HashSet 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(Action 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(Func 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(Action 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(Action 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(Func 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(Func 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.List = null; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnregisterEvent(Action 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.List = null; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ClearAllSubscribersForEvent() where TEvent : EventBase + { + var eventType = typeof(TEvent); + lock (EventHandlers) + { + if (EventHandlers.Remove(eventType)) + { + EventCache.List = null; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnregisterAllEventsForObject(object targetObject) + { + if (targetObject is null) return; + lock (EventHandlers) + { + var eventTypesToRemove = new List(); + 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(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 TriggerEventAsync(TEvent eventArgs) where TEvent : EventBase + { + EnsureAutoManagerInitialized(); + + var collection = EventCache.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 syncHandler) + { + syncHandler(eventArgs); + wasHandled = true; + } + else if (handlerInfo.Handler is Func 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 eventArgs) where TEvent : EventBase + { + EnsureAutoManagerInitialized(); + + var collection = EventCache.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 syncHandler) + { + syncHandler(eventArgs); + wasHandled = true; + } + else if (handlerInfo.Handler is Func 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 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(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() where TEvent : EventBase + { + var eventType = typeof(TEvent); + lock (EventHandlers) + { + return EventHandlers.TryGetValue(eventType, out var collection) + ? collection.GetHandlers() + : Array.Empty(); + } + } + + public static ListenerList GetListenerList() 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(out _)) + { + var listener = mb.gameObject.AddComponent(); + 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; + } + } + } +} \ No newline at end of file diff --git a/Runtime/EventBus.cs.meta b/Runtime/EventBus.cs.meta new file mode 100644 index 0000000..6312bb8 --- /dev/null +++ b/Runtime/EventBus.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b995a65fd1324255a5ade1ae1f0adf32 +timeCreated: 1760099076 \ No newline at end of file diff --git a/Runtime/EventBusDestroyListener.cs b/Runtime/EventBusDestroyListener.cs new file mode 100644 index 0000000..a0246bf --- /dev/null +++ b/Runtime/EventBusDestroyListener.cs @@ -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); + } + } + } +} \ No newline at end of file diff --git a/Runtime/EventBusDestroyListener.cs.meta b/Runtime/EventBusDestroyListener.cs.meta new file mode 100644 index 0000000..1862c27 --- /dev/null +++ b/Runtime/EventBusDestroyListener.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0fd1c9ba32a842199e4285383d83b3b3 +timeCreated: 1772889501 \ No newline at end of file diff --git a/Runtime/EventBusRegHelper.cs b/Runtime/EventBusRegHelper.cs new file mode 100644 index 0000000..a826d76 --- /dev/null +++ b/Runtime/EventBusRegHelper.cs @@ -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 WrapTaskActionWithMethodInfo(Delegate actionDelegate, + MethodInfo originalMethod) + { + var typedAction = (Func)actionDelegate; + var wrapper = new MethodInfoPreservingTaskWrapper(typedAction, originalMethod); + return wrapper.ExecuteAsync; + } + + private class MethodInfoPreservingTaskWrapper + { + private readonly Func _originalAction; + public readonly MethodInfo OriginalMethod; + + public MethodInfoPreservingTaskWrapper(Func 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}"); + } + } + } + } +} \ No newline at end of file diff --git a/Runtime/EventBusRegHelper.cs.meta b/Runtime/EventBusRegHelper.cs.meta new file mode 100644 index 0000000..05ebd35 --- /dev/null +++ b/Runtime/EventBusRegHelper.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a237a407a3a42394eba27bc8e4dbdce2 \ No newline at end of file diff --git a/Runtime/EventCache.cs b/Runtime/EventCache.cs new file mode 100644 index 0000000..e59816f --- /dev/null +++ b/Runtime/EventCache.cs @@ -0,0 +1,7 @@ +namespace ShrinkEventBus +{ + internal static class EventCache where TEvent : EventBase + { + public static ListenerList List; + } +} \ No newline at end of file diff --git a/Runtime/EventCache.cs.meta b/Runtime/EventCache.cs.meta new file mode 100644 index 0000000..7fad8e5 --- /dev/null +++ b/Runtime/EventCache.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0c41a516998341adbddce082cec09938 +timeCreated: 1772889485 \ No newline at end of file diff --git a/Runtime/EventHandlerInfo.cs b/Runtime/EventHandlerInfo.cs new file mode 100644 index 0000000..280e1b0 --- /dev/null +++ b/Runtime/EventHandlerInfo.cs @@ -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; + } +} \ No newline at end of file diff --git a/Runtime/EventHandlerInfo.cs.meta b/Runtime/EventHandlerInfo.cs.meta new file mode 100644 index 0000000..463d79f --- /dev/null +++ b/Runtime/EventHandlerInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 19972a32cbf54c7f94d6214fde5635c2 +timeCreated: 1760098840 \ No newline at end of file diff --git a/Runtime/EventPool.cs b/Runtime/EventPool.cs new file mode 100644 index 0000000..0689053 --- /dev/null +++ b/Runtime/EventPool.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace ShrinkEventBus +{ + public static class EventPool where T : EventBase, new() + { + private static readonly Stack 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); + } + } + } +} \ No newline at end of file diff --git a/Runtime/EventPool.cs.meta b/Runtime/EventPool.cs.meta new file mode 100644 index 0000000..d0aecaf --- /dev/null +++ b/Runtime/EventPool.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c59d817792dd40f08ffed2f1a8d65cb8 +timeCreated: 1772891540 \ No newline at end of file diff --git a/Runtime/EventPriority.cs b/Runtime/EventPriority.cs new file mode 100644 index 0000000..43b766f --- /dev/null +++ b/Runtime/EventPriority.cs @@ -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 + }; + } + } +} \ No newline at end of file diff --git a/Runtime/EventPriority.cs.meta b/Runtime/EventPriority.cs.meta new file mode 100644 index 0000000..c7fdea4 --- /dev/null +++ b/Runtime/EventPriority.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c77c5178c66948f29a7c87c9ae203004 +timeCreated: 1760098774 \ No newline at end of file diff --git a/Runtime/EventResult.cs b/Runtime/EventResult.cs new file mode 100644 index 0000000..777d021 --- /dev/null +++ b/Runtime/EventResult.cs @@ -0,0 +1,9 @@ +namespace ShrinkEventBus +{ + public enum EventResult + { + DEFAULT, + ALLOW, + DENY + } +} \ No newline at end of file diff --git a/Runtime/EventResult.cs.meta b/Runtime/EventResult.cs.meta new file mode 100644 index 0000000..b18a52d --- /dev/null +++ b/Runtime/EventResult.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d5213a68ce8f433a98f272904a5d7bd2 +timeCreated: 1760098791 \ No newline at end of file diff --git a/Runtime/Exceptions.cs b/Runtime/Exceptions.cs new file mode 100644 index 0000000..a652fdf --- /dev/null +++ b/Runtime/Exceptions.cs @@ -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) + { + } + } +} \ No newline at end of file diff --git a/Runtime/Exceptions.cs.meta b/Runtime/Exceptions.cs.meta new file mode 100644 index 0000000..a3f533d --- /dev/null +++ b/Runtime/Exceptions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 893fdc450e7a4a8e8e1b675f0ce084a7 +timeCreated: 1760098825 \ No newline at end of file diff --git a/Runtime/ListenerList.cs b/Runtime/ListenerList.cs new file mode 100644 index 0000000..0caeab0 --- /dev/null +++ b/Runtime/ListenerList.cs @@ -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(); + + 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(); + } + } + + private static int CompareHandlers(EventHandlerInfo a, EventHandlerInfo b) + { + var priorityCompare = a.Priority.CompareTo(b.Priority); + return priorityCompare != 0 ? priorityCompare : b.NumericPriority.CompareTo(a.NumericPriority); + } + } +} \ No newline at end of file diff --git a/Runtime/ListenerList.cs.meta b/Runtime/ListenerList.cs.meta new file mode 100644 index 0000000..9a41846 --- /dev/null +++ b/Runtime/ListenerList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 864eb3e7bf8f443c86472995e408c580 +timeCreated: 1760098877 \ No newline at end of file diff --git a/Runtime/ShrinkEventBus.Runtime.asmdef b/Runtime/ShrinkEventBus.Runtime.asmdef new file mode 100644 index 0000000..9b2a8d4 --- /dev/null +++ b/Runtime/ShrinkEventBus.Runtime.asmdef @@ -0,0 +1,12 @@ +{ + "name": "ShrinkEventBus.Runtime", + "rootNamespace": "ShrinkEventBus", + "references": [ + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true +} \ No newline at end of file diff --git a/Runtime/ShrinkEventBus.Runtime.asmdef.meta b/Runtime/ShrinkEventBus.Runtime.asmdef.meta new file mode 100644 index 0000000..b34cc54 --- /dev/null +++ b/Runtime/ShrinkEventBus.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f9a7e109b2ecd4946ba9202d2288af15 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5b2b28 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..fc2f5e7 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09a3fe335f19475db0839afd05b0d8cb +timeCreated: 1772900717 \ No newline at end of file