Level 1 according to waves

This commit is contained in:
Hazim Bin Ijaz 2025-08-22 21:40:50 +05:00
parent d096c20cf5
commit 204d25f657
3 changed files with 257 additions and 427 deletions

View File

@ -45650,24 +45650,6 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 268364652}
m_PrefabAsset: {fileID: 0}
--- !u!114 &269148361 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 385312391933029727, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
m_PrefabInstance: {fileID: 447491950}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9de313fb3f367d04bbd130657183cdc8, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!4 &269148367 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
m_PrefabInstance: {fileID: 447491950}
m_PrefabAsset: {fileID: 0}
--- !u!1 &270497163
GameObject:
m_ObjectHideFlags: 0
@ -69347,99 +69329,6 @@ MeshFilter:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 446839706}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!1001 &447491950
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 481936153}
m_Modifications:
- target: {fileID: 3951388154437428790, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: enableRegen
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_Name
value: Enemy (1)
objectReference: {fileID: 0}
- target: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.x
value: 0.05879492
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.y
value: 0.43239498
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.z
value: 0.06264454
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.x
value: -0.103
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.y
value: 0.16271059
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.z
value: -0.274
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: a205c5f30032031428dae83c4a2f2af8, type: 3}
--- !u!1001 &447901445
PrefabInstance:
m_ObjectHideFlags: 0
@ -74744,12 +74633,9 @@ Transform:
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 17.00827, y: 2.3127, z: 15.96308}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1806068479}
- {fileID: 269148367}
- {fileID: 2008455231}
m_Children: []
m_Father: {fileID: 1078692958}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &483243085
@ -168780,8 +168666,9 @@ GameObject:
m_Component:
- component: {fileID: 1078692958}
- component: {fileID: 1078692957}
- component: {fileID: 1078692959}
m_Layer: 0
m_Name: GameObject
m_Name: Level 1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@ -168799,11 +168686,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 42850bd66917c2648a04c287cd0fa78f, type: 3}
m_Name:
m_EditorClassIdentifier:
enemiesParent: {fileID: 481936153}
enemies:
- {fileID: 1806068473}
- {fileID: 269148361}
- {fileID: 2008455225}
spawner: {fileID: 1078692959}
checkInterval: 0.25
--- !u!4 &1078692958
Transform:
@ -168821,6 +168704,30 @@ Transform:
- {fileID: 481936153}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1078692959
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1078692956}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9274e1d861dffcc4f828e772e0d685d2, type: 3}
m_Name:
m_EditorClassIdentifier:
waves:
- enemyPrefab: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
count: 2
spawnRate: 5
- enemyPrefab: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
count: 3
spawnRate: 2
spawnPoint: {fileID: 481936153}
containerParent: {fileID: 481936153}
timeBetweenWaves: 10
--- !u!1001 &1080331081
PrefabInstance:
m_ObjectHideFlags: 0
@ -301654,24 +301561,6 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 1805929970}
m_PrefabAsset: {fileID: 0}
--- !u!114 &1806068473 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 385312391933029727, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
m_PrefabInstance: {fileID: 1527259665985799113}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9de313fb3f367d04bbd130657183cdc8, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!4 &1806068479 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
m_PrefabInstance: {fileID: 1527259665985799113}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &1806685759
PrefabInstance:
m_ObjectHideFlags: 0
@ -314028,94 +313917,6 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 1886256379}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &1886577979
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 481936153}
m_Modifications:
- target: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_Name
value: Enemy (2)
objectReference: {fileID: 0}
- target: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.x
value: 0.05879492
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.y
value: 0.43239498
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.z
value: 0.06264454
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.x
value: -0.049
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.y
value: 0.16271059
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.z
value: 0.118
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: a205c5f30032031428dae83c4a2f2af8, type: 3}
--- !u!1001 &1887138831
PrefabInstance:
m_ObjectHideFlags: 0
@ -331623,24 +331424,6 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 2008147940}
m_PrefabAsset: {fileID: 0}
--- !u!114 &2008455225 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 385312391933029727, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
m_PrefabInstance: {fileID: 1886577979}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9de313fb3f367d04bbd130657183cdc8, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!4 &2008455231 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
m_PrefabInstance: {fileID: 1886577979}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &2008475454
PrefabInstance:
m_ObjectHideFlags: 0
@ -355462,99 +355245,6 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 2145953391}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &1527259665985799113
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 481936153}
m_Modifications:
- target: {fileID: 3951388154437428790, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: enableRegen
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_Name
value: Enemy
objectReference: {fileID: 0}
- target: {fileID: 4006626876238256025, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.x
value: 0.05879492
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.y
value: 0.43239498
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalScale.z
value: 0.06264454
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.x
value: 0.224
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.y
value: 0.16271059
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalPosition.z
value: -0.136
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7154477416992791333, guid: a205c5f30032031428dae83c4a2f2af8,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: a205c5f30032031428dae83c4a2f2af8, type: 3}
--- !u!4 &1827334621118886615
Transform:
m_ObjectHideFlags: 0

View File

@ -1,61 +1,69 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Level1 : Level
{
[Header("Enemy Setup")]
[Tooltip("Optional: parent whose children have EnemyBase components")]
public Transform enemiesParent;
[Tooltip("Or manually assign enemies here")]
public List<EnemyBase> enemies = new List<EnemyBase>();
[Header("Monitoring")]
[Tooltip("How often to check alive enemies")]
public float checkInterval = 0.25f;
[Header("Wave Flow")]
public WaveSpawner spawner; // Assign in Inspector
[Min(0.05f)] public float checkInterval = 0.25f;
private bool _running;
private Coroutine _monitorCo;
// Optional: simple counters for UI hooks
private int _currentWaveIdx = -1;
private int _totalWaves = 0;
private int _currentAlive = 0;
private void OnEnable()
{
if (spawner != null)
{
spawner.OnWaveStarted += HandleWaveStarted;
spawner.OnWaveCompleted += HandleWaveCompleted;
spawner.OnAllWavesCompleted += HandleAllWavesCompleted;
spawner.OnAliveCountChanged += HandleAliveChanged;
}
}
private void OnDisable()
{
if (spawner != null)
{
spawner.OnWaveStarted -= HandleWaveStarted;
spawner.OnWaveCompleted -= HandleWaveCompleted;
spawner.OnAllWavesCompleted -= HandleAllWavesCompleted;
spawner.OnAliveCountChanged -= HandleAliveChanged;
}
}
public override void OnLevelStart()
{
base.OnLevelStart();
// Collect enemies if using a parent
if (enemiesParent != null)
if (spawner == null)
{
enemies.Clear();
foreach (Transform child in enemiesParent)
{
var eb = child.GetComponent<EnemyBase>();
if (eb != null) enemies.Add(eb);
}
}
// Turn enemies on & let their own Start() find the player
foreach (var e in enemies)
{
if (e == null) continue;
e.gameObject.SetActive(true);
}
// Edge case: no enemies -> complete immediately
if (CountAlive() == 0)
{
CompleteLevel();
Debug.LogError("[Level1] No WaveSpawner assigned.");
CompleteLevelImmediate();
return;
}
_totalWaves = spawner.TotalWaves;
_running = true;
_monitorCo = StartCoroutine(MonitorEnemies());
Debug.Log("Level1: Started, monitoring enemies...");
// Kick off waves
spawner.StartSpawning();
// As an extra safeguard, monitor completion state
_monitorCo = StartCoroutine(MonitorForCompletion());
Debug.Log("[Level1] Level started, waves spawning…");
}
public override void OnLevelEnd()
{
base.OnLevelEnd();
_running = false;
if (_monitorCo != null)
{
StopCoroutine(_monitorCo);
@ -63,46 +71,55 @@ public class Level1 : Level
}
}
private IEnumerator MonitorEnemies()
private IEnumerator MonitorForCompletion()
{
var wait = new WaitForSeconds(checkInterval);
while (_running)
{
if (CountAlive() == 0)
// Complete when spawner reports all waves completed AND no alive remain
if (spawner.AllWavesCompleted && spawner.CurrentAlive == 0)
{
CompleteLevel();
CompleteLevelImmediate();
yield break;
}
yield return wait;
}
}
private int CountAlive()
private void CompleteLevelImmediate()
{
int alive = 0;
for (int i = 0; i < enemies.Count; i++)
{
var e = enemies[i];
if (e != null && e.gameObject.activeInHierarchy && !e.IsDead())
alive++;
}
return alive;
_running = false;
Debug.Log("[Level1] All waves cleared. Level complete!");
if (GameplayManager.Instance != null)
GameplayManager.Instance.CompleteCurrentLevel();
else
gameObject.SetActive(false);
}
private void CompleteLevel()
// ===== Event handlers (useful for UI/logs/hook-ins) =====
private void HandleWaveStarted(int waveIdx, WaveSpawner.Wave wave)
{
Debug.Log("Level1: All enemies defeated. Completing level.");
_running = false;
_currentWaveIdx = waveIdx;
Debug.Log($"[Level1] Wave {waveIdx + 1}/{_totalWaves} started. Count={wave.count}, Rate={wave.spawnRate:F2}/s");
// TODO: Update UI: “Wave X/Y starting”
}
// Tell GameplayManager if available
if (GameplayManager.Instance != null)
{
GameplayManager.Instance.CompleteCurrentLevel();
}
else
{
// Fallback: just disable this level object
gameObject.SetActive(false);
}
private void HandleWaveCompleted(int waveIdx)
{
Debug.Log($"[Level1] Wave {waveIdx + 1}/{_totalWaves} completed.");
// TODO: UI “Wave complete”
}
private void HandleAllWavesCompleted()
{
Debug.Log("[Level1] All waves spawned. Waiting for last enemies to die…");
// Completion is finalized in MonitorForCompletion once alive == 0
}
private void HandleAliveChanged(int currentAlive)
{
_currentAlive = currentAlive;
// TODO: UI “Enemies Left: currentAlive”
}
}

View File

@ -1,69 +1,192 @@
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WaveSpawner : MonoBehaviour
{
[System.Serializable]
[Serializable]
public class Wave
{
public GameObject enemyPrefab; // Enemy prefab to spawn
public int count = 5; // Number of enemies in this wave
public float spawnRate = 1f; // Enemies per second (1 = 1/sec)
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 the hierarchy tidy")]
[Tooltip("Optional parent to keep hierarchy clean")]
public Transform containerParent;
public float timeBetweenWaves = 5f;
public bool autoStart = false;
[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 AllWavesDone { get; private set; }
public bool AllWavesCompleted { get; private set; }
public int CurrentAlive => _alive.Count;
public readonly List<EnemyBase> spawned = new List<EnemyBase>();
// 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
void Start()
{
if (autoStart) StartSpawning();
}
// 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 || AllWavesDone) return;
StartCoroutine(SpawnRoutine());
if (IsSpawning || AllWavesCompleted) return;
if (spawnPoint == null || waves == null || waves.Length == 0)
{
Debug.LogWarning("[WaveSpawner] Missing spawnPoint or waves.");
return;
}
_routine = StartCoroutine(SpawnRoutine());
}
IEnumerator SpawnRoutine()
public void StopSpawning()
{
if (_routine != null)
{
StopCoroutine(_routine);
_routine = null;
}
IsSpawning = false;
}
private IEnumerator SpawnRoutine()
{
IsSpawning = true;
AllWavesDone = false;
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++)
{
SpawnEnemy(wave.enemyPrefab);
yield return new WaitForSeconds(1f / Mathf.Max(0.0001f, wave.spawnRate));
SpawnEnemyOnce(wave.enemyPrefab);
yield return new WaitForSeconds(1f / wave.spawnRate);
}
if (w < waves.Length - 1)
// 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;
AllWavesDone = true;
AllWavesCompleted = true;
OnAllWavesCompleted?.Invoke();
}
void SpawnEnemy(GameObject enemyPrefab)
private IEnumerator WaitUntilCleared()
{
if (enemyPrefab == null || spawnPoint == null) return;
var wait = new WaitForSeconds(0.25f);
while (true)
{
CompactAliveList();
if (_alive.Count == 0) break;
yield return wait;
}
}
var go = Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation, containerParent);
var eb = go.GetComponent<EnemyBase>();
if (eb != null) spawned.Add(eb);
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;
}
}
}