Road to Game Dev: Enemy Wave Management (Part 1)
Now that we have some more advanced movement patterns set up, it’s time to start setting up the foundations to creating an actual level in the game. As with most things, there’s a number of ways to approach this; the method I’ll be using isn’t the most elegant, but it builds on most of what has already been done.
Patterning Enemy Movement
The first step towards creating a wave is setting up how that wave will actually look and behave. Simply dropping enemies in randomly isn’t really a wave — we want our enemies to have a path, a plan, and a goal. This means that for each enemy, we have to give them a list of individual movement types; For example, a Burst move followed by a halt and then another Burst move.
The challenge here is finding a way to both store and access all of this information; For more advanced applications, it might make sense to make child objects storing the information, or even something called an Abstract Class. We could also choose to store the information in a JSON file and read from the file. For now, however, it’s the most straightforward to simply store the information on the enemy directly and make variants of the prefab. We can create a bunch of arrays storing the information, with each element corresponding to a specific move. Not each array will be used for every move, so they can just be left at 0. Here’s an example of what one of mine looks like:
Reading the Movement Data
Now comes the hard part: Actually reading the movement data and executing it properly. This is where tons of issues can arise, from timing, to ordering, to overlap, etc. Coroutines are our friends once again. We have two options here; one option is to make a coroutine that iterates to the next movement type whenever one finishes, while the other is to have a set of smaller coroutines that simply assign new information after a delay. Either option works; the first option is slightly less memory-intensive, but for this small of an application, I’ll be using the second option since it makes a bit more sense to my brain. Here’s what the coroutine looks like:
When it starts, it checks what cycle the enemy is currently on, in case we want an enemy to repeat movements several times. Then, after a delay, if the cycle is still the same, it re-assigns all of the movement information, allowing our movement controller to automatically switch over. And, if we’re not on burst move, it also stops the timer for it so it doesn’t overlap. Now we just need a master function to make sure the coroutines are triggered at the proper times:
With this, all of the coroutines are technically started at the same time, but the movement change doesn’t actually occur until the total delay has elapsed. This assumes that you assign the duration of each move, however — you don’t need to track the total duration if you’re assigning timestamps instead! The teleport controller is a much more compact version of the movement changer; since it doesn’t use any of the other information, it can use only those few variables.
Last thing here is that cycle check; sometimes, we may want an enemy to repeat a movement pattern again once it flies off the screen, or maybe we want it to reappear randomly in between moves, or simply jump to the top of the screen. For this, we need to declare a “respawn type” that tells the enemy what to do once it leaves the screen. This is as simple as editing our previous function that respawned enemies at the top:
One important thing to note here is the “distance to go” variable. If an enemy respawns at the top of the screen partway through a move, it can cause some extremely weird behavior, so this re-adjusts the destination so that the movement can proceed uninterrupted.
With this foundation in place, it’s time to start putting together waves…