Enemy Wave Spawner System

Objective: Create a system where there are 10 waves of enemies. Each consecutive wave spawns an additional enemy. The 10th wave will have 0 enemies so a boss can be there in the future.

Natalia DaLomba
Women in Technology
5 min readAug 6, 2023

--

Write the beginning of our WaveSystem script, we have a CollectableSpawnInfo struct for our Provisions and PowerUps which is not directly related to the Enemy Wave System. The wave variable is set to 1 and we access it through the Wave property. We also keep track of when the player is defeated or not and we also reference the HealthEntity script.

We also reference the PowerUpSpawner and ProvisionSpawner classes so we can spawn those Collectables through our WaveSystem class further down.

using UnityEngine;

public class WaveSystem : MonoBehaviour {
[System.Serializable]
protected struct CollectableSpawnInfo {
public GameObject prefab;

public float minSpawnTime;
public float maxSpawnTime;
}
[SerializeField] protected static int wave = 1;

protected static bool isPlayerDefeated = false;
private HealthEntity playerHealth;

private PowerUpSpawner powerUpSpawner;
private ProvisionSpawner provisionSpawner;

At Start, we find the player and if the player and HealthEntity component exists, we subscribe the CheckForDeath method to the onDamaged Action from the HealthEntity class. powerUpSpawner and provisionSpawner are both set to reference the correct components.

When the player dies, we unsubscribe the CheckForDeath method from onDamaged. When the wave system starts, we call the startSpawning method which starts two coroutines we already have for the PowerUps and Provisions. CheckForDeath simple says if the player’s health is 0 or below, the player is defeated.

public int Wave => wave;

private void Start() {
GameObject player = GameObject.FindWithTag("Player");
if (player != null) {
playerHealth = player.GetComponent<HealthEntity>();
if (playerHealth != null)
playerHealth.onDamaged += CheckForDeath;
}
powerUpSpawner = GetComponent<PowerUpSpawner>();
provisionSpawner = GetComponent<ProvisionSpawner>();
}

private void OnDestroy() {
if (playerHealth != null)
playerHealth.onDamaged -= CheckForDeath;
}

public void StartSpawning() {

StartCoroutine(powerUpSpawner.SpawnPowerUpCoroutine());
StartCoroutine(provisionSpawner.SpawnProvisionCoroutine());
}

private void CheckForDeath() {
if (playerHealth.Health <= 0)
OnPlayerDeath();
}

public void OnPlayerDeath() {
isPlayerDefeated = true;
}
}

To follow, we create a TMP Text object in our Canvas so the Wave number can be displayed every time we enter a new wave. Ensure the text is empty because will be setting it through code. Create a script called New Wave Display.

This text will display the current wave number for 2 seconds in the center of the screen (how we positioned it on the Canvas in Unity).

We also have the current wave number displayed at the top right corner of the screen with the current script and another TMP text game object set up in Unity like so:

Now we move on to the EnemyWaveSpawner class that inherits from WaveSystem. We initialize the enemy prefab we want to instantiate and the enemy container they will be in. We use a list to keep track of how many enemies are currently spawned and we also create a new instance of the NewWaveDisplay class we just created.

Initially at wave 1, the total/max enemies to spawn is 3. We note the final wave which will be where the boss is, is 10 and we initialize an enemy GameObject for when we’re spawning them which will be a clone of the enemy prefab. We also set up all of the public properties.

It’s typically always a good idea to write input logic in the Update method. So the B key will bring us to the last enemy that’s alive in wave 9. Then once you kill it, you will enter wave 10.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyWaveSpawner : WaveSystem {
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private GameObject enemyContainer;
[SerializeField] private List<GameObject> enemies = new List<GameObject>();
[SerializeField] protected NewWaveDisplay newWaveDisplay;

private int maxEnemies = 3; //final max regular enemies is 11 on wave 9
private int bossWave = 10;
private GameObject enemy;

public bool IsRegularWave => wave < bossWave;
public bool IsBossWave => wave == bossWave;
public NewWaveDisplay NewWaveDisplay => newWaveDisplay;

private void Update() {
//arrive at wave 9 with one enemy to kill and then wave 10 begins
if (Input.GetKeyDown(KeyCode.B)) {
wave = 9;
maxEnemies = 1;
enemies.Clear();
enemies.Add(enemy);
StartCoroutine(WaitForAllEnemiesDefeated());
}
}

//more code below

We write a SpawnEnemyCoroutine which will display the new wave UI at the beginning of the first wave. Then the set amount of enemies will be spawned. Once all enemies for the current wave have spawned, through WaitForAllEnemiesDefeated, we check through every enemy in our list and if any don’t exist anymore, remove it from the list as well.

Then wait 3 seconds and increase the wave number by 1 and maximum amount of enemies that can show up in the next wave by 1.

public IEnumerator SpawnEnemyCoroutine() {
WaitForSeconds wait3Sec = new WaitForSeconds(3);
WaitForSeconds wait5Sec = new WaitForSeconds(5);

while (IsRegularWave) {
StartCoroutine(NewWaveDisplay.ShowWaveText());
yield return wait3Sec;

for (int i = 0; i < maxEnemies; i++) {
Vector3 pos = new Vector3(Random.Range(-8f, 8f), 8.5f, 0);
enemy = Instantiate(enemyPrefab, pos, Quaternion.identity);
enemies.Add(enemy);

enemy.transform.SetParent(enemyContainer.transform);
yield return wait5Sec;
}

yield return WaitForAllEnemiesDefeated();
yield return wait3Sec;

wave++;
maxEnemies++;

if (isPlayerDefeated)
yield break;
}
yield return BossWaveCoroutine();
}

private IEnumerator WaitForAllEnemiesDefeated() {
while (enemies.Count > 0) {
//NOTE: Here, we're removing enemies from the list
//as they are defeated.
for (int i = enemies.Count - 1; i >= 0; i--) {
if (enemies[i] == null)
enemies.RemoveAt(i);
}
yield return null;
}
}

private IEnumerator BossWaveCoroutine() {
StartCoroutine(NewWaveDisplay.ShowWaveText());

//TODO:
yield break;
}
}

This is how our Spawner game object looks and our Enemy Wave Spawner System works!

--

--

Natalia DaLomba
Women in Technology

A Unity C# developer inspired by game design logic used to create digital adventures. https://www.starforce.games/devlog/