Implementing a ‘Wave’ System (2D Space Shooter)

Previously, this 2D Space Shooter was just spawning enemies pretty much indefinitely until the player, inevitably, either loses the game or loses interest (or both!). I’ve since shifted it over to a wave system, whereby the GameLogic controller tells the SpawnManager when to start spawning another wave and what exactly that wave consists of. In order to do this, however, it seemed best create and define a ‘Wave’:

(Have I mentioned I love structs?)

This serialised public struct ‘Wave’, in combination with a public array or list, allows us to specify within the Unity editor what a wave consists of: how many enemies should be spawned during it? which enemy types should be spawned? how frequently should they be spawned? We can then iterate through this list or array using a loop, or alternatively we could add each element to a Queue and then use the Queue.Dequeue method to return the next Wave in the list until it’s empty. I opted for this latter one, if only so that I can (finally) have a go using Queues in a game dev project.

In my GameLogic script, I’ve got a LoadWave coroutine which checks to see if there are any waves left and, if there are, it’ll display the wave number, wait a few seconds, and then pass some of that wave’s information into a public method in the SpawnManager class (the array of enemyPrefabs, number of enemies, etc.). A while loop then halts this, waiting for the ‘waveComplete’ boolean to be set from false to true when all of the enemies have been spawned and destroyed. I typically nest this sort of logic within a larger while loop, but this time round I’m having a go using recursion by calling this LoadWave coroutine after it’s finished (which will then check to see if there are any waves left).

As mentioned, I’m currently passing in individual variables about the wave, mainly because I believe it’s better practice to be clear about what the method, and indeed the object it belongs to, needs to know about the wave. But to be honest, I’m probably going to end up just sending the struct itself over so that the SpawnManager script can store it and refer to it locally at any time. Otherwise I have to keep going back and adding new arguments every time I think of something else the SpawnManager method needs to have passed in (for instance, whether the enemies within the wave can dodge or fire backwards). I’m not entirely sure which is better practice at this stage, but I feel like the SpawnManager could reasonably afford to know as much as possible about the wave it’s meant to spawning. But anyway, that’s enough arguing about arguments.

Once there are no more waves in the queue, I can then call a method to handle the end-state of the game (maybe a brief outro sequence and credits). I think the main thing I’m appreciating about this new system is that it allows me to quickly and easily define all of the rounds for the game within the inspector. I also imagine this would be more accessible to other members of the team, who could drag, drop, and adjust things in GameLogic to test out the game’s difficulty progression. The game’s currently tough as nails (or perhaps I’m just bad at shoot-em-ups), so I may need to experiment with this a bit myself 😉

(I might re-order these public variables, just so it’s a little tidier)

Game Developer | Game Design and Literature Graduate