refactor(EnemyManager): 性能优化与并发安全修复
- EnemyRegistry: 移除List冗余存储, Register/Remove从O(n)→O(1) - 增加PruneInvalidEntries显式清理接口, 消除TryGet副作用 - Remove增加不存在告警, 防重复减成负数 - 增加CTS取消飞行中的异步生成, 关卡切换时取消+重建 - ClearEnemies先快照再遍历, 防Hide回调修改集合抛异常 - entityId去掉取模复用, 直接自增保证唯一 - Enemy EntityGroup调优: Capacity→10000, ReleaseInterval→30, ExpireTime→120
This commit is contained in:
parent
49c300a10e
commit
000984c676
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using SepCore.DataTable;
|
using SepCore.DataTable;
|
||||||
using SepCore.Definition;
|
using SepCore.Definition;
|
||||||
|
|
@ -19,7 +20,7 @@ namespace SepCore.EnemyManager
|
||||||
private EnemyRegistry _enemyRegistry;
|
private EnemyRegistry _enemyRegistry;
|
||||||
private EnemySpawnScheduler _spawnScheduler;
|
private EnemySpawnScheduler _spawnScheduler;
|
||||||
|
|
||||||
public List<EntityBase> Enemies => _enemyRegistry?.Enemies;
|
public IReadOnlyCollection<EntityBase> Enemies => _enemyRegistry?.Enemies;
|
||||||
|
|
||||||
[SerializeField] private int _spawnEnemyMaxCount = 5000;
|
[SerializeField] private int _spawnEnemyMaxCount = 5000;
|
||||||
[SerializeField] private int _spawnDistanceFromPlayer = 20;
|
[SerializeField] private int _spawnDistanceFromPlayer = 20;
|
||||||
|
|
@ -31,6 +32,7 @@ namespace SepCore.EnemyManager
|
||||||
|
|
||||||
private Transform _player;
|
private Transform _player;
|
||||||
private ISpawnPositionStrategy _spawnPositionStrategy;
|
private ISpawnPositionStrategy _spawnPositionStrategy;
|
||||||
|
private CancellationTokenSource _spawnCts;
|
||||||
|
|
||||||
public float SpawnRateScale => _spawnScheduler?.SpawnRateScale ?? 1f;
|
public float SpawnRateScale => _spawnScheduler?.SpawnRateScale ?? 1f;
|
||||||
public float BattleDuration => _duration;
|
public float BattleDuration => _duration;
|
||||||
|
|
@ -44,6 +46,7 @@ namespace SepCore.EnemyManager
|
||||||
_entity = GameEntry.Entity;
|
_entity = GameEntry.Entity;
|
||||||
_enemyRegistry = new EnemyRegistry();
|
_enemyRegistry = new EnemyRegistry();
|
||||||
_spawnScheduler = new EnemySpawnScheduler();
|
_spawnScheduler = new EnemySpawnScheduler();
|
||||||
|
_spawnCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
|
||||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +55,8 @@ namespace SepCore.EnemyManager
|
||||||
{
|
{
|
||||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||||
|
|
||||||
|
_spawnCts?.Dispose();
|
||||||
|
_spawnCts = null;
|
||||||
_enemyRegistry = null;
|
_enemyRegistry = null;
|
||||||
_spawnScheduler = null;
|
_spawnScheduler = null;
|
||||||
_entity = null;
|
_entity = null;
|
||||||
|
|
@ -73,6 +78,7 @@ namespace SepCore.EnemyManager
|
||||||
|
|
||||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
{
|
{
|
||||||
|
_enemyRegistry.PruneInvalidEntries();
|
||||||
var spawnRequests = _spawnScheduler.Tick(elapseSeconds);
|
var spawnRequests = _spawnScheduler.Tick(elapseSeconds);
|
||||||
foreach (var request in spawnRequests)
|
foreach (var request in spawnRequests)
|
||||||
{
|
{
|
||||||
|
|
@ -85,6 +91,10 @@ namespace SepCore.EnemyManager
|
||||||
|
|
||||||
public void OnReset()
|
public void OnReset()
|
||||||
{
|
{
|
||||||
|
_spawnCts.Cancel();
|
||||||
|
_spawnCts.Dispose();
|
||||||
|
_spawnCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
|
||||||
_spawnScheduler.Reset();
|
_spawnScheduler.Reset();
|
||||||
ClearEnemies();
|
ClearEnemies();
|
||||||
_currentSpawnEnemyId = 0;
|
_currentSpawnEnemyId = 0;
|
||||||
|
|
@ -100,14 +110,20 @@ namespace SepCore.EnemyManager
|
||||||
if (_player == null) return;
|
if (_player == null) return;
|
||||||
|
|
||||||
if (_enemyRegistry.Count >= _spawnEnemyMaxCount) return;
|
if (_enemyRegistry.Count >= _spawnEnemyMaxCount) return;
|
||||||
int entityPoolId = _currentSpawnEnemyId % _spawnEnemyMaxCount;
|
int entityId = _currentSpawnEnemyId++;
|
||||||
var enemyData = EntityDataFactory.Create(entityPoolId, enemyType, _currentLevel);
|
var enemyData = EntityDataFactory.Create(entityId, enemyType, _currentLevel);
|
||||||
enemyData.Position = _spawnPositionStrategy.GetSpawnPosition(_player);
|
enemyData.Position = _spawnPositionStrategy.GetSpawnPosition(_player);
|
||||||
_currentSpawnEnemyId++;
|
|
||||||
|
|
||||||
EnemyBase enemy = await _entity.ShowEnemyAsync(enemyData);
|
var ct = _spawnCts.Token;
|
||||||
if (enemy == null)
|
var (isCanceled, enemy) =
|
||||||
|
await _entity.ShowEnemyAsync(enemyData, cancellationToken: ct).SuppressCancellationThrow();
|
||||||
|
if (isCanceled || ct.IsCancellationRequested || enemy == null || !enemy.Available)
|
||||||
{
|
{
|
||||||
|
if (enemy != null && enemy.Available)
|
||||||
|
{
|
||||||
|
_entity.HideEntity(enemy);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,12 +131,14 @@ namespace SepCore.EnemyManager
|
||||||
{
|
{
|
||||||
enemy.SetTarget(_player);
|
enemy.SetTarget(_player);
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemyRegistry.Register(enemy);
|
_enemyRegistry.Register(enemy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearEnemies()
|
private void ClearEnemies()
|
||||||
{
|
{
|
||||||
foreach (var enemy in _enemyRegistry.Enemies)
|
var enemies = new List<EntityBase>(_enemyRegistry.Enemies);
|
||||||
|
foreach (var enemy in enemies)
|
||||||
{
|
{
|
||||||
if (enemy == null || !enemy.Available) continue;
|
if (enemy == null || !enemy.Available) continue;
|
||||||
_entity.HideEntity(enemy);
|
_entity.HideEntity(enemy);
|
||||||
|
|
|
||||||
|
|
@ -6,80 +6,58 @@ namespace SepCore.EnemyManager
|
||||||
{
|
{
|
||||||
public class EnemyRegistry
|
public class EnemyRegistry
|
||||||
{
|
{
|
||||||
private readonly List<EntityBase> _enemies;
|
|
||||||
private readonly Dictionary<int, EntityBase> _enemyById;
|
private readonly Dictionary<int, EntityBase> _enemyById;
|
||||||
|
|
||||||
public int Count { get; private set; }
|
public int Count => _enemyById.Count;
|
||||||
public List<EntityBase> Enemies => _enemies;
|
public IReadOnlyCollection<EntityBase> Enemies => _enemyById.Values;
|
||||||
|
|
||||||
public EnemyRegistry()
|
public EnemyRegistry()
|
||||||
{
|
{
|
||||||
_enemies = new List<EntityBase>();
|
|
||||||
_enemyById = new Dictionary<int, EntityBase>();
|
_enemyById = new Dictionary<int, EntityBase>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Register(EnemyBase enemy)
|
public void Register(EnemyBase enemy)
|
||||||
{
|
{
|
||||||
if (enemy == null) return;
|
if (enemy == null) return;
|
||||||
|
|
||||||
Count++;
|
|
||||||
RemoveFromCache(enemy.Id);
|
|
||||||
_enemies.Add(enemy);
|
|
||||||
_enemyById[enemy.Id] = enemy;
|
_enemyById[enemy.Id] = enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(int entityId)
|
public void Remove(int entityId)
|
||||||
{
|
{
|
||||||
if (Count > 0)
|
if (!_enemyById.ContainsKey(entityId))
|
||||||
{
|
{
|
||||||
Count--;
|
Debug.LogWarning($"EnemyRegistry: Attempt to remove non-existent entity id={entityId}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveFromCache(entityId);
|
_enemyById.Remove(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGet(int entityId, out EntityBase enemy)
|
public bool TryGet(int entityId, out EntityBase enemy)
|
||||||
{
|
{
|
||||||
enemy = null;
|
return _enemyById.TryGetValue(entityId, out enemy);
|
||||||
if (!_enemyById.TryGetValue(entityId, out EntityBase cachedEnemy))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedEnemy == null || !cachedEnemy.Available)
|
public void PruneInvalidEntries()
|
||||||
{
|
{
|
||||||
_enemyById.Remove(entityId);
|
var invalidIds = new List<int>();
|
||||||
return false;
|
foreach (var kvp in _enemyById)
|
||||||
|
{
|
||||||
|
if (kvp.Value == null || !kvp.Value.Available)
|
||||||
|
{
|
||||||
|
invalidIds.Add(kvp.Key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enemy = cachedEnemy;
|
foreach (int id in invalidIds)
|
||||||
return true;
|
{
|
||||||
|
_enemyById.Remove(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_enemies.Clear();
|
|
||||||
_enemyById.Clear();
|
_enemyById.Clear();
|
||||||
Count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveFromCache(int entityId)
|
|
||||||
{
|
|
||||||
_enemyById.Remove(entityId);
|
|
||||||
|
|
||||||
for (int i = _enemies.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
EntityBase cachedEnemy = _enemies[i];
|
|
||||||
if (cachedEnemy == null || cachedEnemy.Id == entityId)
|
|
||||||
{
|
|
||||||
if (cachedEnemy != null)
|
|
||||||
{
|
|
||||||
_enemyById.Remove(cachedEnemy.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_enemies.RemoveAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74801656cc6bf8548bc7f31f1c927157
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -625,7 +625,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[3].m_InstanceCapacity
|
propertyPath: m_EntityGroups.Array.data[3].m_InstanceCapacity
|
||||||
value: 500
|
value: 10000
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[4].m_InstanceCapacity
|
propertyPath: m_EntityGroups.Array.data[4].m_InstanceCapacity
|
||||||
|
|
@ -661,7 +661,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[3].m_InstanceExpireTime
|
propertyPath: m_EntityGroups.Array.data[3].m_InstanceExpireTime
|
||||||
value: 60
|
value: 120
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[4].m_InstanceExpireTime
|
propertyPath: m_EntityGroups.Array.data[4].m_InstanceExpireTime
|
||||||
|
|
@ -697,7 +697,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[3].m_InstanceAutoReleaseInterval
|
propertyPath: m_EntityGroups.Array.data[3].m_InstanceAutoReleaseInterval
|
||||||
value: 0
|
value: 30
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[4].m_InstanceAutoReleaseInterval
|
propertyPath: m_EntityGroups.Array.data[4].m_InstanceAutoReleaseInterval
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue