
目录一、先搞懂“委托”Delegate它是“电话线”二、为什么还要有“事件”Event它是“保险箱”三、游戏实战用事件重构“成就系统”第1步定义全局事件中心单例模式第2步触发器只负责“广播”第3步监听器只负责“响应”四、Unity开发中的特殊存在UnityEvent五、敢死队必看事件的三大致命陷阱1. 内存泄漏最致命的坑2. 空引用异常3. 匿名函数与Lambda的陷阱六、总结接口 vs 事件怎么选前言继上一篇我们用“接口”立下了军令状契约明确了“能做什么”之后这一篇我们聊聊游戏战场上的通讯兵。在C#游戏开发中尤其是Unity事件Events与委托Delegates是仅次于接口的第二大解耦利器。如果说接口解决的是“结构层面的依赖”那事件和委托解决的就是“行为层面的通知”。你可能常常在Update里写大量的if条件去检测状态变化比如血量低于0就播放死亡动画这种“轮询”方式既浪费CPU又让代码像意大利面一样纠缠。而事件驱动就是游戏开发中的“推送”机制——“别总是问我怎么了我变了会主动告诉你”一、先搞懂“委托”Delegate它是“电话线”在说事件之前必须先把委托搞明白。官方定义是委托是一种引用类型它指向一个方法。通俗点说委托就是“方法的快递柜”。你把一个方法存进委托变量里然后可以在任何地方像调用普通方法一样调用它完全不需要知道这个方法具体是谁写的。csharp // 1. 定义委托类型定义快递柜的规格只能放返回void且带一个int参数的方法 public delegate void HealthChangedHandler(int currentHealth); // 2. 使用委托把具体方法存进去 public class Player : MonoBehaviour { public HealthChangedHandler OnHealthChanged; // 这就是个“快递柜” private int health 100; public void TakeDamage(int dmg) { health - dmg; // 如果快递柜里有方法就执行它 if (OnHealthChanged ! null) OnHealthChanged(health); } } // 3. 在其他脚本中订阅把方法塞进快递柜 public class UIManager : MonoBehaviour { void Start() { Player player FindObjectOfTypePlayer(); player.OnHealthChanged UpdateHealthUI; // 塞进去 } void UpdateHealthUI(int hp) { Debug.Log(当前血量 hp); } }你看Player类完全不需要知道UIManager的存在它只管通知“我掉血了”。这就是委托的魔力解耦了“事件触发者”和“事件响应者”。二、为什么还要有“事件”Event它是“保险箱”问题来了上面用委托没问题但它有个致命缺陷——外部可以随意篡改。如果某个熊孩子程序员在别的脚本里写了player.OnHealthChanged null;那你之前所有的订阅都会被清空或者更过分他直接player.OnHealthChanged(999);强行伪造掉血事件。为了封装C#推出了event关键字。加了event外部只能通过订阅和-取消订阅操作不能直接赋值不能直接调用()。不加event普通委托外部可以随意赋值和调用极不安全。csharp // 把上面的委托加上 event 修饰 public event HealthChangedHandler OnHealthChanged; // 外部调用者现在只能这样做 player.OnHealthChanged UpdateHealthUI; // 正确订阅 player.OnHealthChanged - UpdateHealthUI; // 正确取消订阅 // player.OnHealthChanged UpdateHealthUI; // 报错不允许外部直接赋值 // player.OnHealthChanged(100); // 报错不允许外部直接触发结论事件就是“加了访问限制的委托”。在游戏开发中对外暴露时请务必使用event保证你的事件总线不被外部破坏。三、游戏实战用事件重构“成就系统”假设你的游戏有各种各样的行为杀敌、拾取金币、死亡。传统的写法是在Player类里杀敌逻辑写完紧接着调用AchievementManager.Unlock(杀死100个敌人)。这导致Player强依赖于AchievementManager。我们用事件驱动来重构第1步定义全局事件中心单例模式用一个静态类或单例来管理所有事件这样任何地方都能订阅和触发。csharp public static class GameEventCenter { // 使用C#内置的泛型委托 Action省去定义委托的麻烦 public static event Actionint OnEnemyKilled; // 参数敌人ID public static event Actionint OnCoinCollected; // 参数金币数量 public static event Action OnPlayerDeath; // 触发方法通常放在逻辑内部供具体模块调用 public static void TriggerEnemyKilled(int enemyId) { OnEnemyKilled?.Invoke(enemyId); // ?. 是空传播运算符如果没人订阅就不执行 } }第2步触发器只负责“广播”csharp public class Enemy : MonoBehaviour { public int enemyId 1; public void Die() { // 播放死亡特效... 然后只需广播一条消息 GameEventCenter.TriggerEnemyKilled(enemyId); // 我管你是成就系统还是音效系统我只管通知全世界 } }第3步监听器只负责“响应”csharp public class AchievementManager : MonoBehaviour { private int killCount 0; void OnEnable() { // 订阅注册监听 GameEventCenter.OnEnemyKilled OnEnemyKilledHandler; GameEventCenter.OnCoinCollected OnCoinCollectedHandler; } void OnDisable() { // 【重中之重】取消订阅防止内存泄漏 GameEventCenter.OnEnemyKilled - OnEnemyKilledHandler; GameEventCenter.OnCoinCollected - OnCoinCollectedHandler; } private void OnEnemyKilledHandler(int id) { killCount; if (killCount 100) UnlockAchievement(百人斩); } }看Enemy类只有区区一行TriggerEnemyKilled它跟成就系统彻底拜拜了。未来你想加音效、加任务进度、加统计全部零侵入。四、Unity开发中的特殊存在UnityEvent在Unity的Inspector面板中你没法直接拖拽一个普通的C#事件进去赋值。为了解决这个问题Unity提供了UnityEvent。C#event纯代码层面运行高效适合程序员之间协作。UnityEvent可视化可以在Inspector面板中拖拽赋值对策划和美术极其友好。csharp using UnityEngine.Events; public class ButtonInteractable : MonoBehaviour { // 在Inspector面板里会显示一个可视化的列表允许你拖拽任意GameObject上的方法 public UnityEvent onPlayerInteract; void OnTriggerEnter(Collider other) { onPlayerInteract?.Invoke(); // 触发所有在面板里挂载的方法 } }实战建议对性能极度敏感的高频逻辑如每帧检测、大量敌人受伤优先使用纯C#event其开销极小。用于场景编辑器配置的低频逻辑如按钮点击、关卡触发果断用UnityEvent其序列化特性非常方便。五、敢死队必看事件的三大致命陷阱事件虽好用不对就是灾难。1. 内存泄漏最致命的坑如果你订阅了事件但在对象销毁时OnDestroy忘了取消订阅-事件源会一直持有你这个监听器的引用。导致你的GameObject虽然从场景移除了但GC垃圾回收无法回收它内存泄漏铁律在OnEnable里必须在OnDisable里-。2. 空引用异常触发事件前必须判空。现在最优雅的写法是OnHealthChanged?.Invoke(currentHealth); // 一行搞定没人订阅就什么都不做3. 匿名函数与Lambda的陷阱如果你喜欢用x Debug.Log(x)这种匿名方法订阅事件切记你无法通过-正确取消订阅因为每次Lambda都会生成一个新的委托实例内存地址不同。除非你把它存起来否则不要用匿名方法订阅需要取消的事件。六、总结接口 vs 事件怎么选接口Interface解决“是什么”。当对象需要提供某种能力如IDamageable供外部调用时用接口。事件/委托Event/Delegate解决“发生了什么”。当对象需要通知外部发生了某个状态变化但又不想知道谁关心时用事件。一个经典的组合是接口负责执行动作事件负责广播结果。比如Player实现了IAttackable接口当敌人攻击调用TakeDamage导致血量归零时Player触发OnPlayerDeath事件UI和成就系统收到通知并响应。掌握事件与委托你的游戏代码就从“从上到下的流水线”进化成了“分布式消息系统”。模块之间老死不相往来解耦全靠通讯兵事件传递消息。