Knowledge Bits: Implementing a Wave Spawning System in Unity

Eddie Sanchez
9 min readApr 12, 2023

Welcome to another edition of Knowledge Bits! In this article, we’ll dive into the creation of a wave spawning system in Unity. This system enhances the gameplay experience by adding increasing difficulty through progressive waves of enemies, with different movement types and spawn rates.

We’ll explore the modifications made to existing scripts and the addition of new ones to make this wave spawning feature possible. Let’s get started!

Code Changes and Breakdown:

Adding Wave Management to the SpawnManager:

The SpawnManager script has been updated to include a wave management system. The system is responsible for controlling the waves of enemies that spawn, adjusting the spawn rate, and determining the enemy movement types.

The core of this system is the WaveManagement coroutine, which handles the wave progression logic. It calls other helper methods to update the spawn rate and assign enemy movement types based on the current wave number.

Overview:

This coroutine is responsible for managing the wave progression. It adjusts the spawn rate, assigns enemy movement types, spawns enemies, and updates wave duration and wave number.

Breakdown:

  1. IEnumerator WaveManagement(int _wave, float _waveDuration): This method is a coroutine that manages waves in the game. It takes two parameters: _wave, the current wave number, and _waveDuration, the duration of the current wave.
  2. if (_stopSpawning): This checks if the _stopSpawning flag is set to true. If so, it means that spawning should be stopped (e.g., when the player has died).
  3. yield break;: If _stopSpawning is true, the coroutine stops executing and returns immediately.
  4. float updatedSpawnRate = SpawnRateAssignment();: This calls the SpawnRateAssignment() method to get an updated spawn rate based on the current game state.
  5. _spawnRate = updatedSpawnRate;: This assigns the updated spawn rate to the _spawnRate field.
  6. Enemy.MovementType movementType = MovementTypeAssignment(_wave);: This calls the MovementTypeAssignment() method to determine the movement type for enemies in the current wave.
  7. StartCoroutine(SpawnEnemyRoutine(_spawnRate, _waveDuration, movementType));: This starts a coroutine called SpawnEnemyRoutine() with the current spawn rate, wave duration, and enemy movement type as parameters. This coroutine handles spawning enemies during the current wave.
  8. yield return new WaitForSeconds(_waveDuration);: This pauses the WaveManagement() coroutine for the duration of the current wave. Once the wave duration has passed, the coroutine resumes execution.
  9. _waveDuration = WaveDurationAssignment(_waveDuration);: This calls the WaveDurationAssignment() method to calculate the wave duration for the next wave.
  10. _wave++;: This increments the _wave variable to advance to the next wave.
  11. _uiManager.UpdateWaveCount(_wave);: This updates the wave count displayed in the game's UI.
  12. StartCoroutine(WaveManagement(_wave, _waveDuration));: This starts the WaveManagement() coroutine again with the updated wave number and wave duration, allowing the game to progress to the next wave.

The WaveManagement() coroutine essentially manages the game's waves, handling enemy spawning and updating the game's state as waves progress.

Adjusting Spawn Rate and Enemy Movement Types:

Overview:

To make the game progressively more challenging, the spawn rate is reduced as the game advances through the waves. This is done by modifying the _spawnRate variable using the SpawnRateAssignment() method. It ensures that the spawn rate does not decrease below a predefined maximum spawn rate.

Breakdown:

  1. if (_spawnRate - _spawnDecreaseRate >= _maxSpawnRate): This checks if the current spawn rate, minus the spawn decrease rate, is greater than or equal to the maximum spawn rate. This condition ensures that the spawn rate doesn't go below the maximum spawn rate.
  2. _spawnRate -= _spawnDecreaseRate;: If the condition is met, the spawn rate is decreased by the spawn decrease rate. This makes the enemies spawn more frequently as the game progresses, increasing the game's difficulty.
  3. return _spawnRate;: Finally, the method returns the updated spawn rate.

Overview:

The enemy movement types are assigned based on the current wave using the MovementTypeAssignment() method. Different movement types are assigned to enemies depending on the wave number, adding variety and challenge to the game.

Breakdown:

The MovementTypeAssignment references this enum
Pattern Matching with a when clause is used to add conditional logic to the switch statement.

This code defines a method called MovementTypeAssignment(int wave) that takes an integer wave as an argument and returns an Enemy.MovementType enum value. The method is used to determine the movement type of enemies for each wave in the game.

  1. Enemy.MovementType movementType;: This line declares a variable called movementType of type Enemy.MovementType, which is an enum.
  2. The switch statement checks the wave variable's value and determines the corresponding movement type for the enemies based on the wave number:
  • If the wave number is between 1 and 5 (inclusive), the movement type is set to Enemy.MovementType.StraightDown.
  • If the wave number is between 6 and 10 (inclusive), the movement type is set to Enemy.MovementType.Circle.
  • If the wave number is between 11 and 15 (inclusive), the movement type is set to Enemy.MovementType.Angle.
  • If the wave number is between 16 and 20 (inclusive), the movement type is set to Enemy.MovementType.SineWave.
  • If the wave number is greater than 20, the movement type is chosen randomly from the available movement types.
  • If an invalid wave number is given, the method logs an error and sets the movement type to Enemy.MovementType.StraightDown.
  1. return movementType;: Finally, the method returns the determined movement type for the enemies.

The public enum MovementType definition lists the possible movement types for the enemies:

  • StraightDown: Enemies move straight down.
  • Angle: Enemies move at an angle.
  • SineWave: Enemies move in a sine wave pattern.
  • Circle: Enemies move in a circular pattern.

Wave Duration:

To control the time between each wave, the WaveDurationAssignment() method is introduced. This method increases the wave duration by a constant increment, up to a specified maximum duration.

