当前位置: 首页 > news >正文

Unity游戏开发:EventCenter事件中心的设计、实现与最佳实践

1. 项目概述:为什么Unity项目需要一个EventCenter?

在Unity开发中,尤其是中大型项目,组件间的通信是个绕不开的坎。你肯定遇到过这种场景:玩家点击一个UI按钮,需要触发角色跳跃、播放音效、更新任务进度,甚至改变摄像机视角。如果让UI按钮的脚本直接去FindObjectOfType找角色控制器、音频管理器、任务系统,代码很快就会变成一团乱麻,耦合度高到难以维护。这就是典型的“牵一发而动全身”,任何一个模块的改动都可能引发连锁反应。

EventCenter,或者说消息中心、事件系统,就是为了解决这个问题而生的。它的核心思想是解耦集中管理。发送方(Publisher)不关心谁接收,只负责“广播”一个事件;接收方(Subscriber)不关心谁发送,只“订阅”自己关心的事件。两者通过一个中心枢纽——EventCenter——进行连接,彼此不知晓对方的存在。这种设计模式,本质上是对观察者模式(Observer Pattern)的一种应用和封装,使其更贴合Unity的组件化开发习惯。

我经历过不少项目,从最初的手写委托事件,到使用第三方插件,最后回归到自己设计实现一个轻量、高效、类型安全的EventCenter。自己实现的好处是可控、可定制、无依赖,并且能深刻理解其背后的机制。一个设计良好的EventCenter,不仅能提升代码的整洁度和可维护性,还能为项目后续的功能扩展,如存档系统、网络同步、调试工具等,打下坚实的基础。接下来,我将拆解一个在实战中经过检验的EventCenter设计与实现方案,涵盖从核心设计思路到具体代码实现,再到避坑经验的完整过程。

2. 核心设计思路与架构选型

2.1 基于C#委托与泛型的事件系统设计

为什么选择C#的委托(Delegate)和泛型(Generic)作为基石?这是由Unity(基于C#)的语言特性和我们的需求共同决定的。委托本质上是类型安全的函数指针,它是C#实现事件回调的官方推荐方式,性能开销极低。泛型则允许我们为不同类型的事件参数创建通用的容器,避免为每一种参数类型都重复编写相似的代码,这是实现类型安全事件系统的关键。

我们的核心目标是设计一个中心管理器,它需要提供三个最基本的功能:注册(Subscribe)注销(Unsubscribe)触发(Trigger)。一个直观但粗糙的实现可能是用Dictionary<string, Action>,用字符串作为事件Key。但这种方式存在明显问题:字符串容易拼写错误、没有编译时检查、事件参数传递麻烦且类型不安全。

因此,更优的方案是使用泛型委托作为字典的Key。我们定义一个通用的委托类型,例如Action<T>,其中T是事件参数的类型。然后,我们使用Dictionary<Type, Delegate>来存储事件类型和对应的委托链。这里Type是关键,它可以是typeof(int),typeof(string), 或者我们自定义的事件参数类typeof(PlayerHurtEvent)。这样,我们通过事件参数的类型本身来唯一标识一个事件频道,完全避免了字符串的歧义性,并获得了编译时的类型检查。

2.2 支持无参、单参及自定义事件参数

一个灵活的事件系统必须能处理多种情况。我们需要支持:

  1. 无参事件:例如OnGamePaused, 只需要通知系统游戏暂停了,不需要额外信息。对应委托Action
  2. 单参事件:最常见的形式,例如OnScoreChanged(int newScore)。对应委托Action<T>
  3. 自定义结构事件参数:当需要传递多个相关数据时,例如OnPlayerHurt(PlayerHurtEventData data),其中data包含了伤害来源、伤害值、伤害类型等。这通常通过定义一个继承自EventArgs或自定义的类/结构体来实现。对应委托Action<T>

为了统一管理,我们的EventCenter内部需要维护多个字典,分别对应Action,Action<T>,Action<T, U>等。但在实际项目中,Action<T>已经能覆盖99%的场景,自定义事件参数类作为T即可传递任意复杂的数据。因此,一个以Dictionary<Type, Delegate>为核心,专注于处理Action<T>的简化设计,往往是最实用和高效的。

