仓库:vampire-like
本次变更说明
本次仅包含 1 个 commit(4250e48e),主要修复了从 GameStateBattle 切换到 GameStateShop 或 LevelUp 时敌人异常残留的问题。涉及 3 个文件,共 +43 / -8 行,全部为代码改动。
具体改动分析
1. EnemyManagerComponent.cs (+40 / -0)
- 新增了一个
_isStopped布尔字段,用于标记当前是否应该阻止新敌人出现。 - 订阅了
ShowEntitySuccessEventArgs事件(在Awake或Start中),并在事件处理中判断_isStopped状态:如果已停止,则立即隐藏刚生成的敌人实体。 - 在对应的生命周期中注册和注销事件订阅(即
Subscribe/Unsubscribe)。
为什么这么做?
在状态切换时,如果直接调用清除逻辑,可能会遇到异步操作的问题。敌人实体的生成请求在发送给游戏框架(Game Framework)后是无法取消的——即使使用 CancellationToken 取消后续流程,但 ShowEntity 请求已经发出,框架内部仍会执行并触发 ShowEntitySuccess 事件。因此,用 _isStopped 标志位配合事件订阅,可以在敌人出现时立刻将其隐藏,而不是试图取消已发出的请求。这是一种“事后拦截”而非“事前阻止”的策略,虽然有些取巧,但在现有框架约束下是相对安全的做法。
2. EnemyRegistry.cs (+2 / -7)
- 移除了
using UnityEngine;引用。 - 删除了对
_enemyById.ContainsKey(entityId)的提前检查逻辑,简化了代码。
这部分改动属于清理工作,可能与本次修复无关,但顺手整理了冗余代码。
3. Launcher.unity (+1 / -1)
- 将某个整数值从
0改为1。
背景说明:这个改动与本次敌人残留修复无关。之前为了打包测试,将游戏框架的资源模式从编辑器模式临时改成了 AssetBundle 模式,现在只是改回来。属于配置回滚,不影响游戏逻辑。
决策反思
-
为什么选择事件订阅 + 标志位,而不是直接清除所有敌人?
如上所述,ShowEntity请求一旦发出就无法取消。直接清除已生成的敌人虽然能解决视觉残留,但可能会破坏框架内部的状态一致性(例如实体对象池引用未清理导致后续报错)。而利用事件在敌人出现瞬间隐藏,既保证了不再产生新敌人,又避免了破坏框架已有的生命周期管理。 -
_isStopped的复位时机
在 commit message 中已经明确:_isStopped在Init和Reset方法中进行切换——这两个方法对应GameStateBattle的初始化与重置。所以当战斗再次开始时,标志位会被正确清空,不会影响正常战斗流程。 -
潜在的竞态条件
如果玩家在多个状态之间快速切换(例如反复进商店再回战斗),_isStopped的切换与事件订阅/注销是同步的,且事件处理本身在主线程执行,因此不会出现竞态条件。唯一需要关注的是事件订阅是否在适当时机被注销——当前代码在OnDestroy或Dispose中注销,生命周期管理是明确的。
后续关注点
- 验证其他状态切换(如
GameStateShop → GameStateBattle)是否也存在类似残留问题。当前方案只覆盖了离开战斗的场景。 - 在战斗重新开始或场景重载时,确认
_isStopped能被正确复位(目前看来由Init/Reset保证,但仍需测试边界情况)。 - 检查事件订阅/注销的配对,避免内存泄漏——尤其当玩家多次进出战斗时,确保不会重复订阅。
- 移除的
using UnityEngine不会影响其他未记录的MonoBehaviour调用(当前EnemyRegistry不再直接依赖 Unity 类型,移除是安全的)。
注:本文由模型
deepseek-v4-flash生成(草稿与终稿同模型)。