FantasyAsset/Assets/Scripts/WaveSpawner.cs

193 lines
5.6 KiB
C#
Raw Normal View History

2025-08-22 21:40:50 +05:00
using System;
2025-08-22 21:31:17 +05:00
using System.Collections;
using System.Collections.Generic;
2025-08-22 21:40:50 +05:00
using UnityEngine;
2025-08-22 21:31:17 +05:00
public class WaveSpawner : MonoBehaviour
{
2025-08-22 21:40:50 +05:00
[Serializable]
2025-08-22 21:31:17 +05:00
public class Wave
{
2025-08-22 21:40:50 +05:00
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;
2025-08-22 21:31:17 +05:00
}
[Header("Setup")]
public Wave[] waves;
public Transform spawnPoint;
2025-08-22 21:40:50 +05:00
[Tooltip("Optional parent to keep hierarchy clean")]
2025-08-22 21:31:17 +05:00
public Transform containerParent;
2025-08-22 21:40:50 +05:00
[Min(0f)] public float timeBetweenWaves = 3f;
2025-08-22 21:31:17 +05:00
2025-08-22 21:40:50 +05:00
public int CurrentWaveIndex { get; private set; } = -1; // 0-based, -1 = none yet
2025-08-22 21:31:17 +05:00
public bool IsSpawning { get; private set; }
2025-08-22 21:40:50 +05:00
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;
2025-08-22 21:31:17 +05:00
2025-08-22 21:40:50 +05:00
public int TotalWaves => waves != null ? waves.Length : 0;
2025-08-22 21:31:17 +05:00
2025-08-22 21:40:50 +05:00
public void StartSpawning()
2025-08-22 21:31:17 +05:00
{
2025-08-22 21:40:50 +05:00
if (IsSpawning || AllWavesCompleted) return;
if (spawnPoint == null || waves == null || waves.Length == 0)
{
Debug.LogWarning("[WaveSpawner] Missing spawnPoint or waves.");
return;
}
_routine = StartCoroutine(SpawnRoutine());
2025-08-22 21:31:17 +05:00
}
2025-08-22 21:40:50 +05:00
public void StopSpawning()
2025-08-22 21:31:17 +05:00
{
2025-08-22 21:40:50 +05:00
if (_routine != null)
{
StopCoroutine(_routine);
_routine = null;
}
IsSpawning = false;
2025-08-22 21:31:17 +05:00
}
2025-08-22 21:40:50 +05:00
private IEnumerator SpawnRoutine()
2025-08-22 21:31:17 +05:00
{
IsSpawning = true;
2025-08-22 21:40:50 +05:00
AllWavesCompleted = false;
2025-08-22 21:31:17 +05:00
for (int w = 0; w < waves.Length; w++)
{
2025-08-22 21:40:50 +05:00
CurrentWaveIndex = w;
2025-08-22 21:31:17 +05:00
var wave = waves[w];
2025-08-22 21:40:50 +05:00
OnWaveStarted?.Invoke(w, wave);
// spawn phase
2025-08-22 21:31:17 +05:00
for (int i = 0; i < wave.count; i++)
{
2025-08-22 21:40:50 +05:00
SpawnEnemyOnce(wave.enemyPrefab);
yield return new WaitForSeconds(1f / wave.spawnRate);
2025-08-22 21:31:17 +05:00
}
2025-08-22 21:40:50 +05:00
// 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)
2025-08-22 21:31:17 +05:00
yield return new WaitForSeconds(timeBetweenWaves);
}
IsSpawning = false;
2025-08-22 21:40:50 +05:00
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);
}
}
2025-08-22 21:31:17 +05:00
}
2025-08-22 21:40:50 +05:00
// Called by relay on death/destroy/disable
private void NotifyGone(GameObject go)
2025-08-22 21:31:17 +05:00
{
2025-08-22 21:40:50 +05:00
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;
2025-08-22 21:31:17 +05:00
2025-08-22 21:40:50 +05:00
_owner.NotifyGone(gameObject);
_notified = true;
}
2025-08-22 21:31:17 +05:00
}
2025-08-22 21:40:50 +05:00
}