Thank you for following up with me, Nina. I had meant to reply to your previous post, but was too excited to charge ahead and forgot to do so.
I like the way you used the transform.childCount to keep track of enemies being alive. That worked elegantly with the way you are having enemies go back and forth along their path - cool!
After stepping back to re-factor my code, and write it out the main elements on paper, I ended up with something along the lines of how you suggested earlier with a WaveTracker, and a WavesTracker (I named them slightly differently). Then my EnemySpawner maintains a queue of waves that are ready, keeps pulling from that queue as long as there is still an enemy alive from the wave.
To be honest, I suspect that my code is probably still more complicated than it needs to be - you’ll have to tell me how confusing the below is. But it at least made sense to me on paper, and I could follow it well enough to pretty quickly debug and such. I’m also just trying to force myself to learn Event action stuff too, so I can decouple the code. For my next game feature, I’ve even added a separate EventManager singleton class, which various other classes can call so they are decoupled from one another. I might come back and refactor this waves section to use the same EventManager.
Here’s what I ended up with for the Spawner, OneWaveStatusTracker, and WavesTracker. A small naming convention question - I didn’t like calling it WaveStatusTracker and WavesStatusTracker, because the difference (can you spot it!?) is only a plural ‘s’ - the classes didn’t stand out enough to me. What’s common practice though?
Thanks again for your interest! I’m sure I’ll be back in the forums somewhere along the way as I continue with my souped-up Laser Defender.
public class EnemySpawner : MonoBehaviour
{
[SerializeField] List<WaveConfigSO> waveConfigs;
[SerializeField] GameObject oneWaveStatusTrackerPrefab; // the OneWaveStatusTracker prefab
[SerializeField] float timeBetweenWaves = 3f;
Queue<WaveConfigSO> waveQueue;
WaveConfigSO currentWave;
OneWaveStatusTracker currentOneWaveStatusTracker;
List<OneWaveStatusTracker> statusTrackers;
WavesTracker wavesTracker;
bool isSpawning;
void Start()
{
// initialize variables
waveQueue = new Queue<WaveConfigSO>();
statusTrackers = new List<OneWaveStatusTracker>();
// connect to key classes
wavesTracker = FindObjectOfType<WavesTracker>();
// initialize stuff
InitializeOneWaveStatusTrackers();
// order matters for next 3 :
InitializeWaveConfigSOs(); // just giving them a wave number
InitializeWavesTracker(); // must be called AFTER InitializeWaveConfigSOs
InitializeWaveQueue(); // must be called AFTER InitializeWavesTracker
// immediate Events needed
wavesTracker.OnAllWavesDead += AllWavesDead;
// main loop
StartCoroutine(SpawnWaves());
}
public void InitializeOneWaveStatusTrackers()
{
for(int i=0;i<waveConfigs.Count;i++){
// remember our EnemyStatusTracker is actually attached to a game object. So you instantiate the type game object
GameObject newStatusTrackerObject = Instantiate(oneWaveStatusTrackerPrefab, transform.position, Quaternion.Euler(0, 0, 180), transform);
// then with that prefab, you grab its EnemyStatusTracker component
OneWaveStatusTracker newStatusTracker = newStatusTrackerObject.GetComponent<OneWaveStatusTracker>();
// initialize its ID and number of enemies
newStatusTracker.InitializeMe(i, waveConfigs[i].GetEnemyCount());
// add it to the list
statusTrackers.Add(newStatusTracker);
}
}
// give each WaveConfigSO an index num, which will be used throughout
private void InitializeWaveConfigSOs()
{
int i = 0;
foreach(WaveConfigSO waveconfig in waveConfigs)
{
waveconfig.SetAssignedWaveNum(i);
i++;
}
}
private void InitializeWavesTracker()
{
foreach(OneWaveStatusTracker tracker in statusTrackers)
{
wavesTracker.AddTrackedWave(tracker);
}
}
private void InitializeWaveQueue()
{
// put each waveConfig into the Queue
foreach(WaveConfigSO waveConfig in waveConfigs)
{
waveQueue.Enqueue(waveConfig);
}
// Event so it receives notification of a wave ready
wavesTracker.OnOneWaveReady += AddWaveToQueue;
}
// version that's triggered through Event, when a wave survives and needs to be respawned
public void AddWaveToQueue(int i)
{
waveQueue.Enqueue(waveConfigs[i]);
// Debug.Log("In EnemySpawner, added wave " + i + "to the queue");
// if not currently spawning, need to start the coroutine again
if(!isSpawning)
{
StartCoroutine((SpawnWaves()));
}
}
IEnumerator SpawnWaves()
{
isSpawning = true;
while(waveQueue.Count != 0){
currentWave = waveQueue.Dequeue();
// So that as we spawn each enemy, we can assign appropriate OneWaveStatusTracker.
// The WaveConfigSO has its wavenum, which is equivalent to index of statusTrackers list.
// We could probably do this with events instead, but we're doing it explictly instead.
currentOneWaveStatusTracker = statusTrackers[currentWave.GetAssignedWaveNum()];
StartCoroutine(SpawnOneWave());
yield return new WaitForSeconds(timeBetweenWaves);
}
isSpawning = false;
}
IEnumerator SpawnOneWave()
{
// determine how many enemies didn't die in last wave, so will be spawned
List<int> enemyStatuses = new List<int>();
enemyStatuses = currentOneWaveStatusTracker.GetEnemyStatuses();
// Debug.Log("For wave " + currentOneWaveStatusTracker.GetWaveID() + ", enemyStatuses before spawn: " + DebugLogList(enemyStatuses));
int numToSpawn = 0;
foreach(int enemyStatus in enemyStatuses){
if (enemyStatus==1) // means it survived last flight
{
numToSpawn++;
}
}
// Debug.Log("For wave " + currentOneWaveStatusTracker.GetWaveID() + " going to spawn " + numToSpawn);
// cheating, by assuming that all enemies of the wave are of the same type! Big forced thing
GameObject enemyPrefab = currentWave.GetEnemyPrefab(0);
// tell the tracker, so it sets enemy statuses to -1.
// This needs to be done before all the spawning happens, because you might destroy an enemy before the whole wave is spawned.
currentOneWaveStatusTracker.WaveSpawned();
// spawn that number of enemies
for (int i = 0; i < numToSpawn; i++)
{
GameObject enemyInstance = Instantiate(enemyPrefab, currentWave.GetStartingWaypoint().position, Quaternion.Euler(0, 0, 180), currentOneWaveStatusTracker.transform);
// hook up the Events to that instance
currentOneWaveStatusTracker.AddEnemyToTrack(enemyInstance);
// Debug.Log("Status tracker given index" + i + " prefab");
// random pause between enemies
yield return new WaitForSeconds(currentWave.GenerateRandomSpawnTime());
}
}
// used by EnemyPathfinder script
public WaveConfigSO GetCurrentWave(){
return currentWave;
}
IEnumerator Wait(float time)
{
yield return new WaitForSeconds(time);
}
// Event call from WavesTracker
private void AllWavesDead()
{
Debug.Log("EnemySpawner receives Event AllWavesDead");
}
// only used for debugging
public string DebugLogList(List<int> list)
{
StringBuilder builder = new StringBuilder();
builder.Append("[");
for (int i = 0; i < list.Count; i++)
{
builder.Append(list[i]);
if (i < list.Count - 1)
{
builder.Append(", ");
}
}
builder.Append("]");
return builder.ToString();
}
}
Each wave has a tracker, called OneWaveStatusTracker
/*
################### Basic idea ##############
> Initialized with a WaveID and int # enemies. EnemySpawner will do this.
> Create an array of ints, each int represents status of one enemy = main logic feature for class.
> > 0 = dead, 1 = alive and de-spawned back home, -1 = alive and flying
> When spawner grabs a WaveConfigSO + spawns the enemies, this class gets reference to each enemy, adds them to its Event listeners
> Every time an enemy event happens (dies, reaches end of flight path), triggers an UpdateStatus method here.
> > (those events happen in HasHealth, and EnemyPathfinder)
> UpdateStatus checks if all enemies are dead, or if all are accounted for (ie safe back at home or dead)
> If all accounted for, and some are still alive, broadcasts an event that this wave is ready to be respawned. WavesTracker listens.
> If all dead, broadcasts event. WavesTracker listens.
*/
public class OneWaveStatusTracker : MonoBehaviour
{
private int numMaxEnemies;
private int numDeadEnemies;
private int numAccountedFor; // debug only line, not needed for logic
private bool readyToSpawn;
// private bool allDead;
// private bool someStillAlive;
private List<int> enemyStatuses; // is this the right way?
public event Action<int> OnOneWaveDead;
public event Action<int> OnOneWaveSpawned;
public event Action<int> OnOneWaveReady;
private int waveID;
// tightly coupled with EnemySpawner - will be called by thatB
public void InitializeMe(int waveID, int enemyCount)
{
enemyStatuses = new List<int>();
numAccountedFor = 0;
numDeadEnemies = 0;
this.waveID = waveID;
for(int i = 0; i<enemyCount; i++)
{
enemyStatuses.Add(1); // initialize as alive and accounted for
}
// Debug.Log("Tracker initialized for wave " + waveID + ". Enemy count given: " + enemyCount);
numMaxEnemies = enemyCount;
}
// Spawner is going to assign enemies to this tracker
public void AddEnemyToTrack(GameObject enemy){
// trackedEnemies.Add(enemy); // remove this
if (enemy.TryGetComponent<HasHealth>(out HasHealth health))
{
health.OnEnemyDied += OneEnemyDied; // subscribe this WaveConfig's statusTracker to that died event
// Debug.Log("tracking enemy health");
}
if (enemy.TryGetComponent<EnemyPathfinder>(out EnemyPathfinder pathfinder))
{
pathfinder.OnEnemyReachedPathEnd += OneEnemyReachedEnd;
// Debug.Log("tracking enemy flight path");
}
// debugging only
// string outputLog = WaveStatusToString();
// Debug.Log(outputLog);
}
// when wave spawned or re-spawned, set all the 1s to -1, because enemies are no longer accounted for
public void WaveSpawned()
{
for(int i=0; i<enemyStatuses.Count; i++)
{
if (enemyStatuses[i]==1){
enemyStatuses[i] = -1;
}
}
OnOneWaveSpawned?.Invoke(waveID);
}
public void OneEnemyDied(){
int index = 0;
// since they could only be triggered as die after they've been sent out (aka status = -1), look for the first -1
while(enemyStatuses[index] != -1)
{
index++;
}
enemyStatuses[index] = 0; // set dead, aka 0
numDeadEnemies++;
numAccountedFor++; // for debug
// Debug.Log("Event: one enemy died");
UpdateStatus();
}
public void OneEnemyReachedEnd()
{
int index = 0;
// since they could only reach end if they've been sent out (aka status set to -1) look for the first -1
while(enemyStatuses[index] != -1)
{
index++;
}
enemyStatuses[index] = 1; // set alive and accounted for, aka 1
numAccountedFor++; // for debug
// Debug.Log("Event: one enemy reached end of flight path");
UpdateStatus();
}
// called after event that could make Wave either all dead or ready to spawn
private void UpdateStatus()
{
// for debugging
string outputLog = WaveStatusToString();
// check if any unnacounted for. If so, no more status check needed
if (enemyStatuses.Contains(-1))
{
// Debug.Log("EnemyStatusTracker updated. At least one -1. " + outputLog);
return;
}
// then check if all dead. If so, broadcast event
if (numDeadEnemies == numMaxEnemies)
{
// allDead = true;
OnOneWaveDead?.Invoke(waveID);
// Debug.Log("EnemyStatusTracker updated. All are dead. " + outputLog);
return;
}
// if the other two ifs aren't hitting, and the UpdateStatus has been called, it's because one is accounted for
// Debug.Log("EnemyStatusTracker updated. All must be 1. " + outputLog);
OnOneWaveReady?.Invoke(waveID);
}
private string WaveStatusToString()
{
string outputLog = "numMaxEnemies = " + numMaxEnemies;
outputLog += ". numDead = " + numDeadEnemies;
outputLog += ". numAccountedFor = " + numAccountedFor;
return outputLog;
}
// used by EnemySpawner to decide how many to respawn. Array representing how many enemies in this wave are dead vs alive
public List<int> GetEnemyStatuses()
{
return enemyStatuses;
}
public int GetWaveID()
{
return waveID;
}
}
And then one bigger tracker, WavesTracker, which subscribes to status-related Events from the OneWaveStatusTrackers. When I finish up, it will broadcast an event notifying when all waves are dead, and then the level will advance.
public class WavesTracker : MonoBehaviour
{
public event Action<int> OnOneWaveReady;
public event Action OnAllWavesDead;
List<int> waveStatuses;
private void Awake() {
// initialize variables
// needed to move this to Awake, or sometimes it didn't hit beforean AddTrackedWave was called by EnemySpawner.
waveStatuses = new List<int>();
}
private void Start() {
// debug
// Debug.Log("WavesTracker Start() method called, initial list values: " + DebugLogList(waveStatuses));
}
public void AddTrackedWave(OneWaveStatusTracker tracker)
{
waveStatuses.Add(1);
tracker.OnOneWaveReady += OneWaveReady;
tracker.OnOneWaveDead += OneWaveDead;
tracker.OnOneWaveSpawned += OneWaveSpawned;
}
private void OneWaveReady(int i)
{
waveStatuses[i] = 1;
// Debug.Log("WavesTracker OneWaveReady index " + i + ", waveStatuses: " + DebugLogList(waveStatuses));
// Pass the Event up the chain. Could have EnemySpawner do that, but that would be mixing responsibility
OnOneWaveReady?.Invoke(i);
}
private void OneWaveDead(int i)
{
waveStatuses[i] = 0;
// Debug.Log("WavesTracker OneWaveDead index " + i + ", waveStatuses: " + DebugLogList(waveStatuses));
CheckIfAllDead(); // handles the Event broadcast
}
// unnecessary for now. Adding it for completeness' sake, and debugging
private void OneWaveSpawned(int i)
{
waveStatuses[i] = -1;
// Debug.Log("WavesTracker OneWaveSpawned index " + i + ", waveStatuses: " + DebugLogList(waveStatuses));
}
// if they're all dead, will Invoke an Event
private void CheckIfAllDead()
{
// Debug.Log("WavesTracker CheckIfAllDead, waveStatuses: " + DebugLogList(waveStatuses));
for(int i = 0; i < waveStatuses.Count;i++)
{
if(waveStatuses[i] == 1 || waveStatuses[i] == -1)
{
// Debug.Log("Gets into this break line");
return; // an enemy is not dead
}
}
// if you reached end of loop, then they are all dead
OnAllWavesDead?.Invoke();
}
// only used for debugging
public string DebugLogList(List<int> list)
{
StringBuilder builder = new StringBuilder();
builder.Append("[");
for (int i = 0; i < list.Count; i++)
{
builder.Append(list[i]);
if (i < list.Count - 1)
{
builder.Append(", ");
}
}
builder.Append("]");
return builder.ToString();
}
}
A few lines needed in other scripts, which I have the OneWaveStatusTracker listening for:
EnemyPathfinder gets - OnEnemyReachedPathEnd?.Invoke();
HasHealth gets - OnEnemyDied?.Invoke(); // trigger the event, which other stuff listens to
With that big change that you pointed out to me, passing an Instance rather than a Prefab, that got me there.
I’ve now moved on to having subsequent “zones” with restricted movement, between which the player advances when all enemies are dead, or gets knocked back to when her health reaches zero. Events are still tripping me up so much more than I expected - they seem so simple!