Build a Space Shooter with MonoGame – 4

May 20, 2019 7:02 pm Published by Leave your thoughts

Next let’s create a new class named Entity.cs.  The player spaceship and any other enemies in the game with inherit the properties of this class.  In our new Entity class, add a using statement for Microsoft.Xna.Framework.  Every entity will store the following information: is rotatable, scale, position, source origin, destination origin, and physics body.  Add the following to initialize these fields and properties:

protected bool isRotatable = false;
public Vector2 scale = new Vector2(1.5f, 1.5f);
public Vector2 position = new Vector2(0, 0);
protected Vector2 sourceOrigin = new Vector2(0, 0);
public Vector2 destOrigin = new Vector2(0, 0);
public PhysicsBody body { get; set; }

Let’s add our constructor, then instantiate a physics body inside it.  In other words, when another class inherits this Entity class, a physics body will automatically be created for that class.  It will all make sense shortly since it can be a bit confusing. Add the following to add our constructor:

public Entity() {
	body = new PhysicsBody();
}

We will also want a way to set up the bounding box for our entities.  Add the following method:

 public void setupBoundingBox(int width, int height)
{
	body.boundingBox = new Rectangle((int)(position.X - destOrigin.X), (int)(position.Y - destOrigin.Y), (int)(width * scale.X), (int)(height * scale.Y));
}

Finally, we will need to add an Update method.  This method will be called for every entity.  Let’s add the Update method:

public void Update(GameTime gameTime)
{
	if (body != null)
	{
    	position.X += body.velocity.X;
    	position.Y += body.velocity.Y;

    	body.boundingBox = new Rectangle((int)position.X - (int)destOrigin.X, (int)position.Y - (int)destOrigin.Y, body.boundingBox.Width, body.boundingBox.Height);

	}
	else
	{
    	Console.WriteLine("[BaseEntity] body not found, skipping position updates.");
	}
}

Our Entity class is finished!  The next step is to add a new class named PhysicsBody.cs.  This class will contain no constructor or any other methods.  We will just be adding two fields: velocity, and boundingBox.  But first, we will need to add a using statement pointing to Microsoft.Xna.Framework.  After that, add the following code to specify our two fields:

public Vector2 velocity = new Vector2(0, 0);
public Rectangle boundingBox;

Boom.  We are done with the PhysicsBody class.  Next, let’s create another class called Enemy.cs.  This class will be inheriting the Entity class.  Then, any enemy spaceship class will inherit from this Enemy class.  Let’s add using Microsoft.Xna.Framework; and using Microsoft.Xna.Framework.Graphics; to our using statements. The Enemy class will hold an instance of a Texture2D, an AnimatedSprite, and a float containing the angle.

Now we can add our couple fields and property:

protected Texture2D texture;
protected AnimatedSprite sprite;
public float angle { get; set; }

Note, the next chunk of code isn’t your average constructor.  Write the following:

public Enemy(Texture2D texture, Vector2 position, Vector2 velocity) : base()
{
	this.texture = texture;
	sprite = new AnimatedSprite(this.texture, 16, 16, 10);
	scale = new Vector2(Game1.randFloat(10, 20) * 0.1f);
	sourceOrigin = new Vector2(sprite.frameWidth * 0.5f, sprite.frameHeight * 0.5f);
	destOrigin = new Vector2((sprite.frameWidth * 0.5f) * scale.X, (sprite.frameHeight * 0.5f) * scale.Y);
	this.position = position;
	body.velocity = velocity;

	setupBoundingBox(sprite.frameWidth, sprite.frameHeight);
}

Perhaps you notice the “: base()” after the ending parenthesis.  You would normally put in the requirements required by the Entity class, but there are none, so we’ll leave it empty.  We will also have to change the class declaration from:

class Enemy

To:

class Enemy : Entity

This is what tells the compiler that we want our Enemy class to extend Entity.  After our constructor, let’s add an Update method.  Every tick, we will be setting the destination origin and update the animated sprite.  Further, we will also be calling the Update method of the Entity class.  We can accomplish that by utilizing base.Update(gameTime).  Let’s add this method:

public new virtual void Update(GameTime gameTime)
{
	destOrigin = new Vector2(
    	(float)Math.Round((sprite.frameWidth * 0.5f) * scale.X),
    	(float)Math.Round((sprite.frameHeight * 0.5f) * scale.Y)
	);

	sprite.Update(gameTime);

	base.Update(gameTime);
}

We will also add a Draw method, which will be called if any enemy spaceship class extending this class does not have it’s own Draw method:

public virtual void Draw(SpriteBatch spriteBatch)
{
	if (isRotatable)
	{
    	Rectangle destRect = new Rectangle((int)position.X, (int)position.Y, (int)(sprite.frameWidth * scale.X), (int)(sprite.frameHeight * scale.Y));
    	spriteBatch.Draw(texture, destRect, sprite.sourceRect, Color.White, MathHelper.ToRadians(angle), sourceOrigin, SpriteEffects.None, 0);

	}
	else
	{
    	Rectangle destRect = new Rectangle((int)position.X, (int)position.Y, (int)(sprite.frameWidth * scale.X), (int)(sprite.frameHeight * scale.Y));
    	spriteBatch.Draw(texture, destRect, sprite.sourceRect, Color.White, MathHelper.ToRadians(angle), sourceOrigin, SpriteEffects.None, 0);
	}
}

Next, we will add our three enemy classes.  We will start with a new class named, GunShip.  In this new class, let’s make this class extend Entity by changing our class declaration to:

class GunShip : Enemy

Before we continue, let’s not forget to add two using statements like so:

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

Now let’s add three fields to store the shoot delay, the shoot tick, and whether or not the gun ship can shoot:

private int shootDelay = 60;
private int shootTick = 0;
public bool canShoot = false;

Then, let’s add the constructor, as well as provide the arguments to instantiate the Enemy class we’re inheriting:

public GunShip(Texture2D texture, Vector2 position, Vector2 velocity) : base(texture, position, velocity)
{
       	 
}

Every tick, we want to update our shoot timer and the animated sprite.  To accomplish, let’s add the following Update method:

public override void Update(GameTime gameTime)
{
	if (!canShoot)
	{
    	if (shootTick < shootDelay)
    	{
        	shootTick++;
    	}
    	else
    	{
        	canShoot = true;
    	}
	}

	sprite.Update(gameTime);

	base.Update(gameTime);
}

Finally, we will want to add a method to reset our shoot fields, once this gun ship has shot a laser.  Let’s add the following method and call it resetCanShoot:

public void resetCanShoot()
{
	canShoot = false;
	shootTick = 0;
}

Now that the GunShip class is defined, we can add a new class named ChaserShip.cs.  For the chaser ship, we want to check if the player is within a specified distance, then have the chaser ship chaser after the player.  By now, you should know what to do in order to make this class inherit Enemy. Add the following using statements before we forget:

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

To start creating the state system for our ChaserShip class, let’s define a enum with two values:

public enum States
{
	MOVE_DOWN,
	CHASE
}

After that, let’s set the current state by creating a state field:

private States state = States.MOVE_DOWN;

Let’s add two methods to set and retrieve the state property:

public void SetState(States state)
{
	this.state = state;
	isRotatable = true;
}

public States GetState()
{
	return state;
}

Now we can add our constructor, we’ll provide the arguments to the Enemy class as usual.  We will also set the angle to 0:

public ChaserShip(Texture2D texture, Vector2 position, Vector2 velocity) : base(texture, position, velocity)
{
	angle = 0;
}

We will also want to add our Update method that will call the Update method of Enemy:

public override void Update(GameTime gameTime)
{
	base.Update(gameTime);
}

The last class we are adding that inherits Enemy is CarrierShip.  Add a new class and name it CarrierShip.cs, add the two usual using statements, and make this new class inherit Enemy.  The CarrierShip really doesn’t do anything special actually.  We will just be taking arguments in and passing them to the Enemy class.  Add the following code for our constructor:

public CarrierShip(Texture2D texture, Vector2 position, Vector2 velocity) : base(texture, position, velocity)
{
       	 
}

That’s it for the CarrierShip class.  While we’re at it, let’s add a new class for enemy lasers.  Name this new class, EnemyLaser.cs.  Add the usual using statements, and make the class extend Entity.  We will need to add a field to store the texture:

private Texture2D texture;

Let’s add our constructor:

public EnemyLaser(Texture2D texture, Vector2 position, Vector2 velocity) : base()
{
	this.texture = texture;
	this.position = position;
	body.velocity = velocity;

	setupBoundingBox(this.texture.Width, this.texture.Height);
}

As I mentioned previously, Entity doesn’t accept any arguments, so we don’t need to provide any arguments within the base keyword.  We will also add an Update method, which will call the Update method of Entity:

public new void Update(GameTime gameTime)
{
        	base.Update(gameTime);
}

We will conclude writing this class by adding a Draw method:

public void Draw(SpriteBatch spriteBatch)
{
	spriteBatch.Draw(texture, position, Color.White);
}

Let’s create a new class for the player’s laser and name it PlayerLaser.cs.  Add the two using statements, then make the class inherit Entity.  Add a field for storing the texture:

private Texture2D texture;

After that we can add the constructor:

public PlayerLaser(Texture2D texture, Vector2 position, Vector2 velocity) : base()
{
	this.texture = texture;
	this.position = position;
	body.velocity = velocity;

	setupBoundingBox(this.texture.Width, this.texture.Height);
}

Then we can add our Update and Draw methods respectively:

public new void Update(GameTime gameTime)
{
	base.Update(gameTime);
}

public void Draw(SpriteBatch spriteBatch)
{
	spriteBatch.Draw(texture, position, Color.White);
}

The next class we will add is the Player class.  Add a new class and name it Player.cs.  Add the using statements, and make the class extend Entity.  This class will store a texture, an animated sprite, the movement speed, and a property storing whether or not the player is dead.

Add the following code to create these fields and properties:

public Texture2D texture;
public AnimatedSprite sprite;
public float moveSpeed = 4;
private bool _dead = false;
public bool isDead() { return _dead; }
public void setDead(bool isDead) { _dead = isDead; }

After that, let’s create the constructor:

public Player(Texture2D texture, Vector2 position) : base()
{
	this.texture = texture;
	sprite = new AnimatedSprite(this.texture, 16, 16, 10);
	this.position = position;
	sourceOrigin = new Vector2(sprite.frameWidth * 0.5f, sprite.frameHeight * 0.5f);
	destOrigin = new Vector2((sprite.frameWidth * 0.5f) * scale.X, (sprite.frameHeight * 0.5f) * scale.Y);
	setupBoundingBox(sprite.frameWidth, sprite.frameHeight);
}

Now that we added our constructor, we will need to define four methods we will use for movement:

public void MoveUp()
{
	body.velocity.Y = -moveSpeed;
}

public void MoveDown()
{
	body.velocity.Y = moveSpeed;
}

public void MoveLeft()
{
	body.velocity.X = -moveSpeed;
}

public void MoveRight()
{
	body.velocity.X = moveSpeed;
}

Next, we’ll add the Update function which will update the animated sprite, and call the base Update function of Entity:

public void Update(GameTime gameTime)
{
	sprite.Update(gameTime);

	base.Update(gameTime);
}

Finally, we will finish the Player class by adding the Draw method:

public void Draw(SpriteBatch spriteBatch)
{
	if (!_dead)
	{
    	Rectangle destRect = new Rectangle((int)position.X, (int)position.Y, (int)(sprite.frameWidth * scale.X), (int)(sprite.frameHeight * scale.Y));
    	spriteBatch.Draw(texture, destRect, sprite.sourceRect, Color.White);
	}
}

We only want to draw the player if the player is not dead.  The player instance won’t actually be destroyed, but we’ll fake it by making it invisible when it’s hit.

Next we will create a class for explosions.  Name this new class, Explosion.cs and make it extend Entity.  Also add the usual two using statements.  This class will contain three fields: texture, sprite, and origin.  Let’s add them!

private Texture2D texture;
public AnimatedSprite sprite;
public Vector2 origin;

Then let’s add the constructor:

public Explosion(Texture2D texture, Vector2 position) : base()
{
	this.texture = texture;
	sprite = new AnimatedSprite(this.texture, 32, 32, 5);
	sprite.setCanRepeat(false);
	this.position = position;
	origin = new Vector2((sprite.frameWidth * 0.5f) * scale.X, (sprite.frameHeight * 0.5f) * scale.Y);
	setupBoundingBox(sprite.frameWidth, sprite.frameHeight);
}

We can also add the Update method:

public new void Update(GameTime gameTime)
{
	sprite.Update(gameTime);

	base.Update(gameTime);
}

Let’s finally add the Draw method:

public void Draw(SpriteBatch spriteBatch)
{
	Rectangle destRect = new Rectangle((int)position.X - (int)origin.X, (int)position.Y - (int)origin.Y, (int)(sprite.frameWidth * scale.X), (int)(sprite.frameHeight * scale.Y));
	spriteBatch.Draw(texture, destRect, sprite.sourceRect, Color.White);
}

Next it’s time to go on to part five!

Tags: , ,

Categorised in:

This post was written by Jared York

Leave a Reply

Your email address will not be published. Required fields are marked *