Overview:

WaveDurationAssignment(float waveDuration) that takes a float waveDuration as an argument and returns a float. The method is used to calculate and update the duration of each wave in the game.

  1. const float increment = 2.0f;: This line declares a constant float increment with a value of 2.0, which represents the duration increase for each subsequent wave.
  2. const float maxWaveDuration = 60.0f;: This line declares a constant float maxWaveDuration with a value of 60.0, which represents the maximum allowed duration for any wave.
  3. float newWaveDuration = waveDuration + increment;: This line calculates the new wave duration by adding the increment to the current waveDuration.
  4. if (newWaveDuration > maxWaveDuration) { newWaveDuration = maxWaveDuration; }: This line checks if the calculated newWaveDuration exceeds the maxWaveDuration. If it does, the newWaveDuration is set to the maxWaveDuration value.
  5. return newWaveDuration;: Finally, the method returns the updated wave duration value.

Spawning Enemies with Different Movement Types:

Overview: (No Breakdown on this snippet)

A new method, SpawnEnemyRoutine(), is responsible for spawning enemies with the assigned movement types within the specified wave duration. The movement type is passed as an argument to this method, which then assigns it to the spawned enemy using the SetMovementType() method in the Enemy script.

Updating the UIManager:

The UIManager script is updated to display the current wave number on the game’s UI. The UpdateWaveCount() method is called whenever a new wave starts, updating the wave text accordingly.

The wave-related code ties in with the TextUIEffects script to display and animate the wave text when a new wave starts.

  1. _waveTextCounter: This variable holds a reference to the TMP_Text element that displays the current wave count on the screen.
  2. _waveTextPulse: This variable holds a reference to the TextUIEffects component that is responsible for animating the wave text when a new wave starts.

In the UpdateWaveCount(int wave) method:

  1. _waveText.text = $"Wave: {wave}";: This line updates the wave text displayed on the screen.
  2. _waveTextCounter.text = $"Wave: {wave}";: This line updates the wave text counter with the current wave value.
  3. if (_waveTextPulse != null) { _waveTextPulse.ShowAndPulseWaveText(); }: This line checks if the _waveTextPulse reference is not null, and if it isn't, calls the ShowAndPulseWaveText() method from the TextUIEffects script. This method animates the wave text when a new wave starts.

The UIManager script communicates with the TextUIEffects script to display and animate the wave text when a new wave starts. This helps create visual feedback for the player, letting them know that they’ve progressed to a new wave in the game.

  1. Adding Text Animation:

To draw attention to the changing wave number, a new script called TextUIEffects is added. This script is responsible for animating the wave text by pulsating its size over a specified duration. The ShowAndPulseWaveText() method is called in the UIManager script to trigger the animation when a new wave starts.

The entire TextUIEffects script used in my 2D Space Shooter. It attaches to the canvas UI Text element.

Initializing Wave Spawning:

To kick off the wave spawning system, the StartSpawning() method in the SpawnManager script is called when the initial asteroid is destroyed. This event is detected within the Asteroid script's OnTriggerEnter2D() method, which calls the StartSpawning() method upon the asteroid's destruction.

Note: This bit I didn’t include the code for just because it’s super specific to my implementation. You can start your game however you desire, in this 2D Space Shooter, I have our players shoot an asteroid to kick things off.

Create the Current_Wave_text and Wave_Count_text UI elements using TextMeshPro in Unity:

Create the Current_Wave_text UI element:

  • Right-click on the Canvas in the Hierarchy panel, then go to UI > Text — TextMeshPro.
  • Rename the newly created GameObject to “Current_Wave_text”.
  • In the Inspector panel, adjust the RectTransform’s anchors, position, width, height, and other properties as needed to position the text on the screen.
  • In the TextMeshPro component, update the text property to “Wave: 0” or any other default value you prefer.
  • Adjust the font, font size, color, and other styling properties as desired.

Create the Wave_Count_text UI element:

  • Right-click on the Canvas in the Hierarchy panel, then go to UI > Text — TextMeshPro.
  • Rename the newly created GameObject to “Wave_Count_text”.
  • Adjust the RectTransform’s properties to position the text counter on the screen, separate from the Current_Wave_text element.
  • Update the TextMeshPro component’s text property to “Wave: 0” or any other default value you prefer.
  • Adjust the font, font size, color, and other styling properties as desired.

Link the UI elements to the UIManager script:

  • Select the UIManager GameObject in the Hierarchy panel.
  • In the Inspector panel, locate the UIManager script and assign the Current_Wave_text and Wave_Count_text GameObjects to the respective variables (_waveText and _waveTextCounter) by dragging them from the Hierarchy panel or using the Object Picker.
  • Add references in the UIManager.cs script to point to these new UI Elements and attach them in the Unity Editor.

After completing these steps, the UIManager script will be able to update and display the Current_Wave_text and Wave_Count_text UI elements as the game progresses.

Next Time…

In this edition of Knowledge Bits, we’ve covered the implementation of a wave spawning system in Unity. This feature adds depth and increasing difficulty to the gameplay experience, making it more engaging for players. By implementing wave management, adjusting spawn rates, assigning enemy movement types, and updating the game’s UI, we’ve created a dynamic system that enhances the overall game design.

But we’re not stopping here! In our next article, we’ll tackle an intriguing twist in power-up design: creating a power-up that negatively affects the player. Discover how to challenge your players and keep them on their toes with this unexpected gameplay element.

Stay tuned for more Knowledge Bits articles, where we’ll continue exploring various aspects of game development and other fascinating topics!

--

--

Eddie Sanchez

Game Designer/Programmer looking to craft worthwhile experiences and in the process calm my soul. It's restless...