Unity中的发布者-订阅者模式与事件系统
1. 基本概念
1.1 发布者-订阅者模式
-
定义:一种消息通知机制,发布者发送消息,订阅者接收消息
-
角色构成:
- Publisher(发布者):负责定义和发布事件
- Subscriber(订阅者):接收并处理事件
- Event(事件):消息的载体
- EventBus(事件总线):可选,用于集中管理事件
-
工作流程:
- 发布者定义事件
- 订阅者注册(订阅)事件
- 发布者触发事件
- 订阅者接收并处理事件
- 订阅者可以取消订阅事件
-
优点:
- 解耦发布者和订阅者,实现松耦合设计
- 支持一对多的消息传递
- 提高代码的可维护性和扩展性
- 支持动态的订阅和取消订阅
-
缺点:
- 可能导致内存泄漏(如果忘记取消订阅)
- 事件流程不易跟踪和调试
- 过度使用可能导致代码难以理解
1.2 C#中的事件系统实现方式
- 基于委托(delegate):
// 1. 定义委托类型
public delegate void MessageHandler(string message);
// 2. 声明事件
public event MessageHandler OnMessageReceived;
// 3. 触发事件
OnMessageReceived?.Invoke("Hello");
// 4. 订阅事件
OnMessageReceived += HandleMessage;
- 基于EventHandler:
// 1. 定义事件参数
public class MessageEventArgs : EventArgs
{
public string Message { get; }
public MessageEventArgs(string message) => Message = message;
}
// 2. 声明事件
public event EventHandler<MessageEventArgs> OnMessageReceived;
// 3. 触发事件
OnMessageReceived?.Invoke(this, new MessageEventArgs("Hello"));
// 4. 订阅事件
OnMessageReceived += (sender, args) => Debug.Log(args.Message);
- 基于Action:
// 最简单的方式,适用于简单场景
public event Action<string> OnMessageReceived;
2. 项目中的实现分析
2.1 武器切换系统分析
// WeaponSwitcher.cs - 发布者
public class WeaponSwitcher : MonoBehaviour
{
// 1. 定义委托类型 - 指定事件的签名
public delegate void WeaponSwitchedHandler(GunController newWeapon);
// 2. 声明事件 - 创建事件实例
public event WeaponSwitchedHandler OnWeaponSwitched;
// 3. 在适当时机触发事件
public void SwitchWeapon(int direction)
{
// ... 武器切换逻辑 ...
// 使用null条件运算符安全地触发事件
OnWeaponSwitched?.Invoke(weapons[currentIndex]);
}
}
// PlayerControllerNew.cs - 订阅者
public class PlayerControllerNew : MonoBehaviour
{
private void Start()
{
// 4. 订阅事件
weaponSwitcher.OnWeaponSwitched += OnWeaponSwitched;
}
// 5. 实现事件处理方法
public void OnWeaponSwitched(GunController newWeapon)
{
currentGun = newWeapon;
}
private void OnDisable()
{
// 6. 取消订阅事件,防止内存泄漏
weaponSwitcher.OnWeaponSwitched -= OnWeaponSwitched;
}
}
2.2 输入系统事件分析
public class PlayerControllerNew : MonoBehaviour
{
// 1. 声明输入动作引用
private InputAction fireAction;
private void OnEnable()
{
// 2. 订阅输入事件
// performed: 当输入动作完成时触发
// canceled: 当输入动作取消时触发
fireAction.performed += OnFirePerformed;
fireAction.canceled += OnFireCanceled;
}
// 3. 实现事件处理方法
private void OnFirePerformed(InputAction.CallbackContext context)
{
currentGun.isShooting = true;
}
private void OnFireCanceled(InputAction.CallbackContext context)
{
currentGun.isShooting = false;
}
private void OnDisable()
{
// 4. 取消订阅输入事件
fireAction.performed -= OnFirePerformed;
fireAction.canceled -= OnFireCanceled;
}
}
2.3 事件参数的使用分析
在武器切换系统中,我们可以扩展事件参数以传递更多信息:
// 1. 定义武器切换事件参数
public class WeaponSwitchEventArgs : EventArgs
{
public GunController PreviousWeapon { get; }
public GunController NewWeapon { get; }
public float SwitchTime { get; }
public WeaponSwitchEventArgs(GunController previous, GunController next, float time)
{
PreviousWeapon = previous;
NewWeapon = next;
SwitchTime = time;
}
}
// 2. 修改事件声明
public class WeaponSwitcher : MonoBehaviour
{
public event EventHandler<WeaponSwitchEventArgs> OnWeaponSwitched;
private void SwitchWeapon(int direction)
{
var previousWeapon = weapons[currentIndex];
// ... 切换武器逻辑 ...
// 3. 使用事件参数触发事件
OnWeaponSwitched?.Invoke(this, new WeaponSwitchEventArgs(
previousWeapon,
weapons[currentIndex],
Time.time
));
}
}
// 4. 修改事件处理
public class PlayerControllerNew : MonoBehaviour
{
private void HandleWeaponSwitch(object sender, WeaponSwitchEventArgs e)
{
// 可以访问更多信息
Debug.Log($"Switched from {e.PreviousWeapon.name} to {e.NewWeapon.name} at {e.SwitchTime}");
currentGun = e.NewWeapon;
}
}
3. 实际应用场景分析
3.1 UI交互系统
// UI事件系统展示了发布者-订阅者模式的典型应用
public class UIManager : MonoBehaviour
{
[SerializeField] private Button fireButton;
[SerializeField] private Toggle autoFireToggle;
[SerializeField] private Slider ammoSlider;
private void SetupUIEvents()
{
// UI组件作为发布者,UIManager作为订阅者
fireButton.onClick.AddListener(HandleFireButtonClick);
autoFireToggle.onValueChanged.AddListener(HandleAutoFireToggle);
ammoSlider.onValueChanged.AddListener(HandleAmmoSliderChange);
}
private void HandleFireButtonClick()
{
// 处理开火按钮点击
}
private void HandleAutoFireToggle(bool isOn)
{
// 处理自动开火切换
}
private void HandleAmmoSliderChange(float value)
{
// 处理弹药量变化
}
}
3.2 游戏状态管理
// 使用静态事件实现全局状态管理
public class GameEvents
{
// 游戏状态相关事件
public static event Action OnGameStart;
public static event Action OnGamePause;
public static event Action OnGameResume;
public static event Action OnGameOver;
// 玩家状态相关事件
public static event Action<int> OnHealthChanged;
public static event Action<int> OnScoreChanged;
public static event Action<Vector3> OnPlayerPositionChanged;
// 安全的事件触发方法
public static void TriggerGameStart() => OnGameStart?.Invoke();
public static void TriggerHealthChanged(int newHealth) => OnHealthChanged?.Invoke(newHealth);
public static void TriggerScoreChanged(int newScore) => OnScoreChanged?.Invoke(newScore);
}
// 使用示例
public class GameManager : MonoBehaviour
{
private void Start()
{
// 订阅游戏事件
GameEvents.OnGameStart += HandleGameStart;
GameEvents.OnGameOver += HandleGameOver;
}
private void HandleGameStart()
{
// 处理游戏开始逻辑
}
private void HandleGameOver()
{
// 处理游戏结束逻辑
}
private void OnDestroy()
{
// 取消订阅
GameEvents.OnGameStart -= HandleGameStart;
GameEvents.OnGameOver -= HandleGameOver;
}
}
4. 最佳实践
4.1 事件命名规范
- 事件命名通常以"On"开头
- 委托类型名通常以"Handler"或"EventHandler"结尾
- 事件参数类名通常以"EventArgs"结尾
4.2 事件参数传递
// 自定义事件参数
public class WeaponEventArgs : EventArgs
{
public GunController Weapon { get; private set; }
public float SwitchTime { get; private set; }
public WeaponEventArgs(GunController weapon, float switchTime)
{
Weapon = weapon;
SwitchTime = switchTime;
}
}
// 使用自定义事件参数
public event EventHandler<WeaponEventArgs> OnWeaponSwitched;
4.3 生命周期管理
- 在适当的时机订阅和取消订阅事件
- 避免在不需要时保持订阅状态
- 防止内存泄漏
private void OnEnable()
{
// 订阅事件
weaponSwitcher.OnWeaponSwitched += OnWeaponSwitched;
}
private void OnDisable()
{
// 取消订阅事件
weaponSwitcher.OnWeaponSwitched -= OnWeaponSwitched;
}
4.4 性能考虑
- 避免在频繁调用的方法中创建委托实例
- 使用缓存的委托引用而不是每次都创建新的
- 考虑使用对象池来管理事件参数对象
5. 注意事项
5.1 线程安全
- 事件触发时注意线程同步
- 使用线程安全的事件触发方式
private readonly object eventLock = new object();
protected virtual void OnWeaponSwitched(WeaponEventArgs e)
{
lock (eventLock)
{
OnWeaponSwitched?.Invoke(this, e);
}
}
5.2 空引用处理
- 使用null条件运算符(?.)
- 在多线程环境中使用线程安全的事件触发方式
- 注意事件订阅者的生命周期
5.3 内存管理
- 及时取消事件订阅
- 避免循环引用
- 使用弱引用(WeakReference)处理长期存在的事件订阅
6. 实际应用示例
6.1 武器系统示例
// 发布者(WeaponSwitcher)
public class WeaponSwitcher : MonoBehaviour
{
public event Action<GunController> OnWeaponSwitched;
public void SwitchWeapon(int direction)
{
// ... 武器切换逻辑 ...
OnWeaponSwitched?.Invoke(weapons[currentIndex]);
}
}
// 订阅者(PlayerController)
public class PlayerController : MonoBehaviour
{
private void Start()
{
weaponSwitcher.OnWeaponSwitched += HandleWeaponSwitch;
}
private void HandleWeaponSwitch(GunController newWeapon)
{
// ... 处理武器切换 ...
}
private void OnDisable()
{
weaponSwitcher.OnWeaponSwitched -= HandleWeaponSwitch;
}
}
6.2 游戏状态管理示例
public class GameManager : MonoBehaviour
{
public static event Action<GameState> OnGameStateChanged;
private GameState currentState;
public void SetGameState(GameState newState)
{
currentState = newState;
OnGameStateChanged?.Invoke(currentState);
}
}
public class UIManager : MonoBehaviour
{
private void OnEnable()
{
GameManager.OnGameStateChanged += UpdateUI;
}
private void UpdateUI(GameState state)
{
// 更新UI显示
}
private void OnDisable()
{
GameManager.OnGameStateChanged -= UpdateUI;
}
}