Attack management can seem simple, but then easily become daunting over time; what works for a simple solution with only a few basic attacks can become increasingly complex to attempt to maintain as the number and complexity of the attacks increases. I had grand ideas for the number of attacks and their complexities for this boss, and that's why I ensured to make a more general, abstracted solution from the get go, one that wouldn't rise in complexity no matter how many attacks I added.
I decided on making all of the attacks coroutine based: this meant it was easy to space out parts of the attacks, and easy to detect if the attack was finished or still running, which was crucial for this system. Each attack coroutine, which we will call the CoAttack, will be encapsulated by a class called Attack. This Attack class lets us have more control over the management.
So what does this all mean? We use System.Func instead of a simple Coroutine for a variety of reasons. One of them is the fact that we don't have to start the coroutine first, we can initialise it straight from the function. This is useful as it lets us reuse it easily. I marked the field as readonly, as the core CoAttack of an Attack should never change, that wouldn't really make any sense.
Now we don't want our boss to spam attacks, so lets add some cooldown fields.
This allows us to control the frequency at which any given attack occurs by changing the CooldownUnique, and also allows us to stop the boss from spamming by using CooldownGlobal. Maybe we want some kind of ultimate attack that can only be used a few times, or moves that can only be used in different sections of the boss fight, so we're going to add some more fields to accommodate for that too.
Now that we've got all of that out of the way, we can write a function to determine if the attack is ready to be used.
So what exactly are we doing here? First we use the LINQ extension method .Contains to determine if this attack can be used in the current phase. If it can, we then test that it hasn't been used too many times. Lastly, we determine if the cooldown has been exhausted. If all of these tests pass, then the attack is ready.
Now, we can finally use and end our attack relatively simply.
Reaper is the class for the final boss in my game, use whichever Coroutine manager you please.
Now that we've got our attack stuff done, we're gonna want a manager that can abstract this all away for us. Make it nice and easy and automated.
So now we have our manager. This stores: all of the attacks available, the current phase of the boss fight, global cooldown data to prevent attack spamming, and the currently used attack, along with the coroutine manager of choice. Now we're going to write a function that will select the next attack for us.
So what does it do? First it checks if we are currently attacking or experiencing cooldown, either of these will cause the selection to fail immediately. We then narrow down our Attack list to only those that are ready, as determined by Attack.AttackIsReady that we wrote early. Lastly, if any remain, we will use an extension method I wrote called .RandomElement() to select an attack at random and return it.
Lastly, for the AttackManager we will now write the wrapper functions to use an attack.
First we check if there's an attack currently running, and if there isn't then we fire off a new coroutine. We wait for the Attack to finish executing, then we end it. Next we set up the global cooldown, and terminate the overall attack call for the AttackManager, signalling completion.
All done! Lets see what a basic test case could look like.
In the initialisation we just create the manager, create the attack and add the attack to the manager. In update, we constantly check if we're not attacking, and if we aren't, we attempt to attack. So simple!
I hope by reading this article you have a little more idea on how to design systems to be scalable and self managing, learned how to write an attack management system, or just learned a few new tricks! Please feel free to ask any questions in the comments below or by contacting me, and if you haven't already, check out Subsideria! From the 24th of August you'll be able to fight this legendary foe!