2.3 与Unity生命周期及组件销毁的协同

这是Unity环境下实现EventCenter最容易出错的地方。在非Unity的C#程序中,事件的注册和注销通常由对象的构造函数和析构函数(或IDisposable)来管理。但在Unity中,MonoBehaviour组件的销毁时机由引擎管理,且对象可能因为场景切换而被销毁,而EventCenter可能是跨场景的(如单例)。

如果组件在销毁时没有注销它订阅的事件,那么EventCenter中仍然保留着对该组件方法的引用。由于C#委托持有的是方法所在对象的引用,这会导致该组件对象无法被垃圾回收(内存泄漏)。更严重的是,当下一次事件触发时,EventCenter会尝试调用一个已销毁对象上的方法,从而抛出MissingReferenceException

因此,强制且显式地在OnDestroyOnDisable方法中注销所有订阅是铁律。一种更优雅的模式是让组件在OnEnable时订阅,在OnDisable时注销,这样能更好地处理组件的反复激活与禁用。我们的EventCenter设计应当鼓励甚至强制这种模式,例如可以提供辅助方法或基类来简化这个流程。

3. EventCenter核心类的详细实现

3.1 定义事件码与基础事件参数类

虽然我们使用类型作为Key,但为事件定义一个唯一的“事件码”(EventID)依然有其价值,特别是在需要序列化事件、进行网络同步或动态配置时。我们可以创建一个枚举或静态类来集中管理这些事件码。

// 方式一:使用枚举(直观,但扩展性稍差) public enum EventID { OnGameStart, OnPlayerHurt, OnScoreChanged, OnItemPickedUp, OnSceneLoaded, } // 方式二:使用静态类+字符串常量(更灵活,易于动态添加) public static class EventID { public const string GameStart = "GameStart"; public const string PlayerHurt = "PlayerHurt"; // ... }

对于自定义事件参数,定义一个基类是个好习惯,这为未来可能的统一处理(如日志、广播)留有余地。

// 事件参数基类,可以包含一些通用信息,如触发时间、发送者等。 public class BaseEventArg { public object Sender { get; protected set; } public float TriggerTime { get; protected set; } public BaseEventArg(object sender = null) { Sender = sender; TriggerTime = Time.time; } } // 具体事件参数示例:玩家受伤事件 public class PlayerHurtEventArg : BaseEventArg { public int Damage { get; } public Vector3 HitPoint { get; } public GameObject Attacker { get; } public PlayerHurtEventArg(int damage, Vector3 hitPoint, GameObject attacker, object sender = null) : base(sender) { Damage = damage; HitPoint = hitPoint; Attacker = attacker; } }

3.2 实现单例模式的EventCenter管理器

EventCenter通常需要在整个游戏生命周期内存在且唯一,单例模式是最合适的选择。这里我们使用经典的“静态私有实例+公共属性访问”的线程安全懒汉式单例。

