Using a new Component to Create a Sturdier Enemy
Objective: Refactor the current “life” system so we can apply it to enemies as well.
Right now Galaxy Shooter has one type of enemy. While we can vary how they move, they will always fire their laser straight ahead, and they always explode when shot a single time. Not to mention, all enemies of this type have the exact same art.
Today we will make things much more exciting by introducing a new enemy type.
- It will have new art!
- This enemy must be shot by 3 lasers or one missile to be destroyed.
- When damaged, it displays smoking damage effects like the player does.
- It will have new behavior, which we’ll cover in tomorrow’s article
We can add a new sprite to the game like this, and create a new prefab that includes all the enemy necessities, like Enemy and EnemyBehavior class scripts, a circle collider, a rigidbody, and an audio source.
Right away there’s a problem. The new enemy doesn’t have the same explosion animation that the other enemy does. It doesn’t even have an animator, so the game will throw an error when this enemies is supposed to be destroyed.
However, we are able to handle this easily enough by editing the HandleDeathEffects method. If there’s an animator, it will use that to explode. If there isn’t an animator, it will just instantiate an explosion object.
Now we want to give this new enemy multiple lives and damage effects. The Player class already does this. How about we just copy that code?
DON’T! Copying large swaths of code like this is a terrible coding practice. First, we start having to manage similar things in two different places. We’ll have to struggle to figure out exactly what the player needs, what the enemy needs, and what both need, while doing all that in two scripts that have tons of other stuff they handle as well.
Also, we may later want to make some change to this lives system that should affect both the player and the enemies. In that case, we’ll have to remember to make those changes in two completely unrelated locations in the code, or else we’ll likely run into some untended behavior and have no idea why. These are some big problems that could lead to massive headaches.
Instead, lets just do the same thing that we did with the EnemyBehavior classes and use the component pattern again. We can create a basic abstract class LivesComponent that handles all the necessary methods and variables that are necessary for a lives system. We include many methods that used to be in the Player class for handling taking damage and dying. Particularly helpful is the property IsDead which can be publicly accessed by any script that needs to know if this game object should count as being dead.
Now all the player-specific lives functionality can be put into the PlayerLives class which inherits from LivesComponent. Here we override some methods to add camera shake, update the lives meter in the UI, and allow repair powerups to regain a life for the player.
We can put enemy functionality into EnemyLives. This includes getting the AudioSource and Animator for when enemies should be exploding, which is handled by the new HandleDeathEffects method we wrote above.
After doing all this, there’s still a bunch of minor changes we have to make throughout the code. Anything that checked if the player or enemy is dead now has to access their lives component instead. We should set that class as a property so we have easy access to it.
Finally, to finish refactoring, we just need to make sure the player and enemy objects have these new scripts attached, and their inspector variable set to reasonable values. We can also set up our new enemy with extra max lives and some damage effect objects, since enemy lives inherit that functionality from the LivesComponent class.
Now we have this new enemy that can take multiple hits, and show damage effects just like the player does!
You might think it’s a lot of work just to get this simple job done, but there’s way more benefits to this than it first appears. Any time we add some new feature to how the player’s life system works, we now have an extremely easy way to add it to enemies as well. Also, other functionality like shields can be added to enemies easily as well, though we’ll leave that topic for another time.