Dynamic Enemy Movements in Unity: Side-to-Side, Inheritance, and More…
One life remaining. Lasers seek their target but suddenly the Enemy changes position, evading attacks, blocking the Player’s flight path and forcing the Player to make a strategic decision resulting in a collision and GAME OVER. The Enemy’s movement has become more dynamic and unpredictable, creating more difficult circumstances and more dynamic gameplay.
As mentioned in the previous article, Unity Spawn Manager Tutorial: Mastering Enemy Spawning with Coroutines, one Enemy creates conflict but when that Enemy multiplies tension becomes even greater, placing demands on the hero, our Player, to use the game mechanics available to strategically overcome all obstacles and prove themself worthy of the title of hero. Why stop there? Let’s make the User prove their worth by taking it one step further by creating an enemy with multiple behaviors?
To accomplish this the behavior, i.e., game mechanics will embrace the concept of reusability. Reusability in the sense methods and variables will be reused by a sort of “child” class. If the “child” is reusing methods and variables of the original class then the original class must be some sort of “parent”. This represents the concept of inheritance. With that said, the Enemy or Enemies with behavior from the original Enemy class will inherit yet have their own unique behavior. To make sure that each Enemy’s own unique behavior is easy to maintain a Script dedicated to each one will encapsulate a singular responsibility. Easier said than done… Let’s begin.
Reinforcing Singularity
I will start with my original Enemy (Script). Currently the Update()
method has two responsibilities, (1) the movement (i.e., CalculateMovement()
) and (2) Instantiation of the Enemy Laser(s).
To reinforce concept of singularity, I will dedicate the responsibility of getting the Lasers, instantiating and assigning the Enemy Lasers to a custom method, and:
- declare a
private void FireLasers()
method, - copy the statements within the curly brackets, then paste it into the new method.
- Back up in the
Update()
method, I will cache a reference of theFireLasers()
method so that on eachUpdate()
the Enemy fire will continue at the rate set previously.
The last thing to do is to deal with the repetition of identical statements in the OnTriggerEnter2D()
method.
This repetition makes it difficult to maintain if revisions are made in within the Enemy class or between other classes that communicate with the Enemy class. Don’t Repeat Yourself (DRY) is best-practice in the process of developing and maintaining code uses the the strategy that keeps it “DRY” and encapsulates responsibilities that repeat into a single custom method and then cache a reference to that custom method in the same place of the statements they replace.
After cleaning up the code, the Enemy class is ready to be edited using access modifiers that facilitate it as a base class.
Preparing the Enemy as a Base Class
Now, to help any developer who opens the Enemy (Script) understand that it is the base class for other Enemy movement behavior within a hierarchy, I copy the CalculateMovement()
method then paste it before Start()
.
Assuming the role of the Enemy, I need to become the parent/base class so that other child/subclasses can inherit my properties/methods. To do this first I need to:
- know what properties/methods the child will inherit. In this case, the Enemy subclasses will inherit the movement and modify it based on their own properties.
- To only allow subclasses to inherit properties from a base class, while at the same time excluding other programs access to the base class, the keyword/access modifier protected needs placed in front of the method that will be inherited by the subclass.
- Now that the
CalculateMovement()
method is protected, notice another keyword, virtual, was added. The keyword virtual allows me, the base Enemy class, to create other methods that will inherit my properties and method but use those properties in a different way.
The Enemy subclasses are going to inherit the CalculateMovement()
method and its properties but change how they are used so they can move side-to-side, in a circle, and diagonally. In order for the Enemy child/subclass to take the properties and change them, it will need to have the keywords “protected override
” in front of the void CalculateMovement()
method (see below).
Create the Subclass
- Right-click on Scripts folder > Select Create from pop-up menu > Select C# Script.
- I will name the script ‘SideToSideEnemy’.
- I won’t need the
Start()
orUpdate()
method. Delete them. - If the class is using
Monobehaviour
(i.e.,public void class SideToSideEnemy: Monobehaviour
) deleteMonobehaviour
and rename the class to the base class which this subclass inherits properties and method(s) from (i.e.,Enemy
).
Next, declare the method that will be inherited. In this case, it is CalculateMovement()
and identify it it using the access modifiers protected virtual void
.
Next, I need to develop the logic of my movement. Starting with the this.transform.Translate()
, I need to move to the side. To accomplish that movement will be passed parameters Vector3.right (or left) * _speed *
and Time.deltaTime
.
Making an Inaccessible Error, Accessible to a subclass
When I add the statement, I notice that the variable _speed
should be inherited but it has a red squiggly line beneath it, throwing an Error CS0122: member is inaccessible due to its protection level .
In order to make the property of the base class accessible for inheritance, that specific variable must be declared using the keyword, “protected
”. Currently in the Enemy base class it is declared as private
.
After it is changed, there is no longer an Error.
Back in the SideToSideEnemy : Enemy
subclass the variable is now accessible.
Finetuning Side-To-Side Movement of Enemy
Now that I can move side-to-side in real-time, I can check my position on the X-axis, but checking how far I moved left or right is not enough to reverse the lateral movement. I need to create a boundary for myself to:
- compare my position on the X-axis to so that I know when to stop and
- begin to move in the opposite direction.
I will declare my range of lateral movement at the top of the class. This creates a standard of measure for me to decide what to do next. I’ll name it _limitedLateralMovement
and set it to 3.0f
.
Next, in the CalculateMovement(
) method, after I have moved sideways I need to check my range. if
I (this.transform
) move (.Translate
) greater than (>
) the limits of my lateral movement (3.0f
) to my right or (||
) left, then I need to reverse my direction. Movement on an axis in the reverse direction is the opposite value. The opposite of the positive value is the negative value. To check the opposite value set in the variable _limitedLateralMovement
a negative sign is placed in front of it (i.e., -_limitedLateralMovement
). This equates to -3.0f
.
Reversing Direction: Rule of Multiplying Positive and Negative Integers
Once I check my position on the X-axis, only then can reverse my direction. To control my direction, another variable to define an opposing direction will be declared based on the rule in multiplication involving positive and negative integers.
- If a positive integer (I’m on the right side) is multiplied by a negative integer, it equals a negative (so I move to the left); therefore it will move to the left.
- If the negative (I’m on the left side) is multiplied by the negative it equals a positive (so I move to the right). With that said, I declare the variable
_lateralDirection = 1.0f
. - Now I need a negative integer to multiply by this positive integer by. Back down in the
if
statement, I add my_lateralDirection
multiplied by-1;
.
To complete the movement from side-to-side, the transform.Translate()
method will also need to access the data stored in the variable _lateralDirection
so I will multiply my _lateralDirection
and the existing properties within the transform.Translate()
method.
I am moving laterally but to attack the player I need to inherit the downward movement. Below the if
statement that controls lateral direction, I add, this.transform.Translate(_speed * Time.deltaTime * Vector3.down);
.
Now, I have an Enemy that can evade laser fire by moving side-to-side but how to spawn it?
Spawning the New Enemy
After creating my base class and a subclass for the Enemy, I need to find a way to Instantiate
the new Enemy. The SpawnManager()
already handles that responsibility in the Coroutine SpawnEnemyRoutine()
(see below).
First, what is my logic? I will need a variable to store the 3 types of Enemy movement that will be randomly instantiated. Then I can use a switch statement to get the different subclasses (i.e., Script(s)) of enemy movement.
The different Enemy subclasses will be identified as integers and stored the a variable named randomEnemyType
. The method Random.Range()
method will then be passed a minimum value of 0
and a maximum value of 3
.
Next a switch statement will be passed the parameter that uses the method Random.Range()
, i.e., randomEnemyType
. Now I can randomly choose between 3 different cases.
Currently the variable named newEnemy
is being used to Instantiate the _enemyPrefab
.
I need to add my SideToSideEnemy
behavior to the newEnemy
. Google search: “How do I add a component to an existing component? Unity”. Search results reveal the Scriptin API: GameObject.AddComponent().
The AddComponent() method me to “Adds a component class of type componentType
to the GameObject. C# Users can use a generic version”. The Declaration reveals the syntax that needs followed but how to actually implement it? In the generic script example, and within the Start() method the variable named gameObject
uses the AddComponent
method to retrieve a <SphereCollider>();
component.
In theory can follow the same format within the case definition.
- First, I will identify
newEnemy
thenAddComponent
, identify the<SideToSideEnemy>();
component to the new enemy GameObject. - Next, I need to store the component in a variable. To do this I will type the name of my component, then give the component a name,
sideToSideEnemy
, and then set it to thenewEnemy
then have thenewEnemy
Get
theComponent<SideToSideEnemy();
// get the side to side enemy behavior
- After that, I will call a Setter Method for the
_enemyLaserPrefab
to be accessed. First, I call the variable identifying the component (i.e., Script),sideToSideEnemy
, then get the setter method from the Enemy base class,SetLaserPrefab
, then pass in the(_enemyLaserPrefab)
. Finally addbreak;
to end the case.
The Setter Method
Setter Method? What was that? Bear with me for a little longer.
After testing the first time I was running into a problem. Even though my enemies movement behavior was correct, the Enemy Laser was being instantiated but I kept receiving an error message from Console: “_enemyLaserPrefab is inaccessable due to its protection level”. After looking into it further, I understood that the _enemyLaserPrefab
access modifier, private
, within the Enemy base class was interfering with the inheritance of the _enemyLaserPrefab
by the SpawnManager().
- Private variables are only accessible within the that defines them. Subclasses and external classes can’t access them.
To solve the bug, and make the _enemyLaserPrefab
accessible I would add:
- A private reference with the
[SerializeField]
attribute to theprivate
GameObject _enemyLaserPrefab
within the SpawnManager (Script) then drag and drop the prefab into the SpawnManager’s Inspector.
- In the base class, Enemy (Script) create the
public void
method,SetLaserPrefab()
, pass in the(GameObject laserPrefab)
which represents the_enemyLaserPrefab
. This is referred to as a Setter Method. A Setter Method is a public or protected method that allows other classes to set the value of a private or protected variable, making the variable accessible.
Thereafter, I returned to the switch statement in the SpawnManager (Script) and added the SetlaserPrefab()
method (See above: AddComponent).
Finally, back into Unity editor. Run the game. When the new Enemy is Instantiated, the side to side movement completely interrupts my rhythm causing me to have to conserve ammo and reposition the Player before deciding whether or not I can take the shot. This threw off my timing leading to collisions with the Enemy and then an Enemy Laser resulting in GAME OVER.
Upcoming Story
In the upcoming story, I will be adding two additional Enemy movement behaviors, a “Circling” movement behavior and an “Angled” movement behavior. Utilizing inheritance, custom methods, and the SpawnManager, will be used again, along with new methods to create a variety of enemy movement behaviors that make gameplay more dynamic and engaging.
Additional resources surrounding Access modifiers and Inheritance can be found below.