修复 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:
SepComet 2026-06-26 13:53:36 +08:00
parent c111170bb0
commit 4250e48e22
3 changed files with 43 additions and 8 deletions

View File

@ -33,6 +33,11 @@ namespace SepCore.EnemyManager
private Transform _player;
private ISpawnPositionStrategy _spawnPositionStrategy;
private CancellationTokenSource _spawnCts;
// 停战标志。OnReset 置 trueOnInit 置 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() 会同步恢复在途的 SpawnEnemyAsyncenemy 拿不到引用,
// 无法自行 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
}
}

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using SepCore.Entity;
using UnityEngine;
namespace SepCore.EnemyManager
{
@ -24,12 +23,8 @@ namespace SepCore.EnemyManager
public void Remove(int entityId)
{
if (!_enemyById.ContainsKey(entityId))
{
Debug.LogWarning($"EnemyRegistry: Attempt to remove non-existent entity id={entityId}");
return;
}
// 移除是幂等的。ClearEnemies 会同步清空 registry而 HideEntity 的完成回调
// 是异步的,晚几帧才到达,此时该 id 早已不在集合中,属正常情况,静默忽略即可。
_enemyById.Remove(entityId);
}

View File

@ -738,7 +738,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EditorResourceMode
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_JsonHelperTypeName