798 lines
36 KiB
Markdown
798 lines
36 KiB
Markdown
# 02 · SO 事件系统
|
||
|
||
> **命名空间** `BaseGames.Core.Events`
|
||
> **程序集** `BaseGames.Core.Events`(无任何运行时依赖,最底层)
|
||
> **路径** `Assets/Scripts/Core/Events/`
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [设计原则](#1-设计原则)
|
||
2. [泛型事件频道基类](#2-泛型事件频道基类)
|
||
3. [所有事件频道类型](#3-所有事件频道类型)
|
||
4. [全局事件频道 SO 资产清单](#4-全局事件频道-so-资产清单)
|
||
5. [发布 / 订阅模式](#5-发布--订阅模式)
|
||
6. [MMEventManager(备用广播)](#6-mmeventmanager)
|
||
7. [EventChannelRegistry](#7-eventchannelregistry)
|
||
8. [事件订阅生命周期管理 — 自动注销机制](#8-事件订阅生命周期管理--自动注销机制)
|
||
9. [EventBusMonitorWindow — 运行时事件监控面板](#9-eventbusmonitorwindow--运行时事件监控面板)
|
||
|
||
---
|
||
|
||
## 1. 设计原则
|
||
|
||
- 每个事件频道是一个 **ScriptableObject**,在 `Assets/Data/Events/` 下预建资产,Inspector 拖拽引用
|
||
- 发布者和订阅者**只持有频道 SO 的引用**,彼此完全不知道对方的存在
|
||
- 事件频道**不存储状态**;仅在 `Raise()` 调用时执行 C# Action 委托链
|
||
- **禁止在代码中 `new` 出事件频道**;必须使用预建的 `.asset` 资产
|
||
- 所有频道订阅在 `OnEnable` 注册,在 `OnDisable` 取消注册
|
||
|
||
---
|
||
|
||
## 2. 泛型事件频道基类
|
||
|
||
### 文件:`Assets/Scripts/Core/Events/BaseEventChannelSO.cs`
|
||
|
||
```csharp
|
||
using System;
|
||
using UnityEngine;
|
||
|
||
namespace BaseGames.Core.Events
|
||
{
|
||
/// <summary>
|
||
/// 泛型 SO 事件频道基类。T 为负载类型。
|
||
/// </summary>
|
||
public abstract class BaseEventChannelSO<T> : ScriptableObject
|
||
{
|
||
// Editor 备注(Inspector 可见)
|
||
[Multiline] public string description;
|
||
|
||
public event Action<T> OnEventRaised;
|
||
|
||
/// <summary>
|
||
/// 发布事件,执行所有订阅委托。
|
||
/// </summary>
|
||
public void Raise(T value)
|
||
{
|
||
OnEventRaised?.Invoke(value);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 无负载事件频道基类。
|
||
/// </summary>
|
||
public abstract class VoidBaseEventChannelSO : ScriptableObject
|
||
{
|
||
[Multiline] public string description;
|
||
|
||
public event Action OnEventRaised;
|
||
|
||
public void Raise()
|
||
{
|
||
OnEventRaised?.Invoke();
|
||
#if UNITY_EDITOR
|
||
EventBusMonitor.Record(name, "<void>",
|
||
OnEventRaised?.GetInvocationList().Length ?? 0);
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 所有事件频道类型
|
||
|
||
### 基础类型频道
|
||
|
||
```csharp
|
||
// Assets/Scripts/Core/Events/VoidEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/Void")]
|
||
public class VoidEventChannelSO : VoidBaseEventChannelSO { }
|
||
|
||
// Assets/Scripts/Core/Events/BoolEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/Bool")]
|
||
public class BoolEventChannelSO : BaseEventChannelSO<bool> { }
|
||
|
||
// Assets/Scripts/Core/Events/IntEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/Int")]
|
||
public class IntEventChannelSO : BaseEventChannelSO<int> { }
|
||
|
||
// Assets/Scripts/Core/Events/FloatEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/Float")]
|
||
public class FloatEventChannelSO : BaseEventChannelSO<float> { }
|
||
|
||
// Assets/Scripts/Core/Events/StringEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/String")]
|
||
public class StringEventChannelSO : BaseEventChannelSO<string> { }
|
||
|
||
// Assets/Scripts/Core/Events/Vector2EventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/Vector2")]
|
||
public class Vector2EventChannelSO : BaseEventChannelSO<Vector2> { }
|
||
|
||
// Assets/Scripts/Core/Events/TransformEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/Transform")]
|
||
public class TransformEventChannelSO : BaseEventChannelSO<Transform> { }
|
||
```
|
||
|
||
### 游戏专用负载频道
|
||
|
||
```csharp
|
||
// Assets/Scripts/Core/Events/GameStateEventChannelSO.cs
|
||
// ⚠️ payload 类型为 GameStateId(值类型 struct),非旧 GameState 枚举
|
||
[CreateAssetMenu(menuName = "Events/GameState")]
|
||
public class GameStateEventChannelSO : BaseEventChannelSO<GameStateId> { }
|
||
|
||
// Assets/Scripts/Core/Events/SceneLoadRequestEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/SceneLoadRequest")]
|
||
public class SceneLoadRequestEventChannelSO : BaseEventChannelSO<SceneLoadRequest> { }
|
||
|
||
// Assets/Scripts/Core/Events/DamageInfoEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/DamageInfo")]
|
||
public class DamageInfoEventChannelSO : BaseEventChannelSO<DamageInfo> { }
|
||
|
||
// Assets/Scripts/Core/Events/ShopPurchaseEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/ShopPurchase")]
|
||
public class ShopPurchaseEventChannelSO : BaseEventChannelSO<ShopPurchaseEvent> { }
|
||
|
||
// Assets/Scripts/Core/Events/DialogueEventChannelSO.cs
|
||
[CreateAssetMenu(menuName = "Events/DialogueRequest")]
|
||
public class DialogueEventChannelSO : BaseEventChannelSO<DialogueDataSO> { }
|
||
|
||
// Assets/Scripts/Core/Events/BossSkillEventChannelSO.cs
|
||
[System.Serializable]
|
||
public struct BossSkillEvent { public string BossId; public string SkillId; }
|
||
[CreateAssetMenu(menuName = "Events/BossSkill")]
|
||
public class BossSkillEventChannelSO : BaseEventChannelSO<BossSkillEvent> { }
|
||
|
||
// Assets/Scripts/Core/Events/BossPhaseEventChannelSO.cs
|
||
[System.Serializable]
|
||
public struct BossPhaseEvent { public string BossId; public int Phase; }
|
||
[CreateAssetMenu(menuName = "Events/BossPhase")]
|
||
public class BossPhaseEventChannelSO : BaseEventChannelSO<BossPhaseEvent> { }
|
||
|
||
// Assets/Scripts/Core/Events/AbilityTypeEventChannelSO.cs
|
||
// AbilityType 定义于 09_ProgressionModule §1(enum AbilityType)
|
||
[CreateAssetMenu(menuName = "Events/AbilityType")]
|
||
public class AbilityTypeEventChannelSO : BaseEventChannelSO<AbilityType> { }
|
||
|
||
// Assets/Scripts/Core/Events/HitConfirmedEventChannelSO.cs
|
||
[System.Serializable]
|
||
public struct HitInfo { public DamageInfo DamageInfo; public Vector3 HitPoint; }
|
||
[CreateAssetMenu(menuName = "Events/HitConfirmed")]
|
||
public class HitConfirmedEventChannelSO : BaseEventChannelSO<HitInfo> { }
|
||
|
||
// Assets/Scripts/Core/Events/ColorblindModeEventChannelSO.cs
|
||
// ColorblindMode 枚举定义于 16_SupportingModules §AccessibilityManager
|
||
[CreateAssetMenu(menuName = "Events/ColorblindMode")]
|
||
public class ColorblindModeEventChannelSO : BaseEventChannelSO<ColorblindMode> { }
|
||
|
||
// Assets/Scripts/Core/Events/DifficultyChangedEventChannel.cs
|
||
// ⚠️ 命名按 Architecture 19 约定:不加 "SO" 后缀(与其他频道类有意区分以标识自定义 payload)
|
||
// DifficultyScalerSO 定义于 19_DifficultyModule §3
|
||
[CreateAssetMenu(menuName = "Events/DifficultyChanged")]
|
||
public class DifficultyChangedEventChannel : BaseEventChannelSO<DifficultyScalerSO> { }
|
||
|
||
// Assets/Scripts/Core/Events/LiquidEventChannelSO.cs
|
||
// LiquidZone 定义于 21_LiquidPuzzleModule §4
|
||
[CreateAssetMenu(menuName = "Events/LiquidZone")]
|
||
public class LiquidEventChannelSO : BaseEventChannelSO<LiquidZone> { }
|
||
|
||
// Assets/Scripts/Core/Events/WorldMarkerEventChannelSO.cs
|
||
// WorldMarker 定义于 21_LiquidPuzzleModule §14
|
||
[CreateAssetMenu(menuName = "Events/WorldMarker")]
|
||
public class WorldMarkerEventChannelSO : BaseEventChannelSO<WorldMarker> { }
|
||
|
||
// Assets/Scripts/Core/Events/QuestStateChangedEventChannel.cs
|
||
// QuestState 枚举定义于 22_QuestChallengeModule §QuestSO
|
||
[System.Serializable]
|
||
public struct QuestStateChangedEvent { public string QuestId; public QuestState State; }
|
||
[CreateAssetMenu(menuName = "Events/QuestStateChanged")]
|
||
public class QuestStateChangedEventChannel : BaseEventChannelSO<QuestStateChangedEvent> { }
|
||
|
||
// Assets/Scripts/Core/Events/QuestObjectiveEventChannelSO.cs
|
||
// 任务目标进度频道(用于 QuestManager 逐目标通知订阅者)
|
||
[System.Serializable]
|
||
public struct QuestObjectiveEvent { public string QuestId; public string ObjectiveId; public int Progress; public int Required; }
|
||
[CreateAssetMenu(menuName = "Events/QuestObjective")]
|
||
public class QuestObjectiveEventChannelSO : BaseEventChannelSO<QuestObjectiveEvent> { }
|
||
|
||
// Assets/Scripts/Core/Events/StatusEffectEventChannelSO.cs
|
||
// 状态效果频道(StatusEffectManager 施加/过期时广播;StatusEffectType 定义于 06_CombatModule §11)
|
||
[CreateAssetMenu(menuName = "Events/StatusEffect")]
|
||
public class StatusEffectEventChannelSO : BaseEventChannelSO<StatusEffectType> { }
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 全局事件频道 SO 资产清单
|
||
|
||
所有频道资产预建于 `Assets/Data/Events/`,资产文件名格式:`EVT_{描述}.asset`
|
||
|
||
### Core 系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_GameStateChanged` | `GameStateEventChannelSO` | `GameManager` | `UIManager`、`AudioManager`、`InputManager` |
|
||
| `EVT_SceneLoadRequest` | `SceneLoadRequestEventChannelSO` | `RoomTransition`、`GameManager` | `SceneLoader` |
|
||
| `EVT_SceneLoaded` | `StringEventChannelSO` | `SceneLoader` | `GameManager`、`MapManager` |
|
||
| `EVT_PauseRequested` | `VoidEventChannelSO` | `InputReader`(Pause 键) | `GameManager` |
|
||
| `EVT_DifficultyChanged` | `DifficultyChangedEventChannel` | `DifficultyManager.Apply()` | `PlayerStats`、`EnemyStats`(缩放属性) |
|
||
|
||
### 玩家系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_PlayerDied` | `VoidEventChannelSO` | `PlayerStats`(HP ≤ 0) | `GameManager`、`UIManager`、`AudioManager` |
|
||
| `EVT_DeathScreenConfirmed` | `VoidEventChannelSO` | `DeathScreenController`(Respawn 按钮) | `GameManager`(启动 RespawnCoroutine) |
|
||
| `EVT_PlayerRespawned` | `VoidEventChannelSO` | `GameManager`(复活流程末) | `UIManager`、`AudioManager` |
|
||
| `EVT_HPChanged` | `IntEventChannelSO` | `PlayerStats.TakeDamage/HealHP` | `HUDController`(血量 UI) |
|
||
| `EVT_MaxHPChanged` | `IntEventChannelSO` | `PlayerStats.UnlockHP` | `HUDController` |
|
||
| `EVT_SoulPowerChanged` | `IntEventChannelSO` | `PlayerStats.AddSoulPower` | `HUDController` |
|
||
| `EVT_SpiritPowerChanged` | `IntEventChannelSO` | `PlayerStats.AddSpiritPower` | `HUDController` |
|
||
| `EVT_SpringChargesChanged` | `IntEventChannelSO` | `PlayerStats.UseSpring/RestoreSpring` | `HUDController` |
|
||
| `EVT_GeoChanged` | `IntEventChannelSO` | `PlayerStats.AddGeo` | `HUDController` |
|
||
| `EVT_AbilityUnlocked` | `StringEventChannelSO`(abilityId) | `PlayerStats.UnlockAbility` | `AbilityGate`、`HUDController`、`TutorialManager` |
|
||
| `EVT_PlayerFormChanged` | `IntEventChannelSO`(FormType) | `FormController` | `HUDController`、`AudioManager` |
|
||
| `EVT_SkillSetChanged` | `VoidEventChannelSO` | `FormController` | `SkillHUD`(刷新技能栏 UI) |
|
||
| `EVT_ParrySuccess` | `VoidEventChannelSO` | `ParrySystem` | `PlayerStats`(+20 SoulPower)、`Feedback`、`AudioManager` |
|
||
| `EVT_ShieldHPChanged` | `IntEventChannelSO` | `ShieldComponent`(盾牌受击/修复) | `HUDController`(盾槽 UI) |
|
||
|
||
### 战斗系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_EnemyDied` | `TransformEventChannelSO` | `EnemyBase`(HP ≤ 0) | `PlayerStats`(击杀点)、`QuestManager` |
|
||
| `EVT_HitConfirmed` | `HitConfirmedEventChannelSO`(HitInfo) | `HurtBox.ReceiveDamage` | `HitFXSpawner`、`AudioManager`、`PlayerFeedback`(受击方) |
|
||
| `EVT_DamageDealt` | `DamageInfoEventChannelSO` | `HurtBox.ReceiveDamage` | `AnalyticsManager` |
|
||
| `EVT_BossFightStarted` | `StringEventChannelSO`(bossId) | `BossOrchestrator` | `GameManager`(切换 BossFight 状态)、`AudioManager` |
|
||
| `EVT_BossFightEnded` | `BoolEventChannelSO`(胜利/失败) | `BossBase.Die()` | `GameManager`、`AudioManager`、`UIManager` |
|
||
| `EVT_BossFightToggled` | `BoolEventChannelSO`(true=开始,false=结束) | `BossOrchestrator`(开始)、`BossBase.Die()`(结束) | `BossHPBar`(显示/隐藏 Boss 血条) |
|
||
| `EVT_BossHPChanged` | `IntEventChannelSO` | `BossBase`(受击/回血) | `BossHPBar`(HP 进度条更新) |
|
||
| `EVT_BossNameSet` | `StringEventChannelSO`(bossName) | `BossOrchestrator`(战斗开始时) | `BossHPBar`(显示 Boss 名称标签) |
|
||
| `EVT_BossHPMaxSet` | `IntEventChannelSO` | `BossOrchestrator`(战斗开始时) | `BossHPBar`(初始化满血值) |
|
||
| `EVT_BossPhaseChanged` | `BossPhaseEventChannelSO`(bossId, phase) | `BossBase.EnterPhase()` | `BossHUD`(相机切换通过 `CameraStateController.Instance` 直接调用,不订阅事件)|
|
||
| `EVT_BossSkillStarted` | `BossSkillEventChannelSO`(bossId, skillId) | `BossSkillExecutor` | `BossHUD`(显示技能名)(震动通过 `CameraStateController.Instance.TriggerImpulse()` 直接调用)|
|
||
| `EVT_BossSkillEnded` | `BossSkillEventChannelSO`(bossId, skillId) | `BossSkillExecutor` | `BossOrchestrator`(BD 决策推进) |
|
||
| `EVT_BossVulnerabilityWindowOpened` | `StringEventChannelSO`(bossId) | `WeakPointSystem` | `PlayerFeedback`(提示音效)、`HUDController` |
|
||
| `EVT_NailClash` | `VoidEventChannelSO` | `ClashResolver`(玩家与敌人 HitBox 对碰) | `VFXSpawner`(拼刀特效)、`AudioManager`(拼刀音效)、`CameraStateController`(轻振动) |
|
||
| `EVT_StatusEffectApplied` | `StatusEffectEventChannelSO` | `StatusEffectManager.ApplyEffect()` | `HUDController`(状态图标)、`PlayerFeedback`(特效) |
|
||
| `EVT_StatusEffectExpired` | `StatusEffectEventChannelSO` | `StatusEffectManager.Tick()` | `HUDController`(移除状态图标) |
|
||
|
||
### 世界系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_RoomTransitionRequest` | `SceneLoadRequestEventChannelSO` | `RoomTransition` | `SceneLoader` |
|
||
| `EVT_SavePointActivated` | `StringEventChannelSO`(saveId) | `SavePoint` | `GameManager`(触发存档)、`HUDController` |
|
||
| `EVT_FastTravelOpen` | `VoidEventChannelSO` | `SavePoint`(快速旅行已解锁) | `UIManager`(显示快速旅行面板) |
|
||
| `EVT_CollectiblePickup` | `StringEventChannelSO`(itemId) | `Collectible` | `PlayerStats`、`QuestManager`、`AnalyticsManager` |
|
||
| `EVT_GeoRecovered` | `StringEventChannelSO`(sceneId) | `DeathShade.Interact()` | `SaveManager`(标记该场景遗骸已回收) |
|
||
| `EVT_ShowInteractPrompt` | `StringEventChannelSO`(promptText) | `InteractableDetector` | `HUDController`(显示交互提示 UI) |
|
||
| `EVT_HideInteractPrompt` | `VoidEventChannelSO` | `InteractableDetector` | `HUDController`(隐藏交互提示 UI) |
|
||
|
||
### UI / 对话
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_DialogueStartRequest` | `DialogueEventChannelSO` | `InteractableNPC` | `DialogueManager` |
|
||
| `EVT_DialogueStarted` | `VoidEventChannelSO` | `DialogueManager`(对话正式开始) | `InputReaderSO`(切 UI 输入)、`PlayerController`(锁定输入) |
|
||
| `EVT_DialogueEnded` | `VoidEventChannelSO` | `DialogueManager` | `GameManager`(恢复 Gameplay)、`InputReaderSO`(切回 Gameplay) |
|
||
| `EVT_NpcDialogueCompleted` | `StringEventChannelSO`(npcId) | `DialogueManager`(与特定 NPC 对话结束) | `QuestManager`(任务目标进度追踪) |
|
||
| `EVT_CutsceneStarted` | `VoidEventChannelSO` | `CutsceneManager` | `HUDController`(隐藏 HUD)、`InputReaderSO`(禁用输入) |
|
||
| `EVT_CutsceneEnded` | `VoidEventChannelSO` | `CutsceneManager` | `HUDController`(恢复 HUD) |
|
||
| `EVT_ShowPanel` | `StringEventChannelSO`(panelId) | 各触发源 | `UIManager` |
|
||
| `EVT_HidePanel` | `StringEventChannelSO`(panelId) | 各触发源 | `UIManager` |
|
||
|
||
### 商店系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_ShopOpened` | `StringEventChannelSO`(shopId) | `ShopController.Open()` | `UIManager`(显示 ShopPanel) |
|
||
| `EVT_ShopClosed` | `VoidEventChannelSO` | `ShopPanel` 关闭按钮 | `UIManager`(隐藏 ShopPanel) |
|
||
| `EVT_ItemPurchased` | `ShopPurchaseEventChannelSO` | `ShopController` | `PlayerStats`(扣 Geo)、`AchievementManager`(购买成就) |
|
||
|
||
### 存档系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_SaveIndicatorVisible` | `BoolEventChannelSO`(true=显示,false=隐藏) | `SaveManager.SaveAsync()` | `HUDController`(显示/隐藏保存中图标) |
|
||
|
||
### 音频系统
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_RegionEntered` | `StringEventChannelSO`(zoneId) | `AudioZone` | `BGMController`(按 zoneId 查 AudioConfigSO 切换 BGM) |
|
||
| `EVT_PlayBGM` | `StringEventChannelSO`(bgmKey) | `GameManager` 等非区域触发源 | `BGMController` |
|
||
| `EVT_StopBGM` | `VoidEventChannelSO` | `GameManager` | `BGMController` |
|
||
| `EVT_PlaySFX` | `StringEventChannelSO`(sfxKey) | 各触发源 | `AudioManager` |
|
||
|
||
### 可访问性 / 成就 / 防软锁
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_AchievementUnlocked` | `StringEventChannelSO`(achievementId) | `AchievementManager` | `ToastManager`(显示成就 Toast) |
|
||
| `EVT_SoftlockDetected` | `VoidEventChannelSO` | `AntiSoftlockSystem` | `UIManager`(显示确认对话框) |
|
||
| `EVT_ColorblindModeChanged` | `ColorblindModeEventChannelSO` | `AccessibilityManager` | URP Feature(色觉 LUT 切换) |
|
||
| `EVT_SubtitlesToggled` | `BoolEventChannelSO` | `AccessibilityManager` | `DialogueBox`(字幕显隐) |
|
||
| `EVT_HighContrastToggled` | `BoolEventChannelSO` | `AccessibilityManager` | `UIManager`(高对比度 UI Theme 切换) |
|
||
|
||
### 液体 / 导航标记
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_LiquidEntered` | `LiquidEventChannelSO`(LiquidZone) | `LiquidZone.OnTriggerEnter2D()` | `PlayerController`(切换 SwimState) |
|
||
| `EVT_LiquidExited` | `LiquidEventChannelSO`(LiquidZone) | `LiquidZone.OnTriggerExit2D()` | `PlayerController`(退出 SwimState) |
|
||
| `EVT_DrownProgress` | `FloatEventChannelSO`(0–1 进度) | `LiquidZone`(窒息计时器每帧) | `HUDController`(窒息条 UI) |
|
||
| `EVT_PlayerDrowned` | `VoidEventChannelSO` | `LiquidZone`(窒息计时器归零) | `GameManager`(触发死亡流程) |
|
||
| `EVT_WorldMarkerActivated` | `WorldMarkerEventChannelSO`(WorldMarker) | `WorldMarker.Activate()` | `HUDController`(指引箭头)、`MapManager`(地图图标) |
|
||
| `EVT_WorldMarkerDeactivated` | `WorldMarkerEventChannelSO`(WorldMarker) | `WorldMarker.Deactivate()` | `HUDController`、`MapManager` |
|
||
|
||
### 任务 / 挑战
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_QuestStarted` | `StringEventChannelSO`(questId) | `QuestManager.StartQuest()` | `QuestLogUI`(新增条目)、`HUDController`(Toast 提示) |
|
||
| `EVT_QuestCompleted` | `StringEventChannelSO`(questId) | `QuestManager.CompleteQuest()` | `QuestGiver`(刷新对话)、`QuestLogUI`、`AchievementManager` |
|
||
| `EVT_QuestFailed` | `StringEventChannelSO`(questId) | `QuestManager.FailQuest()` | `QuestLogUI`(标记失败)、`HUDController`(失败提示) |
|
||
| `EVT_ObjectiveUpdated` | `QuestObjectiveEventChannelSO`(QuestObjectiveEvent) | `QuestManager.UpdateObjective()` | `QuestLogUI`(进度刷新)、`HUDController`(目标追踪 UI) |
|
||
| `EVT_ChallengeCompleted` | `StringEventChannelSO`(challengeId) | `ChallengeRoomManager` | `HUDController`(结算界面)、`AchievementManager` |
|
||
| `EVT_ChallengeFailed` | `StringEventChannelSO`(challengeId) | `ChallengeRoomManager` | `SaveManager`(触发读档)、`HUDController` |
|
||
|
||
### 事件链 / EventChain
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_ChainCompleted` | `StringEventChannelSO`(chainId) | `EventChainManager` | `ChainCompletedCondition`(链间依赖) |
|
||
| `EVT_DoorOpened` | `StringEventChannelSO`(doorId) | `OpenDoorAction` | `DoorController`(物理开门动画) |
|
||
| `EVT_FlagChanged` | `StringEventChannelSO`(flagId) | `SetFlagAction` | `InteractableNPC`(条件对话刷新) |
|
||
|
||
---
|
||
|
||
## 5. 发布 / 订阅模式
|
||
|
||
### 标准订阅写法
|
||
|
||
```csharp
|
||
public class HUDController : MonoBehaviour
|
||
{
|
||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||
[SerializeField] private IntEventChannelSO _onSoulPowerChanged;
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onHPChanged.OnEventRaised += UpdateHPBar;
|
||
_onSoulPowerChanged.OnEventRaised += UpdateSoulBar;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_onHPChanged.OnEventRaised -= UpdateHPBar;
|
||
_onSoulPowerChanged.OnEventRaised -= UpdateSoulBar;
|
||
}
|
||
|
||
private void UpdateHPBar(int newHP) { /* ... */ }
|
||
private void UpdateSoulBar(int newSoul) { /* ... */ }
|
||
}
|
||
```
|
||
|
||
### 标准发布写法
|
||
|
||
```csharp
|
||
public class PlayerStats : MonoBehaviour
|
||
{
|
||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||
|
||
public void TakeDamage(int amount)
|
||
{
|
||
_currentHP = Mathf.Max(0, _currentHP - amount);
|
||
_onHPChanged.Raise(_currentHP);
|
||
|
||
if (_currentHP == 0)
|
||
_onPlayerDied.Raise();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. MMEventManager(备用广播)
|
||
|
||
用于**无需预建 SO 资产的临时广播**,或 Feel 框架自带事件(如 `MMTopDownEngineEvent`)。
|
||
|
||
```csharp
|
||
// 发布(任意位置)
|
||
MMEventManager.TriggerEvent(new PlayerDiedEvent());
|
||
|
||
// 订阅(实现 MMEventListener<T>)
|
||
public class AudioManager : MonoBehaviour, MMEventListener<PlayerDiedEvent>
|
||
{
|
||
private void OnEnable() => this.MMEventStartListening<PlayerDiedEvent>();
|
||
private void OnDisable() => this.MMEventStopListening<PlayerDiedEvent>();
|
||
|
||
public void OnMMEvent(PlayerDiedEvent eventType)
|
||
{
|
||
// 响应
|
||
}
|
||
}
|
||
```
|
||
|
||
**仅在 Feel 框架集成场景下使用**;其他跨系统通信统一使用 SO 事件频道。
|
||
|
||
---
|
||
|
||
## 7. EventChannelRegistry
|
||
|
||
`EventChannelRegistry` 是运行时频道查找辅助单例,专为**无法在 Inspector 中序列化 SO 引用的动态场景**设计(典型用例:`ICharmEffect` SO 实例需要在运行时订阅事件频道)。
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/Core/Events/EventChannelRegistry.cs
|
||
namespace BaseGames.Core.Events
|
||
{
|
||
/// <summary>
|
||
/// 运行时事件频道注册表。
|
||
/// 在 Persistent 场景 Awake 时由 EventChannelRegistrar 注册所有频道 SO,
|
||
/// 供不能持有 [SerializeField] 的动态对象(CharmEffect SO 等)按类型名查找频道。
|
||
/// </summary>
|
||
public class EventChannelRegistry : MonoBehaviour
|
||
{
|
||
public static EventChannelRegistry Instance { get; private set; }
|
||
|
||
private readonly Dictionary<string, ScriptableObject> _channels = new();
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance != null) { Destroy(gameObject); return; }
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
}
|
||
|
||
/// <summary>由 EventChannelRegistrar 在场景初始化时批量注册频道 SO。</summary>
|
||
public void Register(string key, ScriptableObject channel)
|
||
=> _channels[key] = channel;
|
||
|
||
/// <summary>
|
||
/// 按 key 查找频道。key 约定 = SO 资产文件名(不含扩展名),如 "EVT_OnHitConfirmed"。
|
||
/// 找不到时输出 Error 并返回 null。
|
||
/// </summary>
|
||
public T Get<T>(string key) where T : ScriptableObject
|
||
{
|
||
if (_channels.TryGetValue(key, out var ch) && ch is T typed) return typed;
|
||
Debug.LogError($"[EventChannelRegistry] Key '{key}' not found or wrong type.");
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> **注册时机**:Persistent 场景中的 `EventChannelRegistrar` 组件在 `Awake`(最早执行)时调用
|
||
> `EventChannelRegistry.Instance.Register(key, channelSO)` 完成全部频道注册,
|
||
> 确保 `EquipmentManager.Awake()` 构建 `EquipmentContext` 时 `Instance` 已就绪。
|
||
> 普通 MonoBehaviour 仍优先使用 `[SerializeField]` 直接引用 SO,Registry 仅供动态 SO 对象使用。
|
||
|
||
---
|
||
|
||
## 8. 事件订阅生命周期管理 — 自动注销机制
|
||
|
||
> **P1 优化**:手动在 `OnDisable` 配对 `OnEnable` 的取消注册,在多场景 Additive 加载时易遗漏导致重复注册或 NullRef。引入 `EventSubscription` / `CompositeDisposable` 模式统一管理。
|
||
|
||
### 8.1 EventSubscription — 单订阅 Disposable
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/Core/Events/EventSubscription.cs
|
||
namespace BaseGames.Core.Events
|
||
{
|
||
/// <summary>
|
||
/// 单条订阅的 Disposable 句柄。Dispose() 自动取消注册。
|
||
/// 配合 using 块或 CompositeDisposable 批量释放。
|
||
/// </summary>
|
||
public readonly struct EventSubscription : System.IDisposable
|
||
{
|
||
private readonly System.Action _unsubscribe;
|
||
|
||
public EventSubscription(System.Action unsubscribe)
|
||
=> _unsubscribe = unsubscribe;
|
||
|
||
public void Dispose() => _unsubscribe?.Invoke();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 CompositeDisposable — 批量注销容器
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/Core/Events/CompositeDisposable.cs
|
||
namespace BaseGames.Core.Events
|
||
{
|
||
/// <summary>
|
||
/// 批量管理多条订阅,统一在 Dispose / Clear 时取消所有注册。
|
||
/// MonoBehaviour 生命周期:OnEnable 调用 Subscribe,OnDisable 调用 Clear。
|
||
/// </summary>
|
||
public sealed class CompositeDisposable : System.IDisposable
|
||
{
|
||
private readonly List<System.IDisposable> _items = new();
|
||
|
||
public void Add(System.IDisposable item) => _items.Add(item);
|
||
|
||
public void Clear()
|
||
{
|
||
foreach (var item in _items) item.Dispose();
|
||
_items.Clear();
|
||
}
|
||
|
||
public void Dispose() => Clear();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.3 BaseEventChannelSO — Subscribe 扩展重载
|
||
|
||
```csharp
|
||
// 追加到 BaseEventChannelSO<T>(路径: Assets/Scripts/Core/Events/BaseEventChannelSO.cs)
|
||
namespace BaseGames.Core.Events
|
||
{
|
||
public abstract class BaseEventChannelSO<T> : ScriptableObject
|
||
{
|
||
public event Action<T> OnEventRaised;
|
||
|
||
public void Raise(T value) => OnEventRaised?.Invoke(value);
|
||
|
||
/// <summary>
|
||
/// 订阅并返回可 Dispose 的订阅句柄。
|
||
/// 推荐与 CompositeDisposable 配合使用,代替手动 OnDisable 取消注册。
|
||
/// </summary>
|
||
public EventSubscription Subscribe(Action<T> callback)
|
||
{
|
||
OnEventRaised += callback;
|
||
return new EventSubscription(() => OnEventRaised -= callback);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.4 使用模式
|
||
|
||
**标准用法**(替代 OnEnable/OnDisable 手动配对):
|
||
|
||
```csharp
|
||
public class HUDController : MonoBehaviour
|
||
{
|
||
[SerializeField] VoidEventChannelSO _onCutsceneStarted;
|
||
[SerializeField] VoidEventChannelSO _onCutsceneEnded;
|
||
[SerializeField] IntEventChannelSO _onHealthChanged;
|
||
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
// ── 只写 OnEnable,OnDisable 统一 Clear ──────────────────────────
|
||
private void OnEnable()
|
||
{
|
||
_subs.Add(_onCutsceneStarted.Subscribe(_ => HideHUD()));
|
||
_subs.Add(_onCutsceneEnded.Subscribe(_ => ShowHUD()));
|
||
_subs.Add(_onHealthChanged.Subscribe(UpdateHealthBar));
|
||
}
|
||
|
||
private void OnDisable() => _subs.Clear();
|
||
}
|
||
```
|
||
|
||
**动态订阅**(运行时按需订阅,对象销毁时自动取消):
|
||
|
||
```csharp
|
||
// CharmEffect SO 动态订阅(不持有 MonoBehaviour 生命周期)
|
||
public class CharmEffect : StatusEffectSO
|
||
{
|
||
private EventSubscription _sub;
|
||
|
||
public override void OnApply(GameObject target)
|
||
{
|
||
var channel = EventChannelRegistry.Instance
|
||
.Get<VoidEventChannelSO>("EVT_CharmExpired");
|
||
_sub = channel.Subscribe(_ => OnCharmExpired());
|
||
}
|
||
|
||
public override void OnRemove(GameObject target)
|
||
{
|
||
_sub.Dispose(); // 精确注销,无需持有 channel 引用
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.5 迁移规范
|
||
|
||
| 旧模式 | 新模式 | 备注 |
|
||
|--------|--------|------|
|
||
| `OnEnable` + `OnDisable` 手动 `+=` / `-=` | `CompositeDisposable` + `Subscribe()` | 一对多订阅的 MonoBehaviour |
|
||
| 单条订阅 + 手动 `-=` | `EventSubscription` (`using` 或字段) | 精确生命周期控制 |
|
||
| SO 内部动态订阅 | `EventSubscription` 字段 + `OnRemove Dispose` | CharmEffect 等运行时 SO |
|
||
|
||
> **注意**:`VoidEventChannelSO`(无参版)不继承 `BaseEventChannelSO<T>`,需补充等价 `Subscribe(Action callback)` 方法。原有 `OnEnable/OnDisable` 写法仍合法,迁移以新增代码为主,不强制改旧代码。
|
||
|
||
---
|
||
|
||
## 9. EventBusMonitorWindow — 运行时事件监控面板
|
||
|
||
> **P0 优化**:生产级调试能力。调试"玩家死了但死亡画面没有弹出"、"Boss HP 归零但胜利
|
||
> 事件未触发"等问题时,依靠 `Debug.Log` 逐行追踪效率极低。
|
||
> `EventBusMonitorWindow` 在 Play Mode 中实时显示所有 SO 事件频道的最近触发记录,
|
||
> 一眼定位"谁 Raise 了什么事件、何时触发、订阅者当前有几个"。
|
||
|
||
### 9.1 运行时埋点 — BaseEventChannelSO 修改
|
||
|
||
```csharp
|
||
// 修改: Assets/Scripts/Core/Events/BaseEventChannelSO.cs
|
||
public abstract class BaseEventChannelSO<T> : ScriptableObject
|
||
{
|
||
[Multiline] public string description;
|
||
public event Action<T> OnEventRaised;
|
||
|
||
public void Raise(T value)
|
||
{
|
||
OnEventRaised?.Invoke(value);
|
||
#if UNITY_EDITOR
|
||
EventBusMonitor.Record(name, value?.ToString() ?? "<void>",
|
||
OnEventRaised?.GetInvocationList().Length ?? 0);
|
||
#endif
|
||
}
|
||
// Subscribe() 见 §8.3
|
||
}
|
||
|
||
// ✅ VoidBaseEventChannelSO.Raise() 已同步添加 EventBusMonitor.Record(见 §2 VoidBaseEventChannelSO)
|
||
```
|
||
|
||
### 9.2 EventBusMonitor — 静态记录缓冲
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/Core/Events/Editor/EventBusMonitor.cs
|
||
#if UNITY_EDITOR
|
||
namespace BaseGames.Core.Events.Editor
|
||
{
|
||
/// <summary>
|
||
/// 运行时事件触发记录(仅 Editor)。
|
||
/// BaseEventChannelSO.Raise() 内部调用,CircularBuffer 避免无限增长。
|
||
/// </summary>
|
||
public static class EventBusMonitor
|
||
{
|
||
public const int MaxRecords = 200;
|
||
|
||
public struct EventRecord
|
||
{
|
||
public double Timestamp; // Time.realtimeSinceStartupAsDouble
|
||
public string ChannelName; // SO 资产名称(如 "EVT_PlayerDied")
|
||
public string PayloadText; // value.ToString()
|
||
public int SubscriberCount; // 触发时订阅者数量
|
||
public int Frame; // Time.frameCount
|
||
}
|
||
|
||
private static readonly Queue<EventRecord> _records = new(MaxRecords + 1);
|
||
public static IReadOnlyCollection<EventRecord> Records => _records;
|
||
|
||
// 供 EditorWindow 订阅"有新记录"通知
|
||
public static event System.Action OnRecordAdded;
|
||
|
||
public static void Record(string channelName, string payload, int subCount)
|
||
{
|
||
if (_records.Count >= MaxRecords) _records.Dequeue();
|
||
_records.Enqueue(new EventRecord
|
||
{
|
||
Timestamp = UnityEngine.Time.realtimeSinceStartupAsDouble,
|
||
ChannelName = channelName,
|
||
PayloadText = payload,
|
||
SubscriberCount = subCount,
|
||
Frame = UnityEngine.Time.frameCount,
|
||
});
|
||
OnRecordAdded?.Invoke();
|
||
}
|
||
|
||
public static void Clear() => _records.Clear();
|
||
|
||
// 搜索过滤(大小写不敏感)
|
||
public static IEnumerable<EventRecord> Filter(string keyword)
|
||
=> string.IsNullOrEmpty(keyword)
|
||
? _records
|
||
: _records.Where(r => r.ChannelName.IndexOf(keyword,
|
||
System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||
}
|
||
}
|
||
#endif
|
||
```
|
||
|
||
### 9.3 EventBusMonitorWindow — EditorWindow
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/Editor/EventBusMonitorWindow.cs
|
||
#if UNITY_EDITOR
|
||
namespace BaseGames.Editor
|
||
{
|
||
/// <summary>
|
||
/// 菜单路径: BaseGames/Tools/Event Bus Monitor
|
||
/// 快捷键: Ctrl+Shift+E
|
||
/// </summary>
|
||
public class EventBusMonitorWindow : EditorWindow
|
||
{
|
||
[MenuItem("BaseGames/Tools/Event Bus Monitor %#e")]
|
||
public static void Open()
|
||
=> GetWindow<EventBusMonitorWindow>("Event Bus Monitor");
|
||
|
||
// ── UI 状态 ────────────────────────────────────────────────────────
|
||
private string _filterText = "";
|
||
private bool _autoScroll = true;
|
||
private bool _pauseCapture = false;
|
||
private Vector2 _scrollPos;
|
||
private static readonly Color _zeroSubColor = new Color(1f, 0.4f, 0.4f); // 红:零订阅者触发
|
||
private static readonly Color _normalColor = Color.white;
|
||
|
||
private void OnEnable()
|
||
=> EventBusMonitor.OnRecordAdded += Repaint;
|
||
private void OnDisable()
|
||
=> EventBusMonitor.OnRecordAdded -= Repaint;
|
||
|
||
private void OnGUI()
|
||
{
|
||
// ── 工具栏 ──────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("Filter:", GUILayout.Width(40));
|
||
_filterText = EditorGUILayout.TextField(_filterText,
|
||
EditorStyles.toolbarSearchField, GUILayout.Width(200));
|
||
|
||
GUILayout.FlexibleSpace();
|
||
|
||
_pauseCapture = GUILayout.Toggle(_pauseCapture, "Pause",
|
||
EditorStyles.toolbarButton, GUILayout.Width(50));
|
||
_autoScroll = GUILayout.Toggle(_autoScroll, "Auto Scroll",
|
||
EditorStyles.toolbarButton, GUILayout.Width(80));
|
||
|
||
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(45)))
|
||
EventBusMonitor.Clear();
|
||
}
|
||
|
||
// ── 列标题 ──────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
EditorGUILayout.LabelField("Time", GUILayout.Width(70));
|
||
EditorGUILayout.LabelField("Frame", GUILayout.Width(55));
|
||
EditorGUILayout.LabelField("Channel", GUILayout.Width(220));
|
||
EditorGUILayout.LabelField("Payload", GUILayout.MinWidth(120));
|
||
EditorGUILayout.LabelField("Subs", GUILayout.Width(40));
|
||
}
|
||
EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
|
||
|
||
// ── 记录列表 ─────────────────────────────────────────────────
|
||
using var scroll = new EditorGUILayout.ScrollViewScope(_scrollPos);
|
||
_scrollPos = scroll.scrollPosition;
|
||
|
||
var records = EventBusMonitor.Filter(_filterText).ToArray();
|
||
foreach (var r in records)
|
||
{
|
||
var prevColor = GUI.color;
|
||
GUI.color = r.SubscriberCount == 0 ? _zeroSubColor : _normalColor;
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
EditorGUILayout.LabelField($"{r.Timestamp:F2}s", GUILayout.Width(70));
|
||
EditorGUILayout.LabelField($"#{r.Frame}", GUILayout.Width(55));
|
||
EditorGUILayout.LabelField(r.ChannelName, GUILayout.Width(220));
|
||
EditorGUILayout.LabelField(r.PayloadText, GUILayout.MinWidth(120));
|
||
EditorGUILayout.LabelField(r.SubscriberCount.ToString(), GUILayout.Width(40));
|
||
}
|
||
GUI.color = prevColor;
|
||
}
|
||
|
||
if (_autoScroll && Application.isPlaying)
|
||
_scrollPos.y = float.MaxValue;
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
```
|
||
|
||
### 9.4 使用指南
|
||
|
||
| 场景 | 操作 |
|
||
|------|------|
|
||
| 调试事件未触发 | Filter 输入频道名,观察 Records;空 = Raise 从未调用 |
|
||
| 调试无响应(Subs=0) | 行显示红色 = 有 Raise 但零订阅者,检查 OnEnable 注册 |
|
||
| 调试重复触发 | 观察同一频道在同一 Frame 内多次出现 |
|
||
| 性能分析 | 高频帧内 Raise 次数过多时考虑节流 |
|
||
| 生产构建 | `#if UNITY_EDITOR` 包裹,零运行时开销 |
|