12 KiB
12 KiB
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 + 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)、GameHUDStart 的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/onGameWinGameEvent.Register)、DamageFlashOverlay(→onPlayerDamagedEvent.Register)、ScoreDisplay(→scoreChangedEventIntEvent.Register)、EnemyAI(→echoReleasedEventVector3Event.Register)。 - API:GameEvent 同时支持 Inspector 监听器(GameEventListener) 与代码监听器
Register(System.Action);GameEvent 用Register(Action<T>)。
- 发布方改写:GameManager(
- 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 编辑器内):
- 运行菜单
Architecture > Bootstrap Core Assets生成全部 SO 资产。 - 各 UI/系统组件 Inspector 拖入对应事件/变量资产(GameResultScreen/GameOverScreen/DamageFlashOverlay/ScoreDisplay/EnemyAI/GameHUD 等)。
- 敌人 Prefab 挂 RuntimeSetRegistrar(Enemies)、Player Prefab 挂 RuntimeSetRegistrar(Players),使运行时集合被填充;之后删 GameManager 的 FindObjectsOfType 兜底分支。
- 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):
- OnPlayerDamaged 受击泛红:HealthSystem.Damage 第87行
onPlayerDamagedEvent?.Raise()逻辑正确(同机制 OnPlayerDied 可通),断链原因为 HealthSystem 或/且 DamageFlashOverlay 的On Player Damaged Event字段为 null。 - EchoReleased 摇铃引敌:EchoSystem.StartEcho 第220行
echoReleasedEvent?.Raise(p)+ EnemyAI.OnEnableRegister(OnBell)均正确,断链原因为 EchoSystem 或/且 敌人预制体 EnemyAI 的Echo Released Event字段为 null(极可能玩家预制体 EchoSystem 未接)。
- OnPlayerDamaged 受击泛红:HealthSystem.Damage 第87行
- 诊断加固(已落地):给 HealthSystem / EchoSystem / EnemyAI / DamageFlashOverlay 的事件字段加
OnValidate()编辑器实时告警(Inspector 黄色三角+控制台),并在 Raise 处加一次性空引用Debug.LogWarning;另把 GameHUD 中「自动 new 一个 event=null 的 DamageFlashOverlay」改为仅告警不创建,避免掩盖未接线问题。 - 结论:代码链路正确,失败=事件字段未接。用户需在编辑器看 OnValidate 警告锁定漏接组件,重拖
OnPlayerDamaged/EchoReleased资产(注意 EnemyAI 要接预制体资产而非场景临时实例)。