193 lines
5.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|