Designing Enemies using Abstract Classes!
An abstract class is a class that cannot be instantiated directly, however other classes can inherit from it and be instantiated themselves. What this means for us in game development is that an abstract class can be used as a sort of blueprint or base class, and we can build other classes off of it without having to keep rewriting the same code.
For example, let’s say we have three enemy types: skeletons, giants, and spiders. Instead of jumping in and creating three separate lots of similar (or even identical) variables and methods for these, we can first think about what behaviours or traits these classes will have in common. In our case, we can probably assume they’ll all have some sort of ‘health’ variable (most likely an integer), an Animator component, a sprite, a RigidBody, a collider, etc. So rather than start with each of the enemy types (the ‘child classes’ or subclasses), let’s start with the base class:
To keep things simple, I’ll just be focusing on a fairly simple base class like this (which really just has an Initialise method). We start off by declaring this class an abstract one by using the abstract keyword in the declaration. We’ve then got some protected variables such as health and speed. The “protected” keyword is pretty important when it comes to class inheritance — essentially we’re declaring a private variable that can nonetheless be used and accessed by any classes which inherit from the class the variable is declared in. Which is to say, our Skeleton, Giant, and Spider classes will all be able to use these variables while they remain, essentially, private to other classes.
We’ve then got some other variable declarations that our child classes will also have in common, such as a SpriteRenderer component, and then eventually we get down to our Initialise method. As you can see, we’re using both the protected and virtual keywords with this one. The use of the ‘virtual’ keyword only really becomes clear when we take a look at a child class which ‘overrides’ it by having a local implementation of the method:
Okay, so this might seem a little convoluted at first but let’s follow this through. When our Start method in the Enemy base class calls the “Initialise” method it actually ends up calling the version of the method in the Skeleton base class (since the “override” keyword essentially tells the program to listen to this version rather than the virtual one). At this point, we could have whatever logic we’d like, but since the functionality in the Enemy class does actually suit our purposes we can make the program return to that version of the method by calling base.Initialise(). After this function call, however, we can have whatever logic we’d like — logic which will be specific to the Skeleton class in this case. So in this example, we’ve got the best of both worlds: we can make use of inherited functionality in the parent class while also implementing some unique, local code.
By contrast, our Skeleton child class could in fact be pretty much empty and yet still have the functionality it inherits from the Enemy class:
And just as we’ve done with the Skeleton, we can have our Giant and Spider classes share the same behaviour by inheriting the same Enemy class:
I think I’ll touch on this a bit more in a subsequent article, but I hope this gives you a taste of the opportunities for refactoring and modularising our code class inheritance and abstract classes open up 😉