init
This commit is contained in:
38 files changed
+2187
No files matched your search
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bf98940dc144bd2bffd55ce21295bbd
|
||||
timeCreated: 1772878399
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16e1b18e2a2748e494f2ba648b4697a8
|
||||
timeCreated: 1772878417
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "ShrinkEventBus.Editor",
|
||||
"rootNamespace": "ShrinkEventBus.Editor",
|
||||
"references": [
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": ["Editor"],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": false
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0a89bdb25764f1f8a8e4dcc915abe39
|
||||
timeCreated: 1772900825
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e08e301c95a34847a1071cd96bba1eeb
|
||||
timeCreated: 1772900157
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c410edd1d6b644b498d39ff5e1fbc98b
|
||||
timeCreated: 1772900792
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class CancelableAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class HasResultAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class EventBusSubscriberAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public class EventSubscribeAttribute : Attribute
|
||||
{
|
||||
public EventPriority Priority { get; set; }
|
||||
public bool ReceiveCanceled { get; set; }
|
||||
public int NumericPriority { get; set; }
|
||||
|
||||
public EventSubscribeAttribute()
|
||||
{
|
||||
Priority = EventPriority.NORMAL;
|
||||
ReceiveCanceled = false;
|
||||
NumericPriority = 0;
|
||||
}
|
||||
|
||||
public EventSubscribeAttribute(EventPriority priority, bool receiveCanceled = false)
|
||||
{
|
||||
Priority = priority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
NumericPriority = 0;
|
||||
}
|
||||
|
||||
public EventSubscribeAttribute(int priority)
|
||||
{
|
||||
NumericPriority = priority;
|
||||
Priority = PriorityHelper.ConvertToEventPriority(priority);
|
||||
ReceiveCanceled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bb9d82b04a04f3085b9e68ab4f1ba60
|
||||
timeCreated: 1760098802
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d79f65274145cab7614950a64a92f5
|
||||
timeCreated: 1760099405
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b978c5174cf44d893a41950e7a504e3
|
||||
timeCreated: 1760098714
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b995a65fd1324255a5ade1ae1f0adf32
|
||||
timeCreated: 1760099076
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fd1c9ba32a842199e4285383d83b3b3
|
||||
timeCreated: 1772889501
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a237a407a3a42394eba27bc8e4dbdce2
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal static class EventCache<TEvent> where TEvent : EventBase
|
||||
{
|
||||
public static ListenerList List;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c41a516998341adbddce082cec09938
|
||||
timeCreated: 1772889485
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19972a32cbf54c7f94d6214fde5635c2
|
||||
timeCreated: 1760098840
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c59d817792dd40f08ffed2f1a8d65cb8
|
||||
timeCreated: 1772891540
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public enum EventPriority
|
||||
{
|
||||
HIGHEST = 0,
|
||||
HIGH = 1,
|
||||
NORMAL = 2,
|
||||
LOW = 3,
|
||||
LOWEST = 4,
|
||||
MONITOR = 5
|
||||
}
|
||||
|
||||
internal static class PriorityHelper
|
||||
{
|
||||
public static EventPriority ConvertToEventPriority(int numericPriority)
|
||||
{
|
||||
return numericPriority switch
|
||||
{
|
||||
>= 100 => EventPriority.HIGHEST,
|
||||
>= 50 => EventPriority.HIGH,
|
||||
> 0 => EventPriority.NORMAL,
|
||||
>= -50 => EventPriority.LOW,
|
||||
_ => EventPriority.LOWEST
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c77c5178c66948f29a7c87c9ae203004
|
||||
timeCreated: 1760098774
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public enum EventResult
|
||||
{
|
||||
DEFAULT,
|
||||
ALLOW,
|
||||
DENY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5213a68ce8f433a98f272904a5d7bd2
|
||||
timeCreated: 1760098791
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public class UnsupportedOperationException : InvalidOperationException
|
||||
{
|
||||
public UnsupportedOperationException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public UnsupportedOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public UnsupportedOperationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 893fdc450e7a4a8e8e1b675f0ce084a7
|
||||
timeCreated: 1760098825
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 864eb3e7bf8f443c86472995e408c580
|
||||
timeCreated: 1760098877
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "ShrinkEventBus.Runtime",
|
||||
"rootNamespace": "ShrinkEventBus",
|
||||
"references": [
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9a7e109b2ecd4946ba9202d2288af15
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09a3fe335f19475db0839afd05b0d8cb
|
||||
timeCreated: 1772900717
|
||||
Reference in new issue
Block a user