using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class WaveSpawner : MonoBehaviour { [Serializable] public class Wave { public GameObject enemyPrefab; [Min(1)] public int count = 5; [Tooltip("Enemies per second. 1 = spawn every 1s, 5 = every 0.2s")] [Min(0.0001f)] public float spawnRate = 1f; } [Header("Setup")] public Wave[] waves; public Transform spawnPoint; [Tooltip("Optional parent to keep hierarchy clean")] public Transform containerParent; [Min(0f)] public float timeBetweenWaves = 3f; public int CurrentWaveIndex { get; private set; } = -1; // 0-based, -1 = none yet public bool IsSpawning { get; private set; } public bool AllWavesCompleted { get; private set; } public int CurrentAlive => _alive.Count; // EVENTS (subscribe from Level1 or any other listener) public event Action OnWaveStarted; // args: waveIndex, waveDef public event Action OnWaveCompleted; // args: waveIndex public event Action OnAllWavesCompleted; public event Action OnEnemySpawned; // args: instance public event Action OnAliveCountChanged; // args: currentAlive // Internals private readonly List _alive = new List(); private Coroutine _routine; public int TotalWaves => waves != null ? waves.Length : 0; public void StartSpawning() { if (IsSpawning || AllWavesCompleted) return; if (spawnPoint == null || waves == null || waves.Length == 0) { Debug.LogWarning("[WaveSpawner] Missing spawnPoint or waves."); return; } _routine = StartCoroutine(SpawnRoutine()); } public void StopSpawning() { if (_routine != null) { StopCoroutine(_routine); _routine = null; } IsSpawning = false; } private IEnumerator SpawnRoutine() { IsSpawning = true; AllWavesCompleted = false; for (int w = 0; w < waves.Length; w++) { CurrentWaveIndex = w; var wave = waves[w]; OnWaveStarted?.Invoke(w, wave); // spawn phase for (int i = 0; i < wave.count; i++) { SpawnEnemyOnce(wave.enemyPrefab); yield return new WaitForSeconds(1f / wave.spawnRate); } // wait until wave cleared (no alive remaining from all spawns so far) yield return StartCoroutine(WaitUntilCleared()); OnWaveCompleted?.Invoke(w); if (w < waves.Length - 1 && timeBetweenWaves > 0f) yield return new WaitForSeconds(timeBetweenWaves); } IsSpawning = false; AllWavesCompleted = true; OnAllWavesCompleted?.Invoke(); } private IEnumerator WaitUntilCleared() { var wait = new WaitForSeconds(0.25f); while (true) { CompactAliveList(); if (_alive.Count == 0) break; yield return wait; } } private void SpawnEnemyOnce(GameObject prefab) { if (prefab == null) return; var go = Instantiate(prefab, spawnPoint.position, spawnPoint.rotation, containerParent); // Attach relay so we detect destruction/disable cleanly var relay = go.AddComponent<_EnemyLifeRelay>(); relay.Init(this); _alive.Add(go); OnEnemySpawned?.Invoke(go); OnAliveCountChanged?.Invoke(_alive.Count); } private void CompactAliveList() { for (int i = _alive.Count - 1; i >= 0; i--) { var go = _alive[i]; if (go == null) { _alive.RemoveAt(i); continue; } // Consider not-alive when: // 1) Destroyed (handled by null above), OR // 2) Inactive in hierarchy, OR // 3) Has EnemyBase and IsDead() is true. bool alive = go.activeInHierarchy; if (alive) { var eb = go.GetComponent(); if (eb != null && eb.IsDead()) alive = false; } if (!alive) { _alive.RemoveAt(i); } } } // Called by relay on death/destroy/disable private void NotifyGone(GameObject go) { if (go == null) return; int idx = _alive.IndexOf(go); if (idx >= 0) { _alive.RemoveAt(idx); OnAliveCountChanged?.Invoke(_alive.Count); } } // Helper component placed on each spawned enemy private class _EnemyLifeRelay : MonoBehaviour { private WaveSpawner _owner; private EnemyBase _enemyBase; private bool _notified; public void Init(WaveSpawner owner) { _owner = owner; _enemyBase = GetComponent(); // If your EnemyBase has an OnDeath event, you can hook it here: // _enemyBase.OnDeath += HandleGone; } private void OnDisable() => HandleGone(); private void OnDestroy() => HandleGone(); private void HandleGone() { if (_notified || _owner == null) return; // If EnemyBase exists and isn't actually dead yet (e.g., pooled), skip. // We'll rely on spawner's periodic compaction in that case. if (_enemyBase != null && !_enemyBase.IsDead() && gameObject.activeInHierarchy) return; _owner.NotifyGone(gameObject); _notified = true; } } }