Part 5 - Enemy Projectiles
In this walkthrough, add the ability for an Enemy
object to fire projectiles. This will be similar to how the Player
fires projectiles, but there will be some refactoring along the way.
Basic Projectile Firing
Since the concept of projectiles already exists in the game, it won't be too difficult to allow the Enemy
class to fire them. It will be very similar to the basic Player
projectile firing, with some different values. Open up the Enemy.cs file to get started.
- Find the
Update
method on theEnemy
class - At the bottom of the
Update
method, create a newVector2
object namedprojectilePosition
- Set the
X
value ofprojectilePosition
to be the currentX
position- This will make the projectile appear on the left side of the
Enemy
sprite
- This will make the projectile appear on the left side of the
- Set the
Y
value of theprojectilePosition
to be the currentY
position, plus half of the sprite height- This will make the projectile appear near the vertical middle of the
Enemy
sprite
- This will make the projectile appear near the vertical middle of the
- Under that, create another
Vector2
object namedprojectileVelocity
- Set the
X
value ofprojectileVelocity
to-5.0f
to make it move to the left - Set the
Y
value ofprojectileVelocity
to0.0f
to keep it vertically constant - Call
root.FireProjectile
and pass in the position and velocity to fire the projectile!
The code should look something like this:
Vector2 projectilePosition = new Vector2(position.X, position.Y + SpriteHeight / 2);
Vector2 projectileVelocity = new Vector2(-5.0f, 0.0f);
root.FireProjectile(projectilePosition, projectileVelocity);
Run the program to see what happens. The enemy can fire projectiles, but the problem is that they fire constantly! This is the same problem that occurred with the player's projectiles. It would be possible to simply copy the solution from the Player
class, but there is actually a better way.
Defining the Timer
Class
Since both the Player
and the Enemy
need a cool down period, it would make sense to create a separate utility that both classes can use: a Timer
class. In fact, this type of utility could be used for many things in a game (animations, countdowns, etc)!
Setup
Start with the basic setup for the class.
- Create a new file named Timer.cs in the "src" folder
- Add
using
statements forMicrosoft.Xna.Framework
- Create a
namespace
wrapper forArcadeFlyer2D
- In the body of the
ArcadeFlyer2D
namespace, define aclass
namedTimer
using Microsoft.Xna.Framework;
namespace ArcadeFlyer2D
{
class Timer
{
}
}
Fields and Properties
The fields and properties will be based on the cool down timer fields from the Player
class:
projectileCoolDownTime
projectileTimer
projectileTimerActive
Since the Timer
could be used for more than just projectile cool downs, these can be generalized and adapted. Some of the values will need to be accessible, but some can be hidden.
- In the body of the
Timer
class, add a privatefloat
field namedtotalTime
- This will represent the total duration for the timer
- Under that, add another private
float
field namedtimer
- This will track the actual current time for the timer
- Under that, create a new property named
Active
, with an auto-implementedget
andprivate set
- This way, the classes that use the timer can check whether the timer is currently active
private float totalTime;
private float timer;
public bool Active { get; private set; }
Constructor
Define a constructor to initialize new Timer
objects.
- In the body of the
Timer
class, define a public constructor forTimer
objects - Add a
float
parameter namedtotalTime
to the constructor method signature - In the body of the constructor, set the
totalTime
field to thetotalTime
parameter value - Under that, set the
timer
field value to0.0f
- This way the timer will start at zero
- Under that, set the
Active
property tofalse
- The
Timer
object will not start until it is activated
- The
public Timer(float totalTime)
{
this.totalTime = totalTime;
this.timer = 0.0f;
this.Active = false;
}
StartTimer
Method
The Timer
class should have a way to kick off a new timer process. This will basically be copied from the Player
class.
- In the body of the
Timer
class, define a new method namedStartTimer
- It should have a
void
return type and no parameters
- It should have a
- In the body of the
StartTimer
method, set theActive
property totrue
- This means time will be ticking!
- Under that, set the
timer
field value to0.0f
- This resets the time on the timer
public void StartTimer()
{
Active = true;
timer = 0.0f;
}
Update
Method
A Timer
object should update with each new frame, incrementing the time and resetting as needed. This will basically be copied from the Player
class.
- In the body of the
Timer
class, define a new method namedUpdate
- It should have a
void
return type and take in aGameTime
parameter namedgameTime
- It should have a
- In the body of the
Update
method, create anif
statement - In the condition for the
if
statement, check theActive
property to see if the timer is currently running - In the body of the
if
statement, the timer is active, so increment thetimer
field by theTotalSeconds
- Use
(float)gameTime.ElapsedGameTime.TotalSeconds
to get the total seconds elapsed
- Use
- Under that, still in the body of the
if
statement, create a newif
statement - In the condition for the new
if
statement, check if thetimer
field value has surpassed thetotalTime
field value - In the body of the
if
statement, the timer has completed, so set theActive
property totrue
public void Update(GameTime gameTime)
{
if (Active)
{
timer += (float)gameTime.ElapsedGameTime.TotalSeconds;
if (timer >= totalTime)
{
Active = false;
}
}
}
Timer
Code
At the end of this section, the code in the Timer.cs file should look something like this:
using Microsoft.Xna.Framework;
namespace ArcadeFlyer2D
{
class Timer
{
private float totalTime;
private float timer;
public bool Active { get; private set; }
public Timer(float totalTime)
{
this.totalTime = totalTime;
this.timer = 0.0f;
this.Active = false;
}
public void StartTimer()
{
Active = true;
timer = 0.0f;
}
public void Update(GameTime gameTime)
{
if (Active)
{
timer += (float)gameTime.ElapsedGameTime.TotalSeconds;
if (timer >= totalTime)
{
Active = false;
}
}
}
}
}
Using the Timer
Class
Now that the Timer
class has been created, it's time to use it!
Refactoring the Player
Class
First, refactor the Player
class to use a Timer
object instead of its own fields. Open up the Player.cs file to begin.
- In the body of the
Player
class, remove theprojectileCoolDownTime
,projectileTimer
, andprojectileTimerActive
fields - In place of the removed fields, add a new private
Timer
field namedprojectileCoolDown
- In the
Player
constructor, set theprojectileCoolDown
field to a newTimer
object, passing in0.5f
to theTimer
constructor - In the body of the
HandleInput
method, find theif
statement that checks if a projectile should be fired - Fix the condition of the
if
statement so that it usesprojectileCoolDown.Active
instead ofprojectileTimerActive
- At the bottom of the body of that
if
statement, remove theprojectileTimerActive
andprojectileTimer
field setting - In place of the removed statements, kick off a new timer with
projectileCoolDown.StartTimer()
- In the
Update
method, remove the entirety of theif (projectileTimerActive)
statement - Replace that
if
statement with an update to theTimer
object:projectileCoolDown.Update(gameTime)
The Timer
class allowed quite a bit of code to be removed from the Player
class, which is great!
Updating the Enemy
Class
Next, use another Timer
object on the Enemy
class to limit the number of projectiles fired. Open up the Enemy.cs file to begin.
- In the body of the
Enemy
class, add a new privateTimer
field namedprojectileCoolDown
- In the
Enemy
constructor, set theprojectileCoolDown
field to a newTimer
object, passing in2.0f
to theTimer
constructor - In the body of the
Update
method, find the projectile firing code that creates the twoVector2
variables and calls theroot.FireProjectile
method - Wrap that code in an
if
statement - Make the condition for the
if
statement check if the cool down timer is NOT currently active:!projectileCoolDown.Active
- In the body of the
if
statement, after a projectile has been fired, kick off a new timer withprojectileCoolDown.StartTimer()
- Outside of the
if
statement, update theTimer
object withprojectileCoolDown.Update(gameTime)
Using the Timer
class made it easier to repeat the cool down functionality from the Player
class! Run the game to see how the enemy fires projectiles at a slower rate.
Different Projectile Types
Functionally, the game is working as expected. However, it would be nice to have different projectiles for the player and the enemy. Luckily, this is not too difficult!
Loading an Enemy Projectile Image Asset
To create new enemy projectiles, it will be necessary to have a new image for the them! Use the following fireball image, or any other image:
- Save a new image named EnemyFire.png in the "Content" folder
- Open up the Content.mgcb file in the MonoGame Pipeline Tool
- Click the "Add Existing Item" button
- Select the EnemyFire.png file
- Click the "Build" button
Now the "EnemyFire" asset should be loadable in the game!
Storing the Image in the ArcadeFlyerGame
Class
To make two different types of projectiles, it will be necessary to load in the new image. Open up the ArcadeFlyerGame.cs file to get started.
- In the body of the
ArcadeFlyerGame
class, add a new privateTexture2D
field namedenemyProjectileSprite
to hold the enemy projectile image:private Texture2D enemyProjectileSprite;
- In the body of the
LoadContent
method, useContent.Load<Texture2D>
to load the "EnemyFire" image asset - Store the "EnemyFire" image in the
enemyProjectileSprite
field:enemyProjectileSprite = Content.Load<Texture2D>("EnemyFire");
Updating the FireProjectile
Method Definition
Next, make the FireProjectile
method use a different image depending on the source of the projectile.
- Find the
FireProjectile
method in theArcadeFlyerGame
class - Add a
string
parameter namedprojectileType
to the method signature - In the body of the
FireProjectile
method, declare (but do not set) a newTexture2D
variable namedprojectileImage
- This will store the proper image for the projectile based on the type
- Create an
if
/else
statement under that - Set the condition of the
if
statement to check if theprojectileType
parameter is equal to"Player"
- In the body of the
if
statement, set theprojectileImage
variable toplayerProjectileSprite
- In the body of the
else
, set theprojectileImage
variable toenemyProjectileSprite
- Update the call to the
Projectile
constructor so that it passes inprojectileImage
for the image
public void FireProjectile(Vector2 position, Vector2 velocity, string projectileType)
{
Texture2D projectileImage;
if (projectileType == "Player")
{
projectileImage = playerProjectileSprite;
}
else
{
projectileImage = enemyProjectileSprite;
}
Projectile firedProjectile = new Projectile(position, velocity, projectileImage);
projectiles.Add(firedProjectile);
}
Updating the FireProjectile
Method Calls
At this point, the calls to FireProjectile
are broken in both the Player
class and the Enemy
class. They need to pass in an extra parameter!
- In the Player.cs file, find the call to the
FireProjectile
method - Add a third argument to the method call:
"Player"
- In the Enemy.cs file, find the call to the
FireProjectile
method - Add a third argument to the method call:
"Enemy"
Run the program, and the different types of projectiles should appear!
The ProjectileType
Enumeration
The current code works, but there is one slight improvement to be made. Currently, since FireProjectile
takes in a string
for the projectile type, any text value could be passed. However, not every text value would make sense as a projectile type; there are only "Player" projectiles and "Enemy" projectiles. One way to constrain the possible values passed is to use an enumeration.
In C#, enumerations are value types that are defined by a set of named constants with symbolic meaning. They are quite useful because they can ensure that variables respect any constraints that are necessary. Create a ProjectileType
enumeration to use in the FireProjectile
method.
- Make a new file named ProjectileType.cs in the "src" folder
- In the new file, add a
namespace
wrapper forArcadeFlyer2D
- Within the body of the
namespace
, define a newenum
namedProjectileType
- In the body of the
ProjectileType
enumeration, addPlayer
andEnemy
, separated by a comma (,
)
namespace ArcadeFlyer2D
{
enum ProjectileType
{
Player,
Enemy
}
}
Updating the FireProjectile
Code
Now that the ProjectileType
enumeration exists, there is no need to use string
values to represent a type of projectile! Update the FireProjectile
code to use ProjectileType
instead of string
.
- In the ArcadeFlyerGame.cs file, find the
FireProjectile
method - In the method signature, change the type of the
projectileType
parameter fromstring
toProjectileType
- In the body of the
FireProjectile
method, find theif
statement checking if it is a player type - Update the
if
condition so that it checks againstProjectileType.Player
instead of"Player"
- In the Player.cs, find the
Update
method - In the body of the
Update
method, update theroot.FireProjectile
call to pass inProjectileType.Player
instead of"Player"
- In the Enemy.cs, find the
Update
method - In the body of the
Update
method, update theroot.FireProjectile
call to pass inProjectileType.Enemy
instead of"Enemy"
Run the code again, and make sure everything works the same as it did before!
Final Code
The final code for this walkthrough is available on GitHub.