Unity游戏开发中的事件总线模式:实现模块解耦与高效通信

发布时间:2026/7/24 5:37:10

Unity游戏开发中的事件总线模式:实现模块解耦与高效通信 1. 项目概述为什么我们需要Event Bus在Unity项目里摸爬滚打几年后你肯定遇到过这样的场景一个UI按钮点击后需要通知远处的某个怪物刷新状态同时还要更新任务进度、播放音效、保存游戏数据。新手最常见的做法是什么直接获取引用然后调用方法FindObjectOfTypeMonsterManager().Refresh();或者更糟用单例满天飞。代码很快就变成了“意大利面条”牵一发而动全身改一个功能得翻遍半个项目。这就是耦合带来的噩梦。而Unity-Event-Bus这个模式就是来终结这个噩梦的“解耦神器”。它本质上是一个中央事件调度系统。想象一下一个大型活动的广播中心任何模块比如UI、角色、音效都不需要知道其他模块是谁、在哪它们只需要向广播中心“发布”一个事件例如“玩家升级了”而关心这个事件的其他模块则提前在广播中心“订阅”它。当事件发布时所有订阅者会自动收到通知并执行自己的逻辑。这样做的好处是颠覆性的发布者与订阅者完全解耦。UI按钮不需要知道怪物管理器的存在它只负责发布一个“按钮被点击”的事件。怪物管理器订阅了这个事件并在事件触发时执行刷新。双方老死不相往来却配合得天衣无缝。这对于构建可维护、可测试、易于扩展的中大型Unity项目至关重要也是应对面试中“如何设计低耦合系统”这类八股文的实战利器。2. 核心设计思路从“直接调用”到“事件驱动”在深入代码之前我们先厘清传统方案与Event-Bus方案的根本区别理解其设计哲学。2.1 传统强耦合模式的弊端让我们用一个经典案例——角色拾取物品——来对比。传统做法强耦合// 在PlayerPickup脚本中 public class PlayerPickup : MonoBehaviour { public UIInventory uiInventory; public AudioSource pickupSound; public QuestManager questManager; void OnTriggerEnter(Collider other) { if (other.CompareTag(Item)) { Item item other.GetComponentItem(); // 1. 直接更新UI uiInventory.AddItem(item.data); // 2. 直接播放音效 pickupSound.Play(); // 3. 直接更新任务 questManager.UpdateItemPickupQuest(item.id); // 4. 销毁物品 Destroy(other.gameObject); } } }弊端显而易见依赖具体引用PlayerPickup必须持有UIInventory、AudioSource、QuestManager的引用。获取和管理这些引用非常麻烦拖拽或Find。难以测试你想单独测试拾取逻辑必须为它搭建一个包含所有依赖项的完整环境。难以修改如果未来要增加一个“拾取物品时触发成就系统”的功能你必须修改PlayerPickup这个类的代码违反了开闭原则。逻辑混杂一个简单的物理触发函数里混杂了UI、音频、任务、对象管理等多种职责。2.2 Event-Bus的订阅/发布模式同样的功能用Event-Bus来实现// 首先定义一个事件。这通常是一个简单的类或结构体作为消息的载体。 public struct ItemPickedUpEvent { public ItemData itemData; public Vector3 pickupPosition; } // PlayerPickup脚本发布者变得极其简洁 public class PlayerPickup : MonoBehaviour { void OnTriggerEnter(Collider other) { if (other.CompareTag(Item)) { Item item other.GetComponentItem(); // 发布事件而不是调用具体方法 EventBus.Publish(new ItemPickedUpEvent { itemData item.data, pickupPosition transform.position }); Destroy(other.gameObject); } } } // UIInventory脚本订阅者 public class UIInventory : MonoBehaviour { void OnEnable() { // 订阅事件当ItemPickedUpEvent发布时调用AddItem方法 EventBus.SubscribeItemPickedUpEvent(OnItemPickedUp); } void OnDisable() { // 非常重要在对象失效时取消订阅防止内存泄漏和空引用 EventBus.UnsubscribeItemPickedUpEvent(OnItemPickedUp); } void OnItemPickedUp(ItemPickedUpEvent e) { AddItem(e.itemData); } } // 同理AudioManager、QuestManager等也以同样方式订阅ItemPickedUpEvent。设计思路解析中心化管理EventBus作为一个静态类或单例充当所有事件的中央路由器。它内部维护了一个字典键是事件类型如ItemPickedUpEvent值是该事件对应的回调方法列表。基于类型的订阅订阅时我们告诉EventBus“我对ItemPickedUpEvent类型的事件感兴趣当它发生时请调用我这个方法。” 发布时EventBus查找所有订阅了该事件类型的方法并逐一调用。数据的封装与传递事件对象如ItemPickedUpEvent封装了所有相关的上下文数据。订阅者从事件参数中获取所需数据而不是直接访问发布者的字段。完全解耦PlayerPickup不知道也不关心谁处理了拾取事件。UIInventory也不知道事件是谁发布的。双方只与EventBus这个中间人打交道。注意这里展示的EventBus.Publish/Subscribe是一个理想化的API。在C#中我们需要利用泛型和委托如ActionT来实现类型安全的事件系统。下文会给出具体实现。3. 手把手实现一个强健的Unity Event Bus理解了原理我们来实现一个功能完整、生产可用的Event Bus。这个实现将包含基础订阅/发布、泛型支持、优先级、一次性订阅等实用功能。3.1 核心架构与接口定义首先我们定义最核心的接口。这有助于未来替换不同的实现比如用于单元测试的Mock实现。// IEventBus.cs public interface IEventBus { // 订阅事件当T类型事件发布时调用action void SubscribeT(ActionT action) where T : IEvent; // 订阅事件并指定优先级数字越小优先级越高 void SubscribeT(ActionT action, int priority) where T : IEvent; // 取消订阅 void UnsubscribeT(ActionT action) where T : IEvent; // 发布事件 void PublishT(T eventData) where T : IEvent; // 订阅一次触发后自动取消订阅 IDisposable SubscribeOnceT(ActionT action) where T : IEvent; } // 一个空接口用于标记所有事件。这不是必须的但能增加类型约束的清晰度。 public interface IEvent { }3.2 具体实现EventBus核心类接下来是实现类。这里会用到DictionaryType, ListSubscription来存储订阅关系。// EventBus.cs using System; using System.Collections.Generic; using System.Linq; public class EventBus : IEventBus { // 单例模式提供全局访问点 private static EventBus _instance; public static EventBus Instance _instance ?? (_instance new EventBus()); // 核心数据结构存储事件类型与订阅列表的映射 private readonly DictionaryType, ListSubscription _subscriptions new DictionaryType, ListSubscription(); // 内部类封装一个订阅项包含回调方法和优先级 private class Subscription : IComparableSubscription { public Delegate Action { get; } public int Priority { get; } public Subscription(Delegate action, int priority 0) { Action action; Priority priority; } // 实现IComparable用于按优先级排序 public int CompareTo(Subscription other) { return Priority.CompareTo(other.Priority); } } // 订阅方法 public void SubscribeT(ActionT action, int priority 0) where T : IEvent { var eventType typeof(T); if (!_subscriptions.ContainsKey(eventType)) { _subscriptions[eventType] new ListSubscription(); } var subList _subscriptions[eventType]; // 防止重复订阅简单判断实际项目可能需要更精确的对比 if (subList.Any(s s.Action.Equals(action))) { UnityEngine.Debug.LogWarning($Action already subscribed to event {eventType.Name}); return; } var subscription new Subscription(action, priority); subList.Add(subscription); // 按优先级排序保证高优先级的订阅者先收到事件 subList.Sort(); } // 取消订阅 public void UnsubscribeT(ActionT action) where T : IEvent { var eventType typeof(T); if (_subscriptions.TryGetValue(eventType, out var subList)) { var subscriptionToRemove subList.FirstOrDefault(s s.Action.Equals(action)); if (subscriptionToRemove ! null) { subList.Remove(subscriptionToRemove); } // 如果该事件类型没有订阅者了移除键值对以节省内存 if (subList.Count 0) { _subscriptions.Remove(eventType); } } } // 发布事件 public void PublishT(T eventData) where T : IEvent { var eventType typeof(T); if (!_subscriptions.ContainsKey(eventType)) { // 没有订阅者直接返回 return; } // 获取当前事件的订阅列表副本。 // 非常重要因为在事件处理过程中订阅者可能会执行订阅或取消订阅的操作 // 直接遍历原列表可能导致“集合已修改”的异常。 var subListCopy new ListSubscription(_subscriptions[eventType]); foreach (var subscription in subListCopy) { // 将委托转换为具体的ActionT并调用 var action subscription.Action as ActionT; // 安全调用避免某个订阅者抛出异常影响其他订阅者 try { action?.Invoke(eventData); } catch (Exception e) { UnityEngine.Debug.LogError($Error invoking action for event {eventType.Name}: {e}); } } } // 一次性订阅 public IDisposable SubscribeOnceT(ActionT action) where T : IEvent { // 使用闭包创建一个包装方法 ActionT wrappedAction null; wrappedAction (eventData) { // 执行原动作 action(eventData); // 执行后立即取消订阅自身 Unsubscribe(wrappedAction); }; Subscribe(wrappedAction); // 返回一个IDisposable允许调用者提前取消这次订阅 return new DisposableSubscriptionT(this, wrappedAction); } // 用于一次性订阅的 disposable 辅助类 private class DisposableSubscriptionT : IDisposable where T : IEvent { private readonly EventBus _eventBus; private readonly ActionT _action; private bool _isDisposed false; public DisposableSubscription(EventBus eventBus, ActionT action) { _eventBus eventBus; _action action; } public void Dispose() { if (!_isDisposed) { _eventBus.UnsubscribeT(_action); _isDisposed true; } } } // 清空所有订阅主要用于场景切换或游戏重置 public void Clear() { _subscriptions.Clear(); } }3.3 定义和使用事件现在我们可以定义具体的事件了。事件就是简单的数据容器DTO。// 示例事件定义 public struct PlayerHealthChangedEvent : IEvent { public int CurrentHealth; public int MaxHealth; public int ChangeAmount; // 正数为治疗负数为伤害 public GameObject DamageSource; // 可选的伤害来源 } public struct EnemyDefeatedEvent : IEvent { public EnemyController Enemy; public int ExperienceReward; public Vector3 DeathPosition; } // 在MonoBehaviour中使用 public class PlayerHealth : MonoBehaviour { public int health 100; public int maxHealth 100; public void TakeDamage(int amount, GameObject source null) { health - amount; health Mathf.Clamp(health, 0, maxHealth); // 发布健康变化事件 EventBus.Instance.Publish(new PlayerHealthChangedEvent { CurrentHealth health, MaxHealth maxHealth, ChangeAmount -amount, DamageSource source }); if (health 0) { Die(); } } private void Die() { // 发布玩家死亡事件... } } // UI血条脚本订阅事件 public class HealthBarUI : MonoBehaviour { public Slider healthSlider; void OnEnable() { EventBus.Instance.SubscribePlayerHealthChangedEvent(OnHealthChanged); } void OnDisable() { EventBus.Instance.UnsubscribePlayerHealthChangedEvent(OnHealthChanged); } void OnHealthChanged(PlayerHealthChangedEvent e) { healthSlider.value (float)e.CurrentHealth / e.MaxHealth; // 还可以在这里做血条变色、飘字等效果 } }4. 高级特性与实战优化一个基础的Event Bus已经能解决80%的问题。但对于复杂项目我们还需要一些进阶功能来应对更苛刻的场景。4.1 异步事件支持有些事件处理可能是耗时的如加载资源、网络请求。我们希望发布事件后能异步等待所有订阅者处理完毕。这可以通过async/await和FuncT, Task来实现。// 在IEventBus接口中增加异步发布方法 Task PublishAsyncT(T eventData) where T : IEvent; // 在EventBus类中的实现 public async Task PublishAsyncT(T eventData) where T : IEvent { var eventType typeof(T); if (!_subscriptions.ContainsKey(eventType)) { return; } var subListCopy new ListSubscription(_subscriptions[eventType]); var tasks new ListTask(); foreach (var subscription in subListCopy) { var asyncAction subscription.Action as FuncT, Task; if (asyncAction ! null) { tasks.Add(asyncAction.Invoke(eventData)); } else { // 同步Action也可以包装成Task var syncAction subscription.Action as ActionT; if (syncAction ! null) { tasks.Add(Task.Run(() syncAction.Invoke(eventData))); } } } // 等待所有异步处理完成 await Task.WhenAll(tasks); } // 使用示例一个保存游戏的事件可能需要异步写入文件或上传云端。 public struct GameSaveRequestEvent : IEvent { public string SaveSlot; } public class CloudSaveManager : MonoBehaviour { void OnEnable() { // 订阅异步处理 EventBus.Instance.SubscribeGameSaveRequestEvent(OnGameSaveRequestedAsync); } private async Task OnGameSaveRequestedAsync(GameSaveRequestEvent e) { // 模拟一个耗时的云端保存 await Task.Delay(1000); Debug.Log($Game saved to cloud slot: {e.SaveSlot}); } } // 在某个地方发布异步事件 public async void OnSaveButtonClicked() { await EventBus.Instance.PublishAsync(new GameSaveRequestEvent { SaveSlot AutoSave }); Debug.Log(All save operations completed.); }4.2 事件继承与基类事件有时我们希望订阅一类事件而不是某一个具体事件。例如所有UI相关的事件ButtonClickEventSliderValueChangedEvent都继承自一个UIEvent基类。这样我们可以订阅基类事件来接收所有派生类事件。实现这个功能需要对我们的Publish方法进行修改使其在发布事件时不仅触发订阅了该具体类型的事件也触发订阅了其基类/接口的事件。// 修改Publish方法同步版本示意 public void PublishT(T eventData) where T : IEvent { var eventType typeof(T); // 获取该类型及其所有父类直到IEvent var typesToNotify GetEventTypes(eventType); foreach (var type in typesToNotify) { if (_subscriptions.TryGetValue(type, out var subListForType)) { var subListCopy new ListSubscription(subListForType); foreach (var subscription in subListCopy) { // 这里需要动态调用因为委托类型可能不匹配ActionBaseEvent vs ActionDerivedEvent // 可以使用 dynamic 或者更复杂的反射/委托转换 try { subscription.Action.DynamicInvoke(eventData); } catch (Exception e) { UnityEngine.Debug.LogError($Error invoking action for event {type.Name}: {e}); } } } } } private IEnumerableType GetEventTypes(Type eventType) { var types new ListType { eventType }; var baseType eventType.BaseType; // 遍历所有父类直到IEvent或System.Object while (baseType ! null baseType ! typeof(object) typeof(IEvent).IsAssignableFrom(baseType)) { types.Add(baseType); baseType baseType.BaseType; } // 也可以考虑添加接口 // foreach (var interfaceType in eventType.GetInterfaces().Where(i typeof(IEvent).IsAssignableFrom(i)))... return types; }实操心得事件继承功能虽然强大但会引入一定的性能开销类型遍历、动态调用和复杂性事件处理顺序可能变得不直观。在大多数游戏逻辑中明确的事件类型已经足够。建议仅在确实需要处理一类事件的通用逻辑如日志记录、性能分析时才启用此功能并做好性能测试。4.3 与Unity生命周期深度集成Unity的GameObject和MonoBehaviour有独特的生命周期Awake,OnEnable,OnDisable,OnDestroy。为了让Event Bus用起来更顺手、更安全我们可以创建一些辅助组件。自动订阅/取消订阅组件这个组件可以挂载在任何需要订阅事件的GameObject上自动管理订阅的生命周期。// AutoEventSubscriber.cs public class AutoEventSubscriber : MonoBehaviour { [System.Serializable] public class SubscriptionInfo { // 这里无法直接存储泛型类型需要一个变通方案。 // 一种方法是使用字符串和反射但更推荐另一种使用自定义编辑器绘制并存储方法名。 // 为了简化这里展示一个概念。实际项目可能需要更复杂的编辑器脚本。 public string EventTypeName; // 例如 PlayerHealthChangedEvent public UnityEvent Response; // 用于在Inspector中配置响应 } public ListSubscriptionInfo subscriptions; private void OnEnable() { // 在实际实现中这里需要根据EventTypeName字符串通过反射找到对应的EventBus.Subscribe方法并调用。 // 例如EventBus.Instance.SubscribePlayerHealthChangedEvent(OnEvent); // 由于涉及反射和类型解析代码较复杂此处省略具体实现。 // 许多成熟的框架如UniRx, Zenject提供了类似的特性。 Debug.LogWarning(AutoEventSubscriber 需要配合自定义编辑器实现反射绑定。); } private void OnDisable() { // 同样这里需要取消订阅 } // 一个通用的处理方法被反射调用 private void OnEvent(IEvent e) { // 找到对应的SubscriptionInfo触发其UnityEvent // ... } }更实用的方法使用基类对于代码订阅一个更简单可靠的模式是创建一个基类。// EventSubscriberMonoBehaviour.cs public abstract class EventSubscriberMonoBehaviour : MonoBehaviour { protected virtual void SubscribeEvents() { } protected virtual void UnsubscribeEvents() { } protected virtual void OnEnable() { SubscribeEvents(); } protected virtual void OnDisable() { UnsubscribeEvents(); } // 如果对象被销毁也必须取消订阅OnDisable在Destroy时不一定调用 protected virtual void OnDestroy() { UnsubscribeEvents(); } } // 具体脚本继承此基类 public class MyUIComponent : EventSubscriberMonoBehaviour { protected override void SubscribeEvents() { EventBus.Instance.SubscribePlayerHealthChangedEvent(UpdateUI); EventBus.Instance.SubscribeEnemyDefeatedEvent(ShowReward); } protected override void UnsubscribeEvents() { EventBus.Instance.UnsubscribePlayerHealthChangedEvent(UpdateUI); EventBus.Instance.UnsubscribeEnemyDefeatedEvent(ShowReward); } private void UpdateUI(PlayerHealthChangedEvent e) { /* ... */ } private void ShowReward(EnemyDefeatedEvent e) { /* ... */ } }5. 性能考量、内存管理与最佳实践Event Bus不是银弹滥用或误用会导致性能问题、难以调试的Bug甚至是内存泄漏。以下是必须牢记的实战守则。5.1 性能优化策略避免在频繁调用的函数中发布事件例如不要在Update()中每帧发布事件除非绝对必要。考虑使用条件判断或节流。事件数据结构尽量小事件对象在发布时会被创建并传递给所有订阅者。使用struct而非class可以避免堆内存分配减少GC压力。确保事件只包含必要的数据。谨慎使用事件继承和动态调用如4.2节所述这些功能有开销。如果性能分析表明这里是瓶颈考虑回归到具体类型订阅。使用对象池复用事件对象对于极其高频的事件如每帧的InputEvent可以创建事件对象池避免频繁的new操作。// 简单的事件对象池示例 public class EventPoolT where T : struct, IEvent, new() { private static readonly StackT _pool new StackT(); public static T Get() { if (_pool.Count 0) { return _pool.Pop(); } return new T(); } public static void Release(T item) { // 可选重置item的字段为默认值 _pool.Push(item); } } // 使用方式 var evt EventPoolPlayerMovedEvent.Get(); evt.Position transform.position; evt.Speed currentSpeed; EventBus.Instance.Publish(evt); EventPoolPlayerMovedEvent.Release(evt);5.2 内存泄漏与订阅管理这是Event Bus最容易出问题的地方如果一个对象订阅了事件但在销毁时没有取消订阅Event Bus会一直持有对该对象的委托引用导致该对象无法被垃圾回收。黄金法则有订阅必有取消。且取消必须与订阅成对出现在正确的生命周期点调用。对于MonoBehaviour总是在OnEnable/Start中订阅在OnDisable/OnDestroy中取消订阅。使用4.3节的基类模式可以强制这一点。对于纯C#类实现IDisposable接口在Dispose方法中取消所有订阅。使用SubscribeOnce对于只需要触发一次的逻辑使用一次性订阅可以自动管理生命周期。调试内存泄漏可以在Event Bus中添加调试代码记录当前所有订阅的数量和类型定期输出帮助发现未被清理的订阅。public void PrintAllSubscriptions() { foreach (var kvp in _subscriptions) { Debug.Log($Event: {kvp.Key.Name}, Subscriber Count: {kvp.Value.Count}); } }5.3 调试与日志当事件流复杂时调试变得困难。一个事件发布了谁处理了处理顺序是什么有没有异常添加详细日志在Publish和Subscribe/Unsubscribe方法中添加可开关的调试日志。可视化事件流可以开发一个简单的编辑器窗口实时显示事件发布和订阅的流动情况。这对于理解复杂系统的交互非常有帮助。使用条件编译将调试日志用#if UNITY_EDITOR或自定义的#define EVENTBUS_DEBUG包裹起来避免影响发布版本的性能。public void PublishT(T eventData) where T : IEvent { #if EVENTBUS_DEBUG Debug.Log($[EventBus] Publishing {typeof(T).Name}); #endif // ... 原有逻辑 }5.4 架构分层与事件命名规范分层将事件按领域或模块分类。例如InputEvent、GameplayEvent、UIEvent、AudioEvent。这有助于管理事件命名空间避免冲突。命名规范事件类名使用过去式或名词明确表示“已发生的事实”例如PlayerHealthChangedEvent已改变、ItemPickedUpEvent已被拾取、GameSaveRequestEvent请求。考虑使用Event后缀提高可读性。事件数据属性使用清晰的名字如DamageAmount而非Value。6. 与Unity其他系统及流行框架的对比与整合6.1 Unity原生事件系统UnityEvent与C# EventUnity自带UnityEvent可以在Inspector中可视化配置非常适合简单的、编辑器驱动的脚本通信。而C#的event关键字是语言级别的委托实现轻量且类型安全。何时使用它们何时使用Event BusUnityEvent适用于组件间的、已知的、配置驱动的通信。比如一个按钮的onClick关联到面板上几个已知的脚本方法。它的优势是可视化缺点是难以进行全局的、动态的、跨场景的消息传递。C#event适用于同一类或紧密关联类内部的通信。比如一个Enemy类内部有一个OnDied事件Enemy的各个部分动画、音效、AI订阅它。它轻量高效但作用域有限。Event Bus适用于全局的、松耦合的、跨系统的通信。当发送方和接收方可能完全不知道对方的存在时Event Bus是唯一选择。它是架构层面的工具。它们可以共存一个典型的模式是在MonoBehaviour内部使用C#event或UnityEvent处理本地逻辑同时将重要的状态变化通过Event Bus广播到全局。例如一个Door脚本在打开时触发自己的UnityEvent播放本地动画同时发布一个DoorOpenedEvent到Event Bus让任务系统、成就系统等全局管理器做出反应。6.2 与UniRx、Zenject等框架的整合UniRx (Reactive Extensions for Unity)UniRx的核心是观察者模式和数据流。它的MessageBroker就是一个功能强大的Event Bus实现。如果你已经在项目中使用UniRx来处理异步流和响应式编程那么直接使用MessageBroker是更一致的选择。它天然支持线程调度和强大的操作符。Zenject / VContainer (依赖注入框架)这些框架通常也提供了自己的事件总线或信号总线如Zenject的SignalBus。它们的优势是与依赖注入容器深度集成可以自动处理订阅者的生命周期如将事件绑定到特定作用域并且更容易进行单元测试因为依赖都是注入的。如果你的项目采用了严格的依赖注入架构使用框架自带的事件系统可能更合适。选择建议中小项目追求轻量、简单自己实现或使用本文提供的轻量级Event Bus。项目已大量使用UniRx处理异步和UI优先使用UniRx的MessageBroker。项目采用依赖注入架构强调可测试性和解耦使用Zenject/VContainer的SignalBus。关键点在同一个项目中尽量统一使用一种全局事件通信机制避免混合使用多种总线导致混乱。7. 实战案例构建一个事件驱动的UI系统让我们用一个完整的迷你案例来串联所有知识点一个简单的游戏UI包含血条、经验条、任务提示。所有UI更新都通过Event Bus驱动。1. 定义事件// Events.cs public struct PlayerStatUpdatedEvent : IEvent { public enum StatType { Health, Mana, Experience, Level } public StatType Type; public float CurrentValue; public float MaxValue; } public struct QuestLogUpdatedEvent : IEvent { public string QuestId; public string NewObjectiveText; public bool IsCompleted; }2. 游戏逻辑发布者// PlayerStats.cs public class PlayerStats : EventSubscriberMonoBehaviour // 使用我们的基类 { public float health 100; public float maxHealth 100; public float exp 0; public float expToNextLevel 100; public void TakeDamage(float damage) { health - damage; health Mathf.Max(health, 0); PublishStatEvent(PlayerStatUpdatedEvent.StatType.Health, health, maxHealth); } public void GainExp(float amount) { exp amount; if (exp expToNextLevel) { LevelUp(); } PublishStatEvent(PlayerStatUpdatedEvent.StatType.Experience, exp, expToNextLevel); } private void LevelUp() { /* ... 发布Level事件 */ } private void PublishStatEvent(PlayerStatUpdatedEvent.StatType type, float current, float max) { EventBus.Instance.Publish(new PlayerStatUpdatedEvent { Type type, CurrentValue current, MaxValue max }); } } // QuestSystem.cs public class QuestSystem : MonoBehaviour { public void UpdateQuestObjective(string questId, string objective) { // ... 内部逻辑 EventBus.Instance.Publish(new QuestLogUpdatedEvent { QuestId questId, NewObjectiveText objective, IsCompleted false }); } }3. UI逻辑订阅者// HealthBarUI.cs public class HealthBarUI : EventSubscriberMonoBehaviour { public Slider slider; public Image fillImage; public Gradient healthGradient; // 血条颜色渐变 protected override void SubscribeEvents() { EventBus.Instance.SubscribePlayerStatUpdatedEvent(OnStatUpdated); } protected override void UnsubscribeEvents() { EventBus.Instance.UnsubscribePlayerStatUpdatedEvent(OnStatUpdated); } private void OnStatUpdated(PlayerStatUpdatedEvent e) { if (e.Type PlayerStatUpdatedEvent.StatType.Health) { float fillAmount e.CurrentValue / e.MaxValue; slider.value fillAmount; fillImage.color healthGradient.Evaluate(fillAmount); // 可以在这里添加血条抖动、数字飘动等效果 } } } // QuestTrackerUI.cs public class QuestTrackerUI : EventSubscriberMonoBehaviour { public Text objectiveText; public Animator notificationAnimator; protected override void SubscribeEvents() { EventBus.Instance.SubscribeQuestLogUpdatedEvent(OnQuestUpdated); } protected override void UnsubscribeEvents() { EventBus.Instance.UnsubscribeQuestLogUpdatedEvent(OnQuestUpdated); } private void OnQuestUpdated(QuestLogUpdatedEvent e) { objectiveText.text e.NewObjectiveText; if (notificationAnimator ! null) { notificationAnimator.Play(Flash); } // 如果任务完成可以播放不同的动画或音效 if (e.IsCompleted) { // 发布任务完成事件或者直接在这里处理... } } }4. 效果与优势完全解耦PlayerStats和QuestSystem完全不知道UI的存在。UI脚本也只依赖事件不依赖具体的游戏逻辑类。易于扩展如果要新增一个“伤害数字弹出”的功能只需创建一个DamageNumberUI脚本订阅PlayerStatUpdatedEvent或专门创建一个DamageTakenEvent而无需修改PlayerStats。易于测试你可以单独测试UI只需用代码发布模拟事件而无需启动完整的游戏场景。结构清晰事件成为了系统间的清晰契约。查看所有IEvent的实现你就能快速了解整个游戏有哪些重要的状态变化和交互。通过这个案例你可以看到Event Bus如何将一团乱麻的依赖关系梳理成清晰的事件流让代码的维护性和扩展性得到质的提升。它不仅仅是工具更是一种让代码结构变得更优雅的架构思想。

相关新闻