修复 GameStateBattle -> GameStateShop/LevelUp 时的敌人异常残留问题

用一个 _isStopped 布尔门替代 id 追踪:

- OnReset 开头置 _isStopped = true
- OnInit 置 _isStopped = false
- OnShowEntitySuccess:只要 _isStopped 为真且是 Enemy 组,立即 HideEntity
- SpawnEnemyAsync await 恢复后也补一道 _isStopped 检查(若 await 拿到了 enemy 且已停战,直接 hide 不注册)——双保险

为什么这样对:停战窗口(Shop/LevelUp)期间本就不该有任何敌人,所以"停战期间出生的敌人一律 hide"在语义上完全正确,且不受 await 同步抽空、id 复用、加载时序任何影响。
This commit is contained in:
2026-06-26 13:53:36 +08:00
parent c111170bb0
commit 4250e48e22
3 changed files with 43 additions and 8 deletions
@@ -33,6 +33,11 @@ namespace SepCore.EnemyManager
private Transform _player;
private ISpawnPositionStrategy _spawnPositionStrategy;
private CancellationTokenSource _spawnCts;
// 停战标志。OnReset 置 true,OnInit 置 false。
// ShowEntity 请求一旦发出,取消 await 并不能阻止框架真正创建实体,
// 这些在途敌人会在 OnReset 之后才出生(fire ShowEntitySuccess)。
// 此时 _isStopped 为真,由 OnShowEntitySuccess 在出生瞬间兜底 Hide 掉。
private bool _isStopped;
public float SpawnRateScale => _spawnScheduler?.SpawnRateScale ?? 1f;
public float BattleDuration => _duration;
@@ -49,11 +54,13 @@ namespace SepCore.EnemyManager
_spawnCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
}
private void OnDestroy()
{
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
_spawnCts?.Dispose();
_spawnCts = null;
@@ -65,6 +72,8 @@ namespace SepCore.EnemyManager
public void OnInit(DRLevel level, Player player)
{
_isStopped = false;
_player = player != null ? player.CachedTransform : null;
_spawnPositionStrategy = new RandomCircleSpawnStrategy(_spawnDistanceFromPlayer);
@@ -93,11 +102,16 @@ namespace SepCore.EnemyManager
public void OnReset()
{
// 先置停战标志。Cancel() 会同步恢复在途的 SpawnEnemyAsync(enemy 拿不到引用,
// 无法自行 hide),这些实体会延后出生,由 OnShowEntitySuccess 兜底。
_isStopped = true;
_spawnCts.Cancel();
_spawnCts.Dispose();
_spawnCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
_spawnScheduler.Reset();
ClearEnemies();
_currentSpawnEnemyId = 0;
_currentLevel = 0;
@@ -113,6 +127,7 @@ namespace SepCore.EnemyManager
if (_enemyRegistry.Count >= _spawnEnemyMaxCount) return;
int entityId = _currentSpawnEnemyId++;
var enemyData = EntityDataFactory.Create(entityId, enemyType, _currentLevel);
enemyData.Position = _spawnPositionStrategy.GetSpawnPosition(_player);
@@ -121,6 +136,8 @@ namespace SepCore.EnemyManager
await _entity.ShowEnemyAsync(enemyData, cancellationToken: ct).SuppressCancellationThrow();
if (isCanceled || ct.IsCancellationRequested || enemy == null || !enemy.Available)
{
// 取消通常发生在 OnReset 期间,此时实体往往尚未出生,enemy 为 null 无法 hide。
// 真正的兜底在 OnShowEntitySuccess:停战后出生的敌人会被立即 hide。
if (enemy != null && enemy.Available)
{
_entity.HideEntity(enemy);
@@ -129,6 +146,13 @@ namespace SepCore.EnemyManager
return;
}
// await 恢复时若已停战,说明这是漏网的在途敌人,立即 hide,不注册。
if (_isStopped)
{
_entity.HideEntity(enemy);
return;
}
if (_player != null)
{
enemy.SetTarget(_player);
@@ -172,6 +196,22 @@ namespace SepCore.EnemyManager
}
}
private void OnShowEntitySuccess(object sender, GameEventArgs e)
{
if (e is not ShowEntitySuccessEventArgs ne) return;
if (ne.Entity.EntityGroup?.Name != EnemyGroupName)
{
return;
}
// 停战窗口(Shop/LevelUp)期间不应存在任何敌人。OnReset 之前发出的在途
// ShowEntity 请求会在此刻才真正出生,立即 hide 兜底。
if (_isStopped)
{
_entity.HideEntity(ne.Entity.Id);
}
}
#endregion
}
}