517 lines
16 KiB
C#
517 lines
16 KiB
C#
using System.Collections.Generic;
|
|
using GameFramework.DataTable;
|
|
using GameFramework.Event;
|
|
using GeometryTD.DataTable;
|
|
using GeometryTD.Definition;
|
|
using GeometryTD.Entity;
|
|
using GeometryTD.Entity.EntityData;
|
|
using UnityEngine;
|
|
using UnityGameFramework.Runtime;
|
|
|
|
namespace GeometryTD.CustomComponent
|
|
{
|
|
public class EnemyManager
|
|
{
|
|
private sealed class SpawnEntryRuntime
|
|
{
|
|
public DRLevelSpawnEntry Entry;
|
|
public bool Completed;
|
|
public float NextTriggerTime;
|
|
public float EndTime;
|
|
public int RemainingCount;
|
|
}
|
|
|
|
private const int DefaultEnemyConfigId = 1;
|
|
private const float MinStreamInterval = 0.05f;
|
|
private const float MinBurstGap = 0.01f;
|
|
|
|
private readonly List<Spawner> _spawners = new List<Spawner>();
|
|
private readonly Dictionary<int, Spawner> _spawnerByOrder = new Dictionary<int, Spawner>();
|
|
private readonly List<Vector3> _pathBuffer = new List<Vector3>();
|
|
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new List<SpawnEntryRuntime>();
|
|
private readonly HashSet<int> _trackedEnemyEntityIds = new HashSet<int>();
|
|
private readonly List<int> _trackedEnemyIdBuffer = new List<int>();
|
|
|
|
private CombatScheduler _combatScheduler;
|
|
private EntityComponent _entity;
|
|
private IDataTable<DREnemy> _drEnemy;
|
|
|
|
private int _spawnEnemyMaxCount = 5000;
|
|
private int _currentEnemyCount;
|
|
private int _nextSpawnerIndex;
|
|
private int _currentMapEntityId;
|
|
private bool _initialized;
|
|
private bool _enemyConfigMissingLogged;
|
|
|
|
private float _phaseElapsed;
|
|
private bool _isPhaseRunning;
|
|
|
|
public int AliveEnemyCount => _currentEnemyCount;
|
|
public bool IsPhaseSpawnCompleted { get; private set; } = true;
|
|
public bool IsPhaseRunning => _isPhaseRunning;
|
|
|
|
public void OnInit(CombatScheduler combatScheduler)
|
|
{
|
|
_combatScheduler = combatScheduler;
|
|
if (_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_entity = GameEntry.Entity;
|
|
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
|
|
_currentEnemyCount = 0;
|
|
_nextSpawnerIndex = 0;
|
|
_currentMapEntityId = 0;
|
|
_enemyConfigMissingLogged = false;
|
|
_spawners.Clear();
|
|
_spawnerByOrder.Clear();
|
|
_pathBuffer.Clear();
|
|
_spawnRuntimes.Clear();
|
|
_trackedEnemyEntityIds.Clear();
|
|
_trackedEnemyIdBuffer.Clear();
|
|
_phaseElapsed = 0f;
|
|
_isPhaseRunning = false;
|
|
IsPhaseSpawnCompleted = true;
|
|
|
|
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
|
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
|
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
|
_initialized = true;
|
|
}
|
|
|
|
public void BeginPhase(DRLevelPhase phase, IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
|
|
{
|
|
if (!_initialized || _combatScheduler == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ = phase;
|
|
EndPhase();
|
|
|
|
_phaseElapsed = 0f;
|
|
_isPhaseRunning = true;
|
|
IsPhaseSpawnCompleted = false;
|
|
RefreshSpawnerCache(true);
|
|
|
|
if (spawnEntries != null)
|
|
{
|
|
for (int i = 0; i < spawnEntries.Count; i++)
|
|
{
|
|
SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]);
|
|
if (runtime != null)
|
|
{
|
|
_spawnRuntimes.Add(runtime);
|
|
}
|
|
}
|
|
}
|
|
|
|
IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0;
|
|
}
|
|
|
|
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
|
{
|
|
if (!_initialized || _combatScheduler == null || !_isPhaseRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RefreshSpawnerCache(false);
|
|
_phaseElapsed += elapseSeconds;
|
|
UpdateSpawnRuntimes();
|
|
}
|
|
|
|
public void EndPhase()
|
|
{
|
|
_isPhaseRunning = false;
|
|
_phaseElapsed = 0f;
|
|
_spawnRuntimes.Clear();
|
|
IsPhaseSpawnCompleted = true;
|
|
}
|
|
|
|
public void OnDestroy()
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
_combatScheduler = null;
|
|
return;
|
|
}
|
|
|
|
CleanupTrackedEnemies();
|
|
EndPhase();
|
|
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
|
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
|
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
|
|
|
_spawners.Clear();
|
|
_spawnerByOrder.Clear();
|
|
_pathBuffer.Clear();
|
|
_trackedEnemyEntityIds.Clear();
|
|
_trackedEnemyIdBuffer.Clear();
|
|
_currentEnemyCount = 0;
|
|
_currentMapEntityId = 0;
|
|
_nextSpawnerIndex = 0;
|
|
_combatScheduler = null;
|
|
_initialized = false;
|
|
}
|
|
|
|
public void CleanupTrackedEnemies()
|
|
{
|
|
if (_trackedEnemyEntityIds.Count <= 0)
|
|
{
|
|
_currentEnemyCount = 0;
|
|
return;
|
|
}
|
|
|
|
_trackedEnemyIdBuffer.Clear();
|
|
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
|
|
{
|
|
_trackedEnemyIdBuffer.Add(trackedEnemyEntityId);
|
|
}
|
|
|
|
_trackedEnemyEntityIds.Clear();
|
|
_currentEnemyCount = 0;
|
|
|
|
if (_entity == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++)
|
|
{
|
|
int trackedEnemyEntityId = _trackedEnemyIdBuffer[i];
|
|
if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId))
|
|
{
|
|
_entity.HideEntity(trackedEnemyEntityId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
|
|
{
|
|
if (entry == null || entry.EntryType == EntryType.None)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
SpawnEntryRuntime runtime = new SpawnEntryRuntime
|
|
{
|
|
Entry = entry,
|
|
Completed = false,
|
|
NextTriggerTime = Mathf.Max(0f, entry.StartTime),
|
|
EndTime = Mathf.Max(0f, entry.StartTime),
|
|
RemainingCount = Mathf.Max(0, entry.Count)
|
|
};
|
|
|
|
switch (entry.EntryType)
|
|
{
|
|
case EntryType.Stream:
|
|
{
|
|
float duration = Mathf.Max(0f, entry.Duration);
|
|
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
|
|
runtime.Completed = entry.Count <= 0;
|
|
return runtime;
|
|
}
|
|
case EntryType.Burst:
|
|
case EntryType.Boss:
|
|
runtime.Completed = runtime.RemainingCount <= 0;
|
|
return runtime;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void UpdateSpawnRuntimes()
|
|
{
|
|
bool allCompleted = true;
|
|
for (int i = 0; i < _spawnRuntimes.Count; i++)
|
|
{
|
|
SpawnEntryRuntime runtime = _spawnRuntimes[i];
|
|
if (runtime.Completed)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
switch (runtime.Entry.EntryType)
|
|
{
|
|
case EntryType.Stream:
|
|
ProcessStreamRuntime(runtime);
|
|
break;
|
|
case EntryType.Burst:
|
|
case EntryType.Boss:
|
|
ProcessBurstRuntime(runtime);
|
|
break;
|
|
default:
|
|
runtime.Completed = true;
|
|
break;
|
|
}
|
|
|
|
if (!runtime.Completed)
|
|
{
|
|
allCompleted = false;
|
|
}
|
|
}
|
|
|
|
IsPhaseSpawnCompleted = allCompleted;
|
|
}
|
|
|
|
private void ProcessStreamRuntime(SpawnEntryRuntime runtime)
|
|
{
|
|
if (_phaseElapsed < runtime.NextTriggerTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int countPerWave = Mathf.Max(0, runtime.Entry.Count);
|
|
if (countPerWave <= 0)
|
|
{
|
|
runtime.Completed = true;
|
|
return;
|
|
}
|
|
|
|
float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval;
|
|
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime)
|
|
{
|
|
SpawnEnemies(runtime.Entry, countPerWave);
|
|
runtime.NextTriggerTime += interval;
|
|
}
|
|
|
|
if (runtime.NextTriggerTime > runtime.EndTime)
|
|
{
|
|
runtime.Completed = true;
|
|
}
|
|
}
|
|
|
|
private void ProcessBurstRuntime(SpawnEntryRuntime runtime)
|
|
{
|
|
if (_phaseElapsed < runtime.NextTriggerTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (runtime.RemainingCount <= 0)
|
|
{
|
|
runtime.Completed = true;
|
|
return;
|
|
}
|
|
|
|
float gap = runtime.Entry.Gap;
|
|
if (gap <= 0f)
|
|
{
|
|
SpawnEnemies(runtime.Entry, runtime.RemainingCount);
|
|
runtime.RemainingCount = 0;
|
|
runtime.Completed = true;
|
|
return;
|
|
}
|
|
|
|
gap = Mathf.Max(gap, MinBurstGap);
|
|
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
|
|
{
|
|
SpawnEnemies(runtime.Entry, 1);
|
|
runtime.RemainingCount--;
|
|
runtime.NextTriggerTime += gap;
|
|
}
|
|
|
|
runtime.Completed = runtime.RemainingCount <= 0;
|
|
}
|
|
|
|
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
|
|
{
|
|
if (spawnCount <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Spawner spawner = ResolveSpawner(entry.SpawnPointId);
|
|
if (spawner == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null;
|
|
if (currentMap == null ||
|
|
!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) ||
|
|
_pathBuffer.Count <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DREnemy enemyConfig = GetEnemyConfig(entry.EnemyId);
|
|
if (enemyConfig == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < spawnCount; i++)
|
|
{
|
|
if (_currentEnemyCount >= _spawnEnemyMaxCount)
|
|
{
|
|
break;
|
|
}
|
|
|
|
int enemyEntityId = _entity.GenerateSerialId();
|
|
_trackedEnemyEntityIds.Add(enemyEntityId);
|
|
EnemyData enemyData = new EnemyData(
|
|
enemyEntityId,
|
|
enemyConfig.EntityId,
|
|
_pathBuffer[0],
|
|
enemyConfig.BaseHp,
|
|
enemyConfig.Speed,
|
|
_pathBuffer);
|
|
_entity.ShowEnemy(enemyData);
|
|
}
|
|
}
|
|
|
|
private DREnemy GetEnemyConfig(int enemyId)
|
|
{
|
|
if (_drEnemy == null)
|
|
{
|
|
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
|
|
if (_drEnemy == null)
|
|
{
|
|
if (!_enemyConfigMissingLogged)
|
|
{
|
|
Log.Warning("EnemyManagerComponent can not find DREnemy data table.");
|
|
_enemyConfigMissingLogged = true;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (enemyId > 0)
|
|
{
|
|
DREnemy targetConfig = _drEnemy.GetDataRow(enemyId);
|
|
if (targetConfig != null)
|
|
{
|
|
return targetConfig;
|
|
}
|
|
}
|
|
|
|
DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId);
|
|
if (defaultConfig != null)
|
|
{
|
|
return defaultConfig;
|
|
}
|
|
|
|
DREnemy[] allConfigs = _drEnemy.GetAllDataRows();
|
|
if (allConfigs.Length > 0)
|
|
{
|
|
return allConfigs[0];
|
|
}
|
|
|
|
if (!_enemyConfigMissingLogged)
|
|
{
|
|
Log.Warning("EnemyManagerComponent found no enemy configs.");
|
|
_enemyConfigMissingLogged = true;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private Spawner ResolveSpawner(int spawnPointId)
|
|
{
|
|
if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner))
|
|
{
|
|
return mappedSpawner;
|
|
}
|
|
|
|
if (_spawners.Count <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count];
|
|
_nextSpawnerIndex++;
|
|
return fallbackSpawner;
|
|
}
|
|
|
|
private void RefreshSpawnerCache(bool force)
|
|
{
|
|
MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null;
|
|
if (currentMap == null)
|
|
{
|
|
_spawners.Clear();
|
|
_spawnerByOrder.Clear();
|
|
_currentMapEntityId = 0;
|
|
_nextSpawnerIndex = 0;
|
|
return;
|
|
}
|
|
|
|
if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_spawners.Clear();
|
|
_spawnerByOrder.Clear();
|
|
_nextSpawnerIndex = 0;
|
|
_currentMapEntityId = currentMap.Id;
|
|
|
|
Spawner[] mapSpawners = currentMap.Spawners;
|
|
for (int i = 0; i < mapSpawners.Length; i++)
|
|
{
|
|
Spawner spawner = mapSpawners[i];
|
|
if (spawner == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!currentMap.TryGetDefaultPathCells(spawner, out _))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_spawners.Add(spawner);
|
|
if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder))
|
|
{
|
|
_spawnerByOrder[spawner.SpawnOrder] = spawner;
|
|
}
|
|
}
|
|
|
|
_spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder));
|
|
}
|
|
|
|
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
|
{
|
|
if (!(e is ShowEntitySuccessEventArgs ne)) return;
|
|
|
|
if (ne.EntityLogicType == typeof(EnemyEntity) &&
|
|
_trackedEnemyEntityIds.Contains(ne.Entity.Id))
|
|
{
|
|
_currentEnemyCount++;
|
|
}
|
|
}
|
|
|
|
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
|
{
|
|
if (!(e is ShowEntityFailureEventArgs ne))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ne.EntityLogicType != typeof(EnemyEntity))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_trackedEnemyEntityIds.Remove(ne.EntityId);
|
|
}
|
|
|
|
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
|
{
|
|
if (!(e is HideEntityCompleteEventArgs ne))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_trackedEnemyEntityIds.Remove(ne.EntityId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
|
|
}
|
|
}
|
|
} |