193 lines
5.6 KiB
C#

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<int, Wave> OnWaveStarted; // args: waveIndex, waveDef
public event Action<int> OnWaveCompleted; // args: waveIndex
public event Action OnAllWavesCompleted;
public event Action<GameObject> OnEnemySpawned; // args: instance
public event Action<int> OnAliveCountChanged; // args: currentAlive
// Internals
private readonly List<GameObject> _alive = new List<GameObject>();
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<EnemyBase>();
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<EnemyBase>();
// 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;
}
}
}