diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyManagerComponent.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyManagerComponent.cs index 3db84ad..b2072c4 100644 --- a/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyManagerComponent.cs +++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyManagerComponent.cs @@ -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 } } diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyRegistry.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyRegistry.cs index df23265..f59ed2a 100644 --- a/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyRegistry.cs +++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/EnemyManager/EnemyRegistry.cs @@ -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); } diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity index e793a7a..725f25a 100644 --- a/Assets/Launcher.unity +++ b/Assets/Launcher.unity @@ -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