From 4250e48e229c5702f9bc05ce68c58e7471cad50f Mon Sep 17 00:00:00 2001 From: basil <2428390463@qq.com> Date: Fri, 26 Jun 2026 13:53:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20GameStateBattle=20->=20Gam?= =?UTF-8?q?eStateShop/LevelUp=20=E6=97=B6=E7=9A=84=E6=95=8C=E4=BA=BA?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E6=AE=8B=E7=95=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用一个 _isStopped 布尔门替代 id 追踪: - OnReset 开头置 _isStopped = true - OnInit 置 _isStopped = false - OnShowEntitySuccess:只要 _isStopped 为真且是 Enemy 组,立即 HideEntity - SpawnEnemyAsync await 恢复后也补一道 _isStopped 检查(若 await 拿到了 enemy 且已停战,直接 hide 不注册)——双保险 为什么这样对:停战窗口(Shop/LevelUp)期间本就不该有任何敌人,所以"停战期间出生的敌人一律 hide"在语义上完全正确,且不受 await 同步抽空、id 复用、加载时序任何影响。 --- .../EnemyManager/EnemyManagerComponent.cs | 40 +++++++++++++++++++ .../EnemyManager/EnemyRegistry.cs | 9 +---- Assets/Launcher.unity | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) 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