81 lines
12 KiB
Markdown
81 lines
12 KiB
Markdown
# 2026-07-07
|
||
|
||
## Unity 项目解耦架构重构(gold_dolphin/unity)
|
||
|
||
- **架构审计结论**:定位到严重耦合信号
|
||
- 7 个跨场景单例:GameManager/AudioManager/ScoreManager/SceneLoader(均 PersistentSingleton)、TimeController(Singleton)、EnemyManager/LightMaskSystem(static Instance)
|
||
- `GameObject.Find/FindWithTag/FindObjectsByType` 散落运行时代码;`GameHUD.Update` 仅在 `_playerHealth==null` 时**条件性** `FindWithTag("Player")` 补救(非每帧,但仍是反模式);`EnemyAI` 的 FindWithTag 仅 Start 一次
|
||
- `GameHUD` 用 `typeof(X).GetField(..., NonPublic|Instance)` 反射读私有字段(health/rollCooldown/_lastEchoTime 等),字段改名会静默失效
|
||
- God Class:GameManager(约295行,6职责)、HealthSystem(约216行,健康+死亡+溶解);PlayerController 仅轻度耦合(112行,RequireComponent 3系统),非 God Class
|
||
- static 事件伪总线:onGameOver/onPlayerDamaged/OnEchoReleased/onScoreChanged
|
||
- EnemyAI 直接 `GetComponent<HealthSystem>().Damage()` + HealthSystem 反向调 `GameManager.GameOver()`
|
||
- `Resources.Load` 用于音频/视频
|
||
|
||
- **已落地解耦基础框架** `Assets/Architecture/`(增量、零侵入、未改现有文件):
|
||
- Core/GameEvent.cs(无参事件通道+监听器)、Core/TypedGameEvents.cs(GameEvent<T> + Int/Float/Vector3/String/GameObject 通道及监听器)
|
||
- Variables/(Float/Int/Bool/Vector3 Variable,带 OnValueChanged + ContextMenu 重置)
|
||
- RuntimeSets/RuntimeSet.cs(泛型集合 + TransformRuntimeSet + RuntimeSetRegistrar 自动注册)
|
||
- Editor/VariableDrawer.cs(Inspector 实时显示变量值)
|
||
|
||
- **产出方案文档** `Docs/解耦架构重构方案.md`:分层架构(LO SO总线 / L1系统 / L2实体 / L3表现)、逐条 Before→After 改造映射、P0-P6 分阶段路线图。
|
||
|
||
- **项目约定**:无 .asmdef,所有代码在 Assembly-CSharp;GameEvent/RuntimeSet/FloatVariable 等类名无冲突,新增代码可安全编入。
|
||
|
||
## 同行评审修正(2 份意见,全部核实采纳)
|
||
- 文档 v1.1 已根据评审重写 `Docs/解耦架构重构方案.md`:
|
||
- 严重度下调:`GameHUD` 的 FindWithTag 实为 `_playerHealth==null` 条件回退(非每帧);`PlayerController` 从 God Class 降级为轻度耦合(112行/RequireComponent 3系统)。
|
||
- 行号修正:`EnemyAI.Damage()` 在 `:232`(非 :230)。
|
||
- 补遗漏点:`EnemyHealthBar` 反射读私有字段(`:50-53,73-75`)、`GameHUD` Start 的 `FindObjectOfType<DamageFlashOverlay>`、`onGameWin` 静态事件被 `GameResultScreen` 监听、`GameLostOverlay` 的 `Shader.Find` 与 `FindObjectOfType` 自检查、`MainMenuUIController` 对象名耦合、以及 `ScorePickup/SoulDrop/ScoringUIController/MainMenuUIController/GameLostOverlay` 对 `ScoreManager/SceneLoader` 的直接单例调用线。
|
||
- **代码补强**(评审点):`TypedGameEvents.cs` 补 `StringEventListener`(初稿声称有但未实现);`RuntimeSet.cs` 补泛型 `RuntimeSetRegistrar<T>`(强类型集合如 `EnemyRuntimeSet` 用,领域文件夹写一行具体闭包子类即可)。
|
||
|
||
## 续作:P1+P2 落地改造(代码侧,本次会话)
|
||
|
||
- **P1 静态事件 → SO 事件资产(全部完成,含 onScoreSettled)**
|
||
- 发布方改写:GameManager(`onGameOver`/`onGameWin`→GameEvent)、HealthSystem(`onPlayerDamaged`→GameEvent)、EchoSystem(`OnEchoReleased`→Vector3Event)、ScoreManager(`onScoreChanged`/`onScoreSettled`→IntEvent)。
|
||
- 订阅方改写:GameResultScreen/GameOverScreen(→`onGameOver`/`onGameWin` GameEvent.Register)、DamageFlashOverlay(→`onPlayerDamagedEvent`.Register)、ScoreDisplay(→`scoreChangedEvent` IntEvent.Register)、EnemyAI(→`echoReleasedEvent` Vector3Event.Register)。
|
||
- API:GameEvent 同时支持 Inspector 监听器(GameEventListener) 与代码监听器 `Register(System.Action)`;GameEvent<T> 用 `Register(Action<T>)`。
|
||
- **P2a GameHUD**:接 PlayerHealth IntVariable(OnValueChanged 驱动 UpdateLifeIcons)+ scoreChangedEvent;技能 CD 改读 PlayerController.RollCooldown / EchoSystem.BellCooldown 公共只读属性(替代反射)。
|
||
- **P2b EnemyHealthBar 重要正确性修正**:原方案让所有 HealthSystem 写同一个全局 IntVariable,会导致多敌人共享同一份血量(严重 bug)。改为 EnemyHealthBar 引用**同物体** HealthSystem(RequireComponent 保证),订阅其 `OnHealthChanged` 事件刷新;HealthSystem 的全局 `healthVar`/`maxHealthVar` 写入**仅限 `isPlayer`**。HealthSystem 新增 `CurrentHealth`/`MaxHealth` 只读属性 + `OnHealthChanged` 事件。
|
||
- **P2d GameManager**:DisableAllEnemyAI 改用 Enemies(TransformRuntimeSet) 遍历禁用,替代 `FindObjectsOfType<EnemyAI>()`;EnemyManager 改 SerializeField 引用。保留 `FindObjectsOfType` 兜底分支(enemiesSet 未接入时),接入敌人 Prefab 的 RuntimeSetRegistrar 后可删。
|
||
- **P5 Shader.Find ×5 处理**:GameLostOverlay(水波纹→`waterRippleShader` 字段)、GroundBuilder/CliffWallBuilder(AbyssEdgeGlow→`edgeGlowShader` 字段);EchoSystem 已有 `ringShader` 字段;GroundClipTool.cs:19 为 Editor 脚本(运行时不进包,保留但建议改字段)。
|
||
- **变量跨 Play Mode 状态泄漏(用户重点反馈)**:VariableRegistry + `IVariable.ResetToDefault()` + `[RuntimeInitializeOnLoadMethod(BeforeSceneLoad)]` 在每次进 Play Mode 统一重置;AssetBootstrap 生成变量时同时写 `_value`/`_defaultValue`。
|
||
- **UIBuilder.cs**:BuildResultScreen 时自动 LoadAssetAtPath 连线 OnGameOver/OnGameWin 资产,使编辑器搭建的 UI 直接可用(需先运行 `Architecture > Bootstrap Core Assets`)。
|
||
- **仍需手动/后续步骤(Unity 编辑器内)**:
|
||
1. 运行菜单 `Architecture > Bootstrap Core Assets` 生成全部 SO 资产。
|
||
2. 各 UI/系统组件 Inspector 拖入对应事件/变量资产(GameResultScreen/GameOverScreen/DamageFlashOverlay/ScoreDisplay/EnemyAI/GameHUD 等)。
|
||
3. 敌人 Prefab 挂 RuntimeSetRegistrar(Enemies)、Player Prefab 挂 RuntimeSetRegistrar(Players),使运行时集合被填充;之后删 GameManager 的 FindObjectsOfType 兜底分支。
|
||
4. P2e 剩余:MainMenuUIController 对象名耦合 + GameLostOverlay.cs:85 FindObjectOfType 自检查;EnemyAI.cs:112 Start 一次性 FindWithTag(待 Players 集合接入后移除)。
|
||
- **验证**:全仓 grep 确认 `HealthSystem.onPlayerDamaged`/`ScoreManager.onScore*`/`EchoSystem.OnEchoReleased`/`GameManager.onGameOver/onGameWin` 已无代码引用(仅注释),新 API 表面(OnHealthChanged/CurrentHealth/RollCooldown/BellCooldown/IVariable)均已落地。
|
||
|
||
## 计划/方案文档同步(用户 4 条反馈)
|
||
- Addressables 过度:P5 改为仅 SerializeField 引用(AudioClip/VideoClip),不引入 Addressables。
|
||
- 变量重置策略:计划 P0/P1 明确 VariableRegistry + _defaultValue 方案。
|
||
- Shader.Find 5 处:P5 清单补全。
|
||
- onScoreSettled:P1 一并迁移。
|
||
- 两份文档(`plans/quantum-pulse-turing.md`、`Docs/解耦架构重构方案.md`)均已更新至反映上述反馈。
|
||
|
||
## 编译错误修复(用户反馈 4 个 CS 错误,已全部修正)
|
||
- `GameManager.cs:55/64`:CS0120 —— `GameOver()`/`Win()` 是 static 方法,却直接访问实例字段 `onGameOverEvent`/`onGameWinEvent`。改为 `Instance.onGameOverEvent?.Raise()` / `Instance.onGameWinEvent?.Raise()`(方法开头已有 `if (Instance == null) return;` 保护,安全)。
|
||
- `GameHUD.cs:82/102`:CS0123 —— `IntVariable.OnValueChanged` 是 `Action<int>`,但 `UpdateLifeIcons()` 是无参方法,委托签名不匹配。改为 `UpdateLifeIcons(int value)`(内部 `currentHealth = value`),并把初始直接调用 `UpdateLifeIcons()` → `UpdateLifeIcons(playerHealthVar.Value)`。
|
||
- **经验教训(可复用)**:① 把 static 事件总线改成 SO 事件字段后,原 `GameManager.GameOver()/Win()` 这类 static 入口方法必须改走 `Instance.字段`(或把字段也变 static,但 SO 资产不应是 static);② 订阅 `Variable.OnValueChanged`(`Action<T>`)时,回调方法必须带对应 `T` 参数,否则 CS0123。已提醒用户编译后按接线手册拖资产。
|
||
- `VariableDrawer.cs`:CS0246 —— `Architecture.Editor` 命名空间下的 Editor 脚本引用 `FloatVariable/IntVariable/BoolVariable/Vector3Variable`(位于 `Architecture.Variables`)时漏 `using Architecture.Variables;`。**规则**:Architecture 下的 Editor 脚本都需显式 `using Architecture.Variables;` 与 `using Architecture.Core;`。`AssetBootstrap.cs` 已带齐,无需改。
|
||
- `UIBuilder.cs`(位于 `Assets/Editor/`,非 Architecture/Editor):CS0246 —— 引用 `GameEvent`(`Architecture.Core`)漏 `using Architecture.Core;`,已在文件头补上。已全仓 grep 排雷:其余引用架构类型的外部文件(DamageFlashOverlay/GameHUD/GameOverScreen/GameResultScreen/EnemyAI/EchoSystem/HealthSystem/GameManager/ScoreDisplay/ScoreManager)均已在改写时带齐 using,无其它漏网。`TransformRuntimeSet` 与 `GameEvent` 同处 `Architecture.Core` 命名空间。
|
||
|
||
## 第二轮 Review 意见处理(2 份,全部落地)
|
||
- **关键时序修复**:`VariableRegistry.Register()` 内立即 `variable.ResetToDefault()`,解决 `RuntimeInitializeOnLoadMethod(BeforeSceneLoad)` 在 SO 尚未 OnEnable 注册前执行 `ResetAll`(_variables 为空)导致重置无效、跨局状态残留的问题。`ResetAll` 保留作兜底。
|
||
- **HealthSystem 去单例直调**:玩家死亡改为 `onPlayerDiedEvent?.Raise()`,不再 `GameManager.GameOver()`;`GameManager` 订阅 `OnPlayerDied` → `GameOver()`。新增双方 `onPlayerDiedEvent` 字段(同指 `OnPlayerDied` 资产)。
|
||
- **GameLostOverlay 解耦**:删除静态 `Show()` / `_instance` / `FindObjectOfType`,改为订阅 `OnGameOver` 事件(`OnEnable` 注册 → `Activate`)。`GameManager.DeathTransition` 不再直接调用。
|
||
- **空响应清理**:`GameResultScreen` 删除空 `OnGameOver` 方法+字段+订阅(只负责胜利);`GameOverScreen` 全仓无引用,已删除 `.cs`+`.meta`;`UIBuilder` 同步移除 `onGameOverEvent` 接线(否则 `FindProperty` 返回 null 抛 NRE)。
|
||
- **顺手修**:`GameEvent.Raise()` 改为 Inspector 监听(`_listeners`)先于代码监听(`_codeListeners`);`SpiritLanternSystem` 加 `public float Cooldown`,GameHUD 灵灯 CD 删最后一处反射;GameHUD 移除 `Update` 逐帧延迟订阅 ScoreManager(改为 Start 无条件注册);ScoreDisplay 初始值走事件不再访问 `ScoreManager.Instance`。
|
||
- **静态校验通过**:grep 确认无 `GameOverScreen`/`GameLostOverlay.Show`/`_instance`/`typeof(SpiritLanternSystem)` 残留;`onPlayerDiedEvent`(HealthSystem 发布/GameManager 订阅)、`onGameOverEvent`(GameManager 发布/GameLostOverlay 订阅) 一致。
|
||
- **新增接线要求**:HealthSystem+GameManager 的 `On Player Died Event`→`OnPlayerDied`;GameLostOverlay(场景物体) `On Game Over Event`→`OnGameOver`;DamageFlashOverlay 需场景预置已接线实例;GameResultScreen 仅 `On Game Win Event`。旧场景若残留 GameOverScreen 组件需手动移除。
|
||
|
||
## Play Mode 回归(用户实测,03:15)
|
||
- ✅ 通过:魂灵数(ScoreChanged)、生命图标(PlayerHealth)、玩家死亡过场(OnPlayerDied→OnGameOver+Enemies 集合)、VariableRegistry 重置(退出再进血量=5,无跨局泄漏)。
|
||
- ❌ 失败两项(已定位,均为 SO 事件资产接线缺口,非代码 bug):
|
||
1. **OnPlayerDamaged 受击泛红**:HealthSystem.Damage 第87行 `onPlayerDamagedEvent?.Raise()` 逻辑正确(同机制 OnPlayerDied 可通),断链原因为 HealthSystem 或/且 DamageFlashOverlay 的 `On Player Damaged Event` 字段为 null。
|
||
2. **EchoReleased 摇铃引敌**:EchoSystem.StartEcho 第220行 `echoReleasedEvent?.Raise(p)` + EnemyAI.OnEnable `Register(OnBell)` 均正确,断链原因为 EchoSystem 或/且 敌人预制体 EnemyAI 的 `Echo Released Event` 字段为 null(极可能玩家预制体 EchoSystem 未接)。
|
||
- **诊断加固(已落地)**:给 HealthSystem / EchoSystem / EnemyAI / DamageFlashOverlay 的事件字段加 `OnValidate()` 编辑器实时告警(Inspector 黄色三角+控制台),并在 Raise 处加一次性空引用 `Debug.LogWarning`;另把 GameHUD 中「自动 new 一个 event=null 的 DamageFlashOverlay」改为仅告警不创建,避免掩盖未接线问题。
|
||
- **结论**:代码链路正确,失败=事件字段未接。用户需在编辑器看 OnValidate 警告锁定漏接组件,重拖 `OnPlayerDamaged`/`EchoReleased` 资产(注意 EnemyAI 要接**预制体资产**而非场景临时实例)。
|
||
|