Creating a Wave System
Objective: Learn how to implement a wave system.
At the moment we have our enemies being spawn at random every couple of seconds, indefinitely. Now we want to update this to a wave system, meaning a certain amount of enemies are going to be spawned every wave, getting increasingly harder each wave.
After defeating a wave, the next will include more enemies, faster enemies, that also spawn faster.
Since we are going to implement a Wave System to our game, it would also be nice to have a message that will tell the player when the next wave is going to start.
[SerializeField] private TextMeshProUGUI _waveSpawnText;
[SerializeField] private TextMeshProUGUI _waveNumberText;
With both our new variables for our texts, lets just adjust them on the Inspector.

So we have our UIManager ready to be coded. Lets start there.
public void UpdateWaveText(int wave)
{
_waveNumberText.text = "Wave: " + wave.ToString();
}
This one pretty simple. When we have a new wave, we simply call this method to update the display.
public IEnumerator NextWaveSpawnRoutine(int wave)
{
_waveSpawnText.gameObject.SetActive(true);
for (int i = 5; i >= 0; i--)
{
if(i > 0)
{
_waveSpawnText.text = "WAVE " + wave + " STARTS IN " + i + "!";
yield return _waveDelay;
}
else
{
_waveSpawnText.text = "WAVE " + wave + " STARTS NOW !";
yield return _nowDelay;
}
}
_waveSpawnText.gameObject.SetActive(false);
}
Now to our Coroutine. This is how we are going to show the player when our next wave starts. We have a loop that counts down from 5 to zero. Every second it updates the display with the current wave, and the second.
When it reaches zero, it just says that the wave is starting, and after a brief delay, we turn off the text.

Now to our SpawnManager. This one is going to include more variables and changes.
private int _currentWave = 1;
private int _enemiesPerWave = 5;
private float _spawnInterval = 5.0f;
private float _enemySpeedMultiplier = 1f;
private float _spawnIntervalDecreasePercentage = 10f;
private List<GameObject> _activeEnemies = new List<GameObject>();
So we have 6 new variables for our SpawnManager. Let’s break them down:
_currentWave: Holds the current wave of our game.
_enemiesPerWave: Holds the amount of enemies that should be spawned.
_spawnInterval: Holds the interval in which the enemies are spawned.
_enemySpeedMultipler: Holds the multiplier for the enemy speed, making our enemies faster.
_spawnIntervalDecreasePercentage: Holds how much faster each wave will go related to the previous one.
_activeEnemies: Holds the list for all enemies that are alive in our game. This is used for us to check if all enemies spawned died and we can start our next wave.
private IEnumerator EnemySpawnRoutine()
{
yield return UIManager.Instance.NextWaveSpawnRoutine(_currentWave);
while (!_isPlayerAlive)
{
for (int i = 0; i < _enemiesPerWave; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(_spawnInterval);
}
while (_activeEnemies.Count > 0)
{
yield return null;
}
_currentWave++;
_enemiesPerWave += 2;
_spawnInterval *= 1.0f - (_spawnIntervalDecreasePercentage / 100.0f);
_enemySpeedMultiplier += 0.05f;
UIManager.Instance.UpdateWaveText(_currentWave);
yield return UIManager.Instance.NextWaveSpawnRoutine(_currentWave);
}
}
Now to our Coroutine. Firstly, we use a Yield Return to the Coroutine from our UIManager, which is our NextWaveSpawnRoutine. We pass our current wave as a parameter. This tells Unity to wait until the Coroutine is finished, before executing the rest of our code.
Inside our loop, which will be ran as long as our player lives. We first do a loop that will spawn all enemies inside our EnemiesPerWave variable.
Inside our Loop, we Yield Return our SpawnInterval, which started at 5 seconds, but gets increasingly faster as the game progresses.
After all enemies have been spawned, we check if there are still any enemies alive. This is done inside our While loop. While there are any living enemies, we just Yield Return null.
When all enemies are dead, we leave our loop. Then we proceed to increment all variables that are used for our Wave System. Making sure our wave increased, more enemies will spawn, the spawn interval will be lower, and our enemies will be faster.
Then we update our UIManager to match the correct wave, and yield return the coroutine for our next wave.
Now to our enemies. Since we are going to increase our enemies movement speed, this is going to be called when our enemies are spawned.
private void SpawnEnemy()
{
float randomPositionX = Random.Range(_minXPosition, _maxXPosition);
_enemySpawnPosition.Set(randomPositionX, _spawnPositionY, 0f);
var enemy = Instantiate(_enemyPrefab, _enemySpawnPosition, Quaternion.identity, _enemyContainer.transform);
_activeEnemies.Add(enemy);
var enemyBehavior = enemy.GetComponent<EnemyBehavior>();
if (enemyBehavior != null)
enemyBehavior.IncreaseSpeed(_enemySpeedMultiplier);
}
Still inside our SpawnManager, when instantiating our enemy, we now are grabbing a handle of our enemy. After this is done, we can add him to our list, and grab a handle of his EnemyBehavior script. After null checking, we just make sure to call the method that we are going to create next, which increases the movement speed of our enemies.
And lastly for our SpawnManager, after an enemy is destroyed, we need to remove him from our list. We are going to create a method, in which the enemy can provided its GameObject, so wew can remove him from the list.
public void DestroyEnemy(GameObject enemy)
{
_activeEnemies.Remove(enemy);
}
Now we move to our EnemyBehavior script. We need our method to increase movement speed, which is being called after the enemy is instantiated.
public void IncreaseSpeed(float multiplier)
{
_enemySpeed *= multiplier;
}
This allows us to pass a multiplier, which will increase our enemy movement speed. Each wave is increasing our movement speed by 5%, but this can be changed if needed.
public void TakeDamage()
{
_animator.SetTrigger(_onEnemyDeathHash);
_enemySpeed = 0f;
_audioSource.Play();
Destroy(gameObject, 2.5f);
SpawnManager.Instance.DestroyEnemy(gameObject);
Destroy(this);
}
Now in our TakeDamage method, we just make sure to call the DestroyEnemy from our SpawnManager, passing our gameObject as reference, making sure to remove the enemy being destroy from our active enemies.

And now our Wave System is completely working. As we can see, after destroying our last enemy, we just go into our NextWaveSpawnRoutine, and them proceed to spawn more enemies, which get stronger every wave!