using System; using System.Collections.Generic; public class EventCenter { // 单例实例 private static EventCenter _instance; public static EventCenter Instance => _instance ?? (_instance = new EventCenter()); // 核心字典:以事件参数类型为Key,对应的委托链为Value private readonly Dictionary<Type, Delegate> _eventTable = new Dictionary<Type, Delegate>(); // 私有构造函数,防止外部实例化 private EventCenter() { } // ---------- 订阅事件 ---------- /// <summary> /// 订阅一个带有参数的事件 /// </summary> /// <typeparam name="T">事件参数类型,必须继承自BaseEventArg</typeparam> /// <param name="handler">事件处理函数</param> public void Subscribe<T>(Action<T> handler) where T : BaseEventArg { Type eventType = typeof(T); if (!_eventTable.ContainsKey(eventType)) { _eventTable[eventType] = handler; } else { // 使用Delegate.Combine安全地合并委托 _eventTable[eventType] = Delegate.Combine(_eventTable[eventType], handler); } } // ---------- 注销事件 ---------- /// <summary> /// 注销一个带有参数的事件 /// </summary> public void Unsubscribe<T>(Action<T> handler) where T : BaseEventArg { Type eventType = typeof(T); if (_eventTable.TryGetValue(eventType, out var existingDelegate)) { Delegate newDelegate = Delegate.Remove(existingDelegate, handler); if (newDelegate == null) // 如果委托链为空,移除该事件条目 { _eventTable.Remove(eventType); } else { _eventTable[eventType] = newDelegate; } } // 如果事件不存在,静默失败是通常可接受的行为 } // ---------- 触发事件 ---------- /// <summary> /// 触发一个带有参数的事件 /// </summary> public void Trigger<T>(T eventArg) where T : BaseEventArg { Type eventType = typeof(T); if (_eventTable.TryGetValue(eventType, out var action)) { // 安全调用:捕获并处理委托调用过程中的任何异常,避免一个异常导致后续所有监听器失效。 try { (action as Action<T>)?.Invoke(eventArg); } catch (Exception e) { UnityEngine.Debug.LogError($"Error invoking event {eventType}: {e}"); // 根据项目需求,可以选择是否将异常抛出 } } } // ---------- 清空事件 ---------- /// <summary> /// 清空所有事件监听。通常在场景切换或游戏重置时调用。 /// </summary> public void Clear() { _eventTable.Clear(); } }

注意:上面的Trigger方法中使用了异常捕获。这是一个重要的设计决策。如果不捕获异常,当某个监听者的处理函数抛出异常时,委托链的调用会中断,排在后面的监听者将不会被调用。这可能导致难以调试的bug。捕获并记录异常,可以保证事件广播的完整性,便于定位问题模块。

3.3 提供无参事件的快捷方式

虽然自定义参数类功能强大,但很多简单通知确实不需要参数。为了API的简洁,我们可以为无参事件提供一组重载方法。内部实现上,我们可以定义一个空的事件参数类EmptyEventArg

// 空事件参数 public class EmptyEventArg : BaseEventArg { } public partial class EventCenter // 可以使用partial类来组织代码 { // 无参事件订阅 public void Subscribe(string eventId, Action handler) { // 内部将字符串eventId映射到一个特定的Action<EmptyEventArg> // 或者维护另一个专门处理字符串Key和Action的字典。 // 这里为了简化,我们用一个统一的字典,但需要处理类型转换。 // 更清晰的实现是为无参事件单独维护一个 Dictionary<string, Action>。 // 以下是混合字典的一种实现思路(需调整_eventTable类型为Dictionary<object, Delegate>): // _eventTable[eventId] = Delegate.Combine(_eventTable.GetValueOrDefault(eventId), handler); } // 同理实现 Unsubscribe(string, Action) 和 Trigger(string) }

在实际项目中,我更倾向于保持核心API的纯粹性,即只使用Action<T>这一种形式。对于真正的“无参”事件,可以定义一个VoidEventArg或直接使用EmptyEventArg,这样能保持机制的统一,减少维护复杂度。开发者只需要多写一行new EmptyEventArg(),换来的是整个事件系统结构的清晰和稳定。

4. 在Unity组件中的使用规范与最佳实践

4.1 订阅与注销的标准生命周期模板

为了杜绝内存泄漏和空引用异常,必须在MonoBehaviour中严格遵守订阅/注销的配对原则。最可靠的模式如下:

using UnityEngine; public class PlayerUI : MonoBehaviour { private void OnEnable() { // 在OnEnable中订阅,确保组件激活时能接收到事件 EventCenter.Instance.Subscribe<PlayerHurtEventArg>(OnPlayerHurt); EventCenter.Instance.Subscribe<ScoreChangedEventArg>(OnScoreChanged); } private void OnDisable() { // 在OnDisable中注销,无论组件是销毁还是被禁用,都能安全清理 // 使用Unsubscribe注销,参数必须与Subscribe时完全一致(包括泛型类型和方法引用)。 EventCenter.Instance.Unsubscribe<PlayerHurtEventArg>(OnPlayerHurt); EventCenter.Instance.Unsubscribe<ScoreChangedEventArg>(OnScoreChanged); } // 注意:如果组件永远不会被禁用(Disable),只在销毁时清理,那么可以只在OnDestroy中注销。 // 但OnEnable/OnDisable模式更通用,能处理SetActive(false)的情况。 // private void OnDestroy() // { // EventCenter.Instance.Unsubscribe<PlayerHurtEventArg>(OnPlayerHurt); // } private void OnPlayerHurt(PlayerHurtEventArg eventArg) { // 更新血条UI healthBar.fillAmount = (float)currentHealth / maxHealth; // 可以在这里直接使用eventArg.Damage, eventArg.HitPoint等信息 } private void OnScoreChanged(ScoreChangedEventArg eventArg) { scoreText.text = $"Score: {eventArg.NewScore}"; } }

4.2 事件触发方的职责与数据封装

触发事件的一方,其职责是“构造并广播一个事实”,而不应关心这个事实如何被消费。这要求触发方封装好完整、准确的事件数据。

public class EnemyAttack : MonoBehaviour { public int attackDamage = 10; void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) { // 1. 构造事件参数,包含所有必要信息 var hurtEvent = new PlayerHurtEventArg( damage: attackDamage, hitPoint: other.ClosestPoint(transform.position), attacker: this.gameObject, sender: this // 可选,传递发送者自身 ); // 2. 触发事件 EventCenter.Instance.Trigger(hurtEvent); // 注意:触发事件后,通常不应该再直接操作其他组件。 // 例如,不应该在这里直接调用 PlayerHealth.TakeDamage(attackDamage)。 // 伤害计算逻辑应该由订阅了PlayerHurtEventArg的某个系统(如战斗系统)来处理。 // 这样,EnemyAttack只负责“通知发生了攻击接触”,具体扣血、播放受击动画等由专门的系统响应。 } } }

这种设计使得EnemyAttack组件极其简单和独立。未来如果想增加攻击特效、音效、成就统计,只需要让相应的系统订阅PlayerHurtEventArg即可,无需修改EnemyAttack的代码。

4.3 利用事件参数基类进行高级处理

定义BaseEventArg基类的一个高级用法是,可以编写一些全局的监听器,用于监控、日志或调试所有事件。

public class EventLogger : MonoBehaviour { private void OnEnable() { // 使用反射或为基类也提供事件通道?这里有一个技巧: // 我们可以单独订阅一个特殊的“所有事件”通道,或者在EventCenter内部提供广播所有事件的机制。 // 更简单的做法是,在开发阶段,修改EventCenter.Trigger方法,在调用具体委托前,先调用一个全局的日志委托。 // 例如: // EventCenter.Instance.OnAnyEventTriggered += LogEvent; } void LogEvent(BaseEventArg arg) { Debug.Log($"[Event][{Time.time:F2}] {arg.GetType().Name} triggered. Sender: {arg.Sender}"); // 可以在这里将事件记录到文件,或发送到远程调试服务器。 } }

另一种做法是在BaseEventArg中加入一个EventID字符串属性,这样即使使用泛型类型作为Key,也能快速识别事件类型,便于动态处理。

5. 高级特性扩展与性能优化

5.1 添加事件优先级与顺序控制机制

默认情况下,委托链中多个监听器的调用顺序是不确定的(实际上是添加顺序)。但在某些情况下,我们需要确保某些处理优先执行。例如,伤害计算应该先于UI血条更新;输入处理应该先于角色移动。

我们可以通过为订阅方法增加一个priority参数来实现。内部实现上,不再使用简单的Delegate.Combine,而是维护一个有序列表(如List<Action<T>>)或者一个优先级队列。订阅时根据优先级插入到正确位置,触发时按顺序调用。

public void Subscribe<T>(Action<T> handler, int priority = 0) where T : BaseEventArg { // 内部使用 List<SubscriberInfo<T>> 来存储,SubscriberInfo包含handler和priority // 在Trigger时,按priority排序后依次调用。 }

实操心得:优先级机制会增加EventCenter的复杂度,并可能引入意想不到的依赖。在绝大多数项目中,通过合理设计事件粒度(例如,分拆OnPreDamageCalculateOnPostDamageApplied两个事件)来替代优先级,是更清晰、更可维护的做法。除非有非常强烈的全局顺序需求,否则建议谨慎添加此功能。

5.2 实现一次性事件监听与自动清理

有些事件我们只关心第一次触发,例如“游戏第一次加载完成”。我们可以提供一个SubscribeOnce方法。

public void SubscribeOnce<T>(Action<T> handler) where T : BaseEventArg { Action<T> wrappedHandler = null; wrappedHandler = (arg) => { // 调用原处理函数 handler(arg); // 调用后立即注销自身 Unsubscribe(wrappedHandler); }; Subscribe(wrappedHandler); }

这个方法创建了一个包装委托,它在被调用一次后,会自动从事件列表中移除自己。这对于资源加载、初始化等场景非常有用,可以避免手动注销的麻烦。

5.3 使用对象池优化高频事件参数

在战斗、粒子系统等高频触发事件的场景中,频繁地new事件参数对象会产生大量的GC(垃圾回收)压力,可能导致游戏卡顿。此时,可以对事件参数对象进行池化。

我们可以创建一个简单的泛型对象池GenericPool<T>,并在EventCenter.Trigger内部或外部使用它。

public class PlayerHurtEventArgPool { private static readonly ConcurrentBag<PlayerHurtEventArg> pool = new ConcurrentBag<PlayerHurtEventArg>(); public static PlayerHurtEventArg Get(int damage, Vector3 hitPoint, GameObject attacker) { if (pool.TryTake(out var item)) { // 复用对象,重置数据 // 注意:这里需要为PlayerHurtEventArg添加一个Reset或Init方法,因为其属性可能是只读的。 // 更常见的做法是设计为可变结构体或类,在Get时赋值。 item.Damage = damage; item.HitPoint = hitPoint; item.Attacker = attacker; return item; } return new PlayerHurtEventArg(damage, hitPoint, attacker); } public static void Release(PlayerHurtEventArg item) { // 清理引用,防止内存泄漏 item.Attacker = null; pool.Add(item); } } // 在触发事件时 var eventArg = PlayerHurtEventArgPool.Get(damage, hitPoint, attacker); EventCenter.Instance.Trigger(eventArg); PlayerHurtEventArgPool.Release(eventArg); // 注意:需要在所有监听者处理完毕后释放!

这里有一个关键问题:你无法确定所有监听者何时处理完这个eventArg对象。如果过早释放,正在处理的监听者可能会访问到已被池回收并重用的对象,导致数据错乱。解决方案是使用引用计数延迟释放(例如在下一帧释放)。对于大多数游戏,除非每秒触发成千上万次事件,否则GC压力通常可以接受。因此,对象池优化属于高级优化手段,应在性能分析确认瓶颈后再考虑引入。

6. 常见问题排查与调试技巧

6.1 事件监听不响应的排查流程

当你触发了一个事件,但监听器没有按预期工作时,可以按以下步骤排查:

  1. 检查订阅与注销的时机:这是最常见的问题。确保监听器的Subscribe调用确实执行了(例如,在OnEnable中),并且没有在预期时间之前被Unsubscribe。在SubscribeUnsubscribe方法内部添加Debug.Log,打印事件类型和监听器信息,是快速定位时机问题的好方法。
  2. 确认事件参数类型完全匹配Subscribe<PlayerHurtEventArg>Trigger<PlayerHurtEventArg>中的泛型类型必须一字不差。如果你定义了一个PlayerHurtData类,但触发时用了PlayerHurtEventArg,则无法匹配。使用typeof(T).FullName打印出来对比。
  3. 检查委托目标是否存活:如果监听器是某个MonoBehaviour的实例方法,而该实例所在的GameObject已经被销毁,但事件没有注销,那么委托目标就变成了一个“僵尸”引用。EventCenter触发事件时,调用不会出错(因为方法信息还在),但实际的this对象是null,可能导致方法内部访问成员变量时抛出NullReferenceException。确保遵循OnEnable/OnDisable的配对模式。
  4. 查看EventCenter内部字典状态:在EventCenter类中临时添加一个公共方法,用于打印当前所有注册的事件类型及其委托数量,在怀疑的时候调用一下,可以直观看到事件是否被正确注册。

6.2 避免循环触发与栈溢出

事件系统最危险的陷阱之一是循环触发。例如:

  • 事件A的监听器在处理过程中,触发了事件B。
  • 事件B的某个监听器,又触发了事件A。
  • 如此循环,直到调用栈溢出,游戏崩溃。

如何避免?

  • 保持事件处理函数轻量:事件处理函数应该只做最简单的数据转发或状态标记,复杂的逻辑应该转移到Update协程或其他管理器中异步执行。
  • 谨慎在事件处理中触发新事件:如果不可避免,必须确保有终止条件。例如,使用一个静态的bool isFiring标志位,在触发前检查,防止重入。
  • 设计扁平化的事件流:尽量让事件广播形成一个有向无环图(DAG),而不是循环图。例如,用“命令”(Command)模式来处理复杂的交互链。

6.3 利用Unity编辑器扩展进行可视化调试

对于复杂的项目,一个可视化的调试工具至关重要。我们可以为EventCenter编写一个简单的Editor窗口,实时显示所有活跃的事件监听。

#if UNITY_EDITOR using UnityEditor; using UnityEngine; public class EventCenterDebugWindow : EditorWindow { [MenuItem("Tools/EventCenter Debugger")] public static void ShowWindow() { GetWindow<EventCenterDebugWindow>("EventCenter Debugger"); } private void OnGUI() { if (Application.isPlaying && EventCenter.Instance != null) { // 这里需要通过反射或其他方式访问EventCenter内部的_eventTable // 假设我们为EventCenter添加了一个返回字典只读副本的属性 var table = EventCenter.Instance.GetEventTableForDebug(); foreach (var kvp in table) { EditorGUILayout.LabelField($"Event: {kvp.Key.Name}"); Delegate[] invocationList = kvp.Value?.GetInvocationList(); if (invocationList != null) { EditorGUI.indentLevel++; foreach (var del in invocationList) { EditorGUILayout.LabelField($" -> {del.Target}.{del.Method.Name}"); } EditorGUI.indentLevel--; } } } else { EditorGUILayout.LabelField("Enter Play Mode to debug EventCenter."); } } } #endif

这个调试窗口可以在运行时列出所有已注册的事件,以及每个事件上挂载的所有监听方法及其所属对象,对于排查“谁监听了我这个事件”或者“这个事件被监听了几次”这类问题非常有效。

6.4 线程安全考虑

Unity的API并非线程安全,绝大部分操作必须在主线程执行。我们的EventCenter在Unity环境下,默认也只在主线程中使用。因此,上面实现的基础版本不是线程安全的。如果你在后台线程(例如,网络接收线程、繁重计算线程)中触发了事件,而事件的监听器试图访问Unity对象(如GameObject,Transform),将会导致错误。

解决方案

  • 强制主线程执行:在EventCenter.Trigger方法中,检查当前是否为主线程。如果不是,使用UnityEngine.Dispatcher(需要自己实现或使用第三方库)或MainThreadDispatcher将委托调用排队到主线程执行。
  • 明确区分:在架构设计上就规定,所有涉及Unity对象状态变更的事件,必须在主线程触发。来自后台线程的数据,应先封装成消息,由主线程轮询取出后再触发相应事件。

对于大多数单机游戏,不需要考虑线程安全。对于网络游戏或使用了多线程进行资源加载的游戏,则需要仔细设计。

7. 与其他Unity系统及设计模式的结合

7.1 与UnityAction和UnityEvent的对比与选择

Unity自带了两套事件机制:UnityAction(委托)和UnityEvent(序列化事件)。它们在Inspector面板中可视化配置的优势非常明显,适合用于组件间的简单通信,尤其是UI按钮绑定、动画事件等。

  • UnityEvent:可序列化,能在Inspector中拖拽赋值,非常适合设计师和策划配置。但它性能稍差,且不适合跨场景、跨模块的通信。
  • EventCenter:代码驱动,类型安全,性能高,是系统间通信的利器。但它无法在Inspector中配置。

最佳实践是混合使用:在组件内部、预制体内部使用UnityEvent进行可视化配置;在不同系统、管理器、服务之间使用EventCenter进行解耦通信。例如,一个CollectibleItem组件可以用UnityEvent在Inspector中配置拾取时的粒子特效和音效;同时,它也会触发一个EventCenterOnItemPickedUp事件,通知任务系统、成就系统、库存系统等全局管理器。

7.2 作为中介者模式(Mediator)的核心

EventCenter本身就是中介者模式的一个完美体现。它充当了所有模块之间的中介,模块之间不直接通信,都通过EventCenter中转。这极大地降低了系统的耦合度,使得增加新模块或修改现有模块变得非常容易,符合开放-封闭原则。

7.3 在状态机、UI管理器等复杂系统中的应用

在复杂的游戏系统中,EventCenter可以作为神经系统,连接各个独立的状态机。

  • 游戏状态机:当游戏从Playing状态切换到Paused状态时,触发OnGamePaused事件。UI管理器监听此事件,显示暂停菜单;音频管理器监听,暂停背景音乐;所有敌人生成器监听,停止生成逻辑。
  • UI管理器:当打开背包界面时,触发OnInventoryOpened事件。游戏主循环监听,将时间缩放设置为0;输入系统监听,将输入模式从“角色控制”切换到“UI导航”。
  • 资源加载系统:当场景异步加载完成时,触发OnSceneLoaded事件。依赖该场景资源的其他系统(如敌人配置表、对话系统)可以开始自己的初始化。

通过EventCenter,这些系统无需相互引用,只需关注自己感兴趣的事件,使得整个游戏架构清晰、灵活且易于测试。

设计并实现一个健壮的EventCenter,是迈向专业Unity开发的重要一步。它不仅仅是一个工具类,更是一种架构思想的体现。从最初小心翼翼地使用,到后来在项目中游刃有余地设计事件流,这个过程会让你对模块化、解耦和代码复用有更深的理解。记住,没有银弹,EventCenter也不是万能的,过度使用会导致事件流难以追踪。但在合适的场景下运用它,无疑会让你的项目代码质量提升一个档次。

http://www.cnnetsun.cn/news/3827583.html

相关文章:

  • CTF竞赛入门指南:从零基础到实战夺旗
  • GD32F103驱动GD25Q128 SPI Flash:硬件连接、软件驱动与调试避坑指南
  • Java动态代理与Cglib性能对比及实践指南
  • MATLAB App Designer 入门实战:从零构建简易计算器
  • 利用Spacedesk将平板变无线副屏:原理、部署与优化全指南
  • 科源制药产品拟中选第十二批国家药品集采
  • Ubuntu解压ZIP文件报错全解析:从编码、权限到损坏修复的完整指南
  • Unity动画插件DOTween Pro实战:从核心原理到性能优化全解析
  • OpenCV模板匹配实战:从原理到C++优化与工业应用
  • Typst:解决TeX和LaTeX局限,多模式助力技术文档创建!
  • Unity 2021.3.19f1 LTS 安装与配置全指南:从环境搭建到效率优化
  • 从工具依赖到本质洞察:构建技术深度与高效工程思维
  • Windows 11 从零部署 OpenClaw AI 智能体框架:环境配置、核心概念与实战应用
  • OpenAI与Anthropic API实战:从基础调用到工具调用完整指南
  • 冥想第一千九百六十天
  • SpringBoot分润系统开发实战与架构设计
  • Aiboteclaw:基于视觉识别与CDP协议,解决传统RPA因UI层级变化失效的自动化新方案
  • 基于 YOLOv26 的鸟类识别检测系统(全套源码+数据集)
  • Flutter三方库鸿蒙适配实战:以annas_archive_api为例
  • C++ std::sort与cmp函数深度解析:从严格弱序到高效自定义排序实战
  • Unity到Unreal Engine迁移实战:核心挑战、技术决策与性能优化
  • 汽车零部件降尘试验箱 整车电子沙尘可靠性测试
  • XM25LU128DWIQT-XMC(武汉新芯)SPI NOR Flash芯片说明书
  • 彻底告别无声哑巴片!MiniMax H3 开源登场:2K 极清直出,12 项多模态参考炸场
  • 如何高效破解Wallpaper Engine资源格式:RePKG完整解决方案
  • Java开发者如何应对技术焦虑:从稳固基本盘到AI Agent开发的演进路径
  • ArkTS 基础语法入门:变量声明与数据类型全解析
  • COMSOL光子晶体能带计算原理与工程实践
  • 体验家XMPlus互联网医院在线问诊体验管理:从找医生到复诊的全旅程体验数据闭环
  • 战略管理全流程:从规划到落地的实战方法论