using System; using System.Collections.Generic; using System.Threading; using UnityEngine; public class WaveManager : MonoBehaviour { [Header("Waves")] [SerializeField] private List _waves = new(); [SerializeField] private float _intermissionDuration = 2f; [SerializeField] private bool _startOnAwake = true; [Header("Spawn Position")] [SerializeField] private Transform[] _spawnPoints; [SerializeField] private float _spawnRadius = 0f; [SerializeField] private Transform _enemyParent; [Header("On Defeat")] [SerializeField] private bool _destroyAliveEnemiesOnDefeat = true; public event Action OnWaveStart; public event Action OnWaveCleared; public event Action OnAllWavesCleared; public event Action OnDefeat; public int CurrentWaveIndex { get; private set; } public int TotalWaveCount => _waves != null ? _waves.Count : 0; public WaveData CurrentWave => _waves != null && CurrentWaveIndex >= 0 && CurrentWaveIndex < _waves.Count ? _waves[CurrentWaveIndex] : null; public float TimeRemaining { get; private set; } public int AliveCount => _aliveEnemies.Count; public int RemainingToSpawn { get; private set; } public bool IsRunning { get; private set; } public bool IsDefeated { get; private set; } public bool IsVictory { get; private set; } private readonly List _aliveEnemies = new(); private CancellationTokenSource _waveCts; private void Start() { if (_startOnAwake) StartWaves(); } private void OnDestroy() { _waveCts?.Cancel(); _waveCts?.Dispose(); } public void StartWaves() { if (_waves == null || _waves.Count == 0) return; RunAllWaves(); } public void StopWaves() { _waveCts?.Cancel(); IsRunning = false; } private async void RunAllWaves() { _waveCts?.Cancel(); _waveCts?.Dispose(); _waveCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken); CancellationToken token = _waveCts.Token; IsDefeated = false; IsVictory = false; try { for (CurrentWaveIndex = 0; CurrentWaveIndex < _waves.Count; CurrentWaveIndex++) { WaveData wave = _waves[CurrentWaveIndex]; if (wave == null) continue; if (wave.StartDelay > 0f) await Awaitable.WaitForSecondsAsync(wave.StartDelay, token); OnWaveStart?.Invoke(CurrentWaveIndex, wave); bool cleared = await RunWave(wave, token); if (!cleared) { IsDefeated = true; OnDefeat?.Invoke(CurrentWaveIndex); if (_destroyAliveEnemiesOnDefeat) DestroyAliveEnemies(); return; } OnWaveCleared?.Invoke(CurrentWaveIndex); if (CurrentWaveIndex < _waves.Count - 1 && _intermissionDuration > 0f) await Awaitable.WaitForSecondsAsync(_intermissionDuration, token); } IsVictory = true; OnAllWavesCleared?.Invoke(); } catch (OperationCanceledException) { } } private async Awaitable RunWave(WaveData wave, CancellationToken token) { IsRunning = true; TimeRemaining = wave.TimeLimit; _aliveEnemies.Clear(); int totalToSpawn = 0; foreach (var entry in wave.Spawns) if (entry != null) totalToSpawn += Mathf.Max(entry.Count, 0); RemainingToSpawn = totalToSpawn; // 적 스폰은 백그라운드로 흘려보내고, 본 루프는 시간/클리어만 체크. SpawnWaveEnemiesAsync(wave, token); while (true) { token.ThrowIfCancellationRequested(); _aliveEnemies.RemoveAll(e => e == null); if (RemainingToSpawn == 0 && _aliveEnemies.Count == 0) { IsRunning = false; return true; } TimeRemaining -= Time.deltaTime; if (TimeRemaining <= 0f) { IsRunning = false; return false; } await Awaitable.NextFrameAsync(token); } } private async void SpawnWaveEnemiesAsync(WaveData wave, CancellationToken token) { try { foreach (var entry in wave.Spawns) { if (entry == null || entry.EnemyPrefab == null) { RemainingToSpawn -= entry != null ? entry.Count : 0; continue; } for (int i = 0; i < entry.Count; i++) { token.ThrowIfCancellationRequested(); SpawnEnemy(entry.EnemyPrefab); RemainingToSpawn--; if (wave.SpawnInterval > 0f) await Awaitable.WaitForSecondsAsync(wave.SpawnInterval, token); } } } catch (OperationCanceledException) { } } private void SpawnEnemy(Enemy prefab) { Vector3 position = GetSpawnPosition(); Enemy enemy = Instantiate(prefab, position, Quaternion.identity, _enemyParent); _aliveEnemies.Add(enemy); } private Vector3 GetSpawnPosition() { Vector3 basePos; if (_spawnPoints != null && _spawnPoints.Length > 0) { Transform pick = _spawnPoints[UnityEngine.Random.Range(0, _spawnPoints.Length)]; basePos = pick != null ? pick.position : transform.position; } else { basePos = transform.position; } if (_spawnRadius > 0f) { Vector2 offset = UnityEngine.Random.insideUnitCircle * _spawnRadius; basePos.x += offset.x; basePos.y += offset.y; } return basePos; } private void DestroyAliveEnemies() { for (int i = _aliveEnemies.Count - 1; i >= 0; i--) { if (_aliveEnemies[i] != null) Destroy(_aliveEnemies[i].gameObject); } _aliveEnemies.Clear(); } private void OnGUI() { GUIStyle style = new GUIStyle(GUI.skin.box) { fontSize = 22, alignment = TextAnchor.MiddleCenter, richText = true, normal = { textColor = Color.white } }; string text; if (IsVictory) { text = "VICTORY"; } else if (IsDefeated) { text = $"DEFEAT\nWave {CurrentWaveIndex + 1} timed out"; } else if (IsRunning && CurrentWave != null) { string name = string.IsNullOrEmpty(CurrentWave.WaveName) ? $"Wave {CurrentWaveIndex + 1}/{TotalWaveCount}" : CurrentWave.WaveName; text = $"{name}\nTime: {TimeRemaining:F1}s\nEnemies: {AliveCount} (+{RemainingToSpawn} 대기)"; } else { return; } GUI.Box(new Rect(Screen.width / 2f - 180f, 20f, 360f, 110f), text, style); } private void OnDrawGizmosSelected() { Color pointColor = Color.cyan; Color radiusColor = new Color(0f, 1f, 1f, 0.25f); if (_spawnPoints != null && _spawnPoints.Length > 0) { foreach (var p in _spawnPoints) { if (p == null) continue; Gizmos.color = pointColor; Gizmos.DrawWireSphere(p.position, 0.3f); if (_spawnRadius > 0f) { Gizmos.color = radiusColor; Gizmos.DrawWireSphere(p.position, _spawnRadius); } } } else { Gizmos.color = pointColor; Gizmos.DrawWireSphere(transform.position, 0.3f); if (_spawnRadius > 0f) { Gizmos.color = radiusColor; Gizmos.DrawWireSphere(transform.position, _spawnRadius); } } } }