Build a Space Shooter with MonoGame – 6

by | May 20, 2019 | Build a Space Shooter with MonoGame | 0 comments

Now that we’re done with the classes for our game object’s, let’s create one more class for our menu buttons.  Add a new class named MenuButton.cs.  In that class, add the usual two using statements.  This class does not need to extend anything. Next, add the following fields and properties:

private Game1 game;
private Vector2 position;
private Texture2D texDefault;
private Texture2D texOnDown;
private Texture2D texOnHover;
private Texture2D currentTexture;
public Rectangle boundingBox;

public bool isActive { get; set; }
public bool lastIsDown = false;
private bool _isDown = false;
private bool _isHovered = false;

Then we will add two methods for setting whether the button is pushed down or not, and whether the mouse is hovering over the button or not.  In addition we will be adding a third method to update the texture of the button:

public void SetDown(bool isDown)
{
	if (!_isDown && isDown)
	{
    	game.sndBtnDown.Play();
	}
	_isDown = isDown;

	ChangeTexture();
}
public void SetHovered(bool isHovered)
{
	if (!_isHovered && !_isDown && isHovered)
	{
    	game.sndBtnOver.Play();
	}
	_isHovered = isHovered;

	ChangeTexture();
}

private void ChangeTexture()
{
	if (_isDown)
	{
    	currentTexture = texOnDown;
	}
	else
	{
    	if (_isHovered)
    	{
        	currentTexture = texOnHover;
    	}
    	else
    	{
        	currentTexture = texDefault;
    	}
	}
}

After that, let’s add our constructor:

public MenuButton(Game1 game, Vector2 position, Texture2D texDefault, Texture2D texOnDown, Texture2D texOnHover)
{
	this.game = game;
	this.position = position;
	this.texDefault = texDefault;
	this.texOnDown = texOnDown;
	this.texOnHover = texOnHover;
	currentTexture = this.texDefault;
	boundingBox = new Rectangle((int)position.X, (int)position.Y, this.texDefault.Width, this.texDefault.Height);
}

Finally, we can conclude MenuButton.cs with the Draw method:

public void Draw(SpriteBatch spriteBatch)
{
	if (isActive)
	{
    		spriteBatch.Draw(currentTexture, position, Color.White);
	}
}

At this point, we can hop back over to Game1.cs.  In Game1.cs, right under where we set the current game state, add the following:

private KeyboardState keyState = Keyboard.GetState();

The above line is used for the movement logic.  After this line, we will want to define two menu buttons for the play button and the restart button:

private MenuButton playButton;
private MenuButton restartButton;

Then, we will add several lists for keeping track of explosions, enemies, lasers, and the like:

private List<Explosion> explosions = new List<Explosion>();
private List<Enemy> enemies = new List<Enemy>();
private List<EnemyLaser> enemyLasers = new List<EnemyLaser>();
private List<PlayerLaser> playerLasers = new List<PlayerLaser>();
private Player player = null;
private ScrollingBackground scrollingBackground;

Next, let’s add two lines for the restart timer, which will be used when the player is destroyed:

private int restartDelay = 60 * 2;
private int restartTick = 0;

After that, we need to add two more lines for the enemy spawner timer:

private int spawnEnemyDelay = 60;
private int spawnEnemyTick = 0;

Then, let’s write two more lines for the player shoot timer:

private int playerShootDelay = 15;
private int playerShootTick = 0;

In the Initialize method of Game.cs, we can set the mouse to be visible when you move it over the game window:

IsMouseVisible = true;

We can also set the width and height of the game window:

graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 640;

Finally, we have to apply the changes in order for these properties to take effect.

graphics.ApplyChanges();

Let’s take another look at the LoadContent method.  At the bottom after we load our SpriteFont, add the following few lines to instantiate our scrolling background, create our menu buttons, and change the game scene to the main menu:

scrollingBackground = new ScrollingBackground(texBgs);


playButton = new MenuButton(this, new Vector2(graphics.PreferredBackBufferWidth * 0.5f - (int)(texBtnPlay.Width * 0.5), graphics.PreferredBackBufferHeight * 0.5f), texBtnPlay, texBtnPlayDown, texBtnPlayHover);
restartButton = new MenuButton(this, new Vector2(graphics.PreferredBackBufferWidth * 0.5f - (int)(texBtnPlay.Width * 0.5), graphics.PreferredBackBufferHeight * 0.5f), texBtnRestart, texBtnRestartDown, texBtnRestartHover);


changeGameState(GameState.MainMenu);

Let’s take another look at the Update method.  Right after “TODO: Add your update logic here,” but before the switch statement, add:

keyState = Keyboard.GetState();

scrollingBackground.Update(gameTime);

Next, let’s fill in the UpdateMainMenu method.  Pretty much, all we’re doing in this method is to update the menu button state depending on the mouse state and position.  If the mouse moves over the player button, it will play the hover sound and display the hover texture. If the mouse button is pressed while the mouse is over the play button, the button down sound will play and the corresponding texture will be show.  Let’s add the following to UpdateMainMenu:

if (playButton.isActive)
{
	MouseState mouseState = Mouse.GetState();

	if (playButton.boundingBox.Contains(mouseState.Position))
	{
    	if (mouseState.LeftButton == ButtonState.Pressed)
    	{
        	playButton.SetDown(true);
        	playButton.SetHovered(false);
    	}
    	else
    	{
        	playButton.SetDown(false);
        	playButton.SetHovered(true);
    	}

    	if (mouseState.LeftButton == ButtonState.Released && playButton.lastIsDown)
    	{
        	changeGameState(GameState.Gameplay);
    	}
	}
	else
	{
    	playButton.SetDown(false);
    	playButton.SetHovered(false);
	}

	playButton.lastIsDown = mouseState.LeftButton == ButtonState.Pressed ? true : false;
}
else
{
	playButton.isActive = true;
}

I’ll give a brief rundown what we’ll be doing in the UpdateGameplay method.  If the player doesn’t exist yet (when the game starts), we create an instance of it and assign it to the player field.  At the same time we will be updating the restart timer. After that we update the player’s movement (via the keyboard checks).  Then we restrict the player position to the bounds of the screen. We also want to update all of the game object lists, as well as check for collisions.  Let’s start by the initial check testing whether the player is null or not. Add the following to the UpdateGameplay method:

if (player == null) {
	player = new Player(texPlayer, new Vector2(graphics.PreferredBackBufferWidth * 0.5f, graphics.PreferredBackBufferHeight * 0.5f));
}
else {
	player.body.velocity = new Vector2(0, 0);

if (player.isDead())
{
	if (restartTick < restartDelay)
	{
    	restartTick++;
	}
	else
	{
    	changeGameState(GameState.GameOver);
    	restartTick = 0;
	}
}
else
{
	if (keyState.IsKeyDown(Keys.W))
	{
    	player.MoveUp();
	}
	if (keyState.IsKeyDown(Keys.S))
	{
    	player.MoveDown();
	}
	if (keyState.IsKeyDown(Keys.A))
	{
    	player.MoveLeft();
	}
	if (keyState.IsKeyDown(Keys.D))
	{
    	player.MoveRight();
	}
	if (keyState.IsKeyDown(Keys.Space))
	{
    	if (playerShootTick < playerShootDelay)
    	{
        	playerShootTick++;
    	}
    	else
    	{
        	sndLaser.Play();
        	PlayerLaser laser = new PlayerLaser(texPlayerLaser, new Vector2(player.position.X + player.destOrigin.X, player.position.Y), new Vector2(0, -10));
        	playerLasers.Add(laser);
        	playerShootTick = 0;
    	}
	}
}

player.Update(gameTime);

player.position.X = MathHelper.Clamp(player.position.X, 0, graphics.PreferredBackBufferWidth - player.body.boundingBox.Width);
player.position.Y = MathHelper.Clamp(player.position.Y, 0, graphics.PreferredBackBufferHeight - player.body.boundingBox.Height);
}

After this check, we will be updating entity positions:

/**
* UPDATE ENTITY POSITIONS
**/
for (int i = 0; i < playerLasers.Count; i++)
{
	playerLasers[i].Update(gameTime);

	if (playerLasers[i].position.Y < 0)
	{
    	playerLasers.Remove(playerLasers[i]);
    	continue;
	}
}

for (int i = 0; i < enemyLasers.Count; i++)
{
	enemyLasers[i].Update(gameTime);

	if (player != null)
	{
    	if (!player.isDead())
    	{
        	if (player.body.boundingBox.Intersects(enemyLasers[i].body.boundingBox))
        	{
            	sndExplode[randInt(0, sndExplode.Count - 1)].Play();
            	Explosion explosion = new Explosion(texExplosion, new Vector2(player.position.X + player.destOrigin.X, player.position.Y + player.destOrigin.Y));
            	explosions.Add(explosion);

            	player.setDead(true);
        	}
    	}
	}

	if (enemyLasers[i].position.Y > GraphicsDevice.Viewport.Height)
	{
    	enemyLasers.Remove(enemyLasers[i]);
	}
           	 
}

for (int i = 0; i < enemies.Count; i++)
{
	enemies[i].Update(gameTime);

	if (player != null)
	{
    	if (!player.isDead())
    	{
        	if (player.body.boundingBox.Intersects(enemies[i].body.boundingBox))
        	{
            	sndExplode[randInt(0, sndExplode.Count - 1)].Play();
            	Explosion explosion = new Explosion(texExplosion, new Vector2(player.position.X + player.destOrigin.X, player.position.Y + player.destOrigin.Y));
            	explosions.Add(explosion);

            	player.setDead(true);
        	}

        	if (enemies[i].GetType() == typeof(GunShip))
        	{
            	GunShip enemy = (GunShip)enemies[i];

            	if (enemy.canShoot)
            	{
                	EnemyLaser laser = new EnemyLaser(texEnemyLaser, new Vector2(enemy.position.X, enemy.position.Y), new Vector2(0, 5));
                	enemyLasers.Add(laser);

                	enemy.resetCanShoot();
            	}
        	}
        	if (enemies[i].GetType() == typeof(ChaserShip))
        	{
            	ChaserShip enemy = (ChaserShip)enemies[i];

            	if (Vector2.Distance(enemies[i].position, player.position + player.destOrigin) < 320)
            	{
                		enemy.SetState(ChaserShip.States.CHASE);
            	}

            	if (enemy.GetState() == ChaserShip.States.CHASE)
            	{
                	Vector2 direction = (player.position + player.destOrigin) - enemy.position;
                	direction.Normalize();

                	float speed = 3;
                	enemy.body.velocity = direction * speed;

                	if (enemy.position.X + (enemy.destOrigin.X) < player.position.X + (player.destOrigin.X))
                	{
                    	enemy.angle = enemy.angle - 5;
                	}
                	else
                	{
                    	enemy.angle = enemy.angle + 5;
                	}
            	}
        	}
    		}
	}

	if (enemies[i].position.Y > GraphicsDevice.Viewport.Height)
	{
    	enemies.Remove(enemies[i]);
	}
}

for (int i = 0; i < explosions.Count; i++)
{
	explosions[i].Update(gameTime);

	if (explosions[i].sprite.isFinished())
	{
    	explosions.Remove(explosions[i]);
	}
}

We also want to add a collision check testing if each player laser has collided with an enemy:

for (int i = 0; i < playerLasers.Count; i++)
{
    bool shouldDestroyLaser = false;
    for (int j = 0; j < enemies.Count; j++)
    {
        if (playerLasers[i].body.boundingBox.Intersects(enemies[j].body.boundingBox))
        {
            sndExplode[randInt(0, sndExplode.Count - 1)].Play();

            Explosion explosion = new Explosion(texExplosion, new Vector2(enemies[j].position.X, enemies[j].position.Y));
            explosion.scale = enemies[j].scale;

            Console.WriteLine("Shot enemy.  Origin: " + enemies[j].destOrigin + ", pos: " + enemies[j].position);

            explosion.position.Y += enemies[j].body.boundingBox.Height * 0.5f;
            explosions.Add(explosion);

            enemies.Remove(enemies[j]);

            shouldDestroyLaser = true;
        }
    }

    if (shouldDestroyLaser)
    {
        playerLasers.Remove(playerLasers[i]);
    }
}

Finally, we want to add some logic for the enemy spawn timer.  We will be selecting a random enemy to spawn, then we add the instance to the enemies list.  Add the following code to conclude our UpdateGameplay method:

// Enemy spawning
if (spawnEnemyTick < spawnEnemyDelay)
{
	spawnEnemyTick++;
}
else
{
	Enemy enemy = null;
           	 
	if (randInt(0, 10) <= 3)
	{
    	Vector2 spawnPos = new Vector2(randFloat(0, graphics.PreferredBackBufferWidth), -128);
    	enemy = new GunShip(texEnemies[0], spawnPos, new Vector2(0, randFloat(1, 3)));
	}
	else if (randInt(0, 10) >= 5)
	{
    	Vector2 spawnPos = new Vector2(randFloat(0, graphics.PreferredBackBufferWidth), -128);
    	enemy = new ChaserShip(texEnemies[1], spawnPos, new Vector2(0, randFloat(1, 3)));
	}
	else
	{
    	Vector2 spawnPos = new Vector2(randFloat(0, graphics.PreferredBackBufferWidth), -128);
    	enemy = new CarrierShip(texEnemies[2], spawnPos, new Vector2(0, randFloat(1, 3)));
	}

	enemies.Add(enemy);

	spawnEnemyTick = 0;
}

Now, we can move on to the UpdateGameOver method!  This method will be very similar to the UpdateMainMenu method, but we’ll be dealing with the restart button instead of the play button.  In the UpdateGameOver method, add:

if (restartButton.isActive)
{
	MouseState mouseState = Mouse.GetState();

	if (restartButton.boundingBox.Contains(mouseState.Position))
	{
    	if (mouseState.LeftButton == ButtonState.Pressed)
    	{
        	restartButton.SetDown(true);
        	restartButton.SetHovered(false);
    	}
    	else
    	{
        	restartButton.SetDown(false);
        	restartButton.SetHovered(true);
    	}

    	if (mouseState.LeftButton == ButtonState.Released && restartButton.lastIsDown)
    	{
        	changeGameState(GameState.Gameplay);
    	}
	}
	else
	{
    	restartButton.SetDown(false);
    	restartButton.SetHovered(false);
	}

	restartButton.lastIsDown = mouseState.LeftButton == ButtonState.Pressed ? true : false;
}
else
{
	restartButton.isActive = true;
}

Next, in the resetGameplay method, we will be setting the player to not be dead, then reset the player position.  In the resetGameplay method, add the following:

if (player != null)
{
	player.setDead(false);
	player.position = new Vector2((int)(graphics.PreferredBackBufferWidth * 0.5), (int)(graphics.PreferredBackBufferHeight * 0.5));
}

Then, in the changeGameState method, we want to clear all of the lists of game objects, call resetGameplay, then change the state.  Add the following to changeGameState:

playButton.isActive = false;
restartButton.isActive = false;
explosions.Clear();
enemies.Clear();
playerLasers.Clear();
enemyLasers.Clear();
resetGameplay();

_gameState = gameState;

We have to make one quick addition to the Draw method, which is to add the draw call for the scrolling background.  Between the spriteBatch.Begin call and the switch statement, let’s add this line:

scrollingBackground.Draw(spriteBatch);

Now we can move on to the draw methods for our game states.  Let’s start with the main menu. In the DrawMainMenu method, add the following:

string title = "SPACE SHOOTER";
spriteBatch.DrawString(fontArial, title, new Vector2(graphics.PreferredBackBufferWidth * 0.5f - (fontArial.MeasureString(title).X * 0.5f), graphics.PreferredBackBufferHeight * 0.2f), Color.White);

playButton.Draw(spriteBatch);

After that, in the DrawGameplay method, add the following to draw each object in our lists of game objects:

for (int i = 0; i < enemies.Count; i++)
{
	enemies[i].Draw(spriteBatch);
}

for (int i = 0; i < playerLasers.Count; i++)
{
	playerLasers[i].Draw(spriteBatch);
}

for (int i = 0; i < enemyLasers.Count; i++)
{
	enemyLasers[i].Draw(spriteBatch);
}

for (int i = 0; i < explosions.Count; i++)
{
	explosions[i].Draw(spriteBatch);
}

if (player != null)
{
	player.Draw(spriteBatch);
}

Finally, in the DrawGameOver method, add the following to draw the elements on the game over game state:

string title = "GAME OVER";
spriteBatch.DrawString(fontArial, title, new Vector2(graphics.PreferredBackBufferWidth * 0.5f - (fontArial.MeasureString(title).X * 0.5f), graphics.PreferredBackBufferHeight * 0.2f), Color.White);

restartButton.Draw(spriteBatch);

And that concludes this course!  If you have any questions, comments, or general feedback, I’d love to hear it.  You can email me at jared.york@yorkcs.com, or tweet me at @jaredyork_.

If you found this course valuable, and would like to receive news about future tutorials and courses we release, please fill out the form.

You can find the full source code for this course on GitHub.

Subscribe To Our Newsletter

Subscribe To Our Newsletter

Join our mailing list to receive the latest news regarding tutorials and courses we publish!

Please check your inbox to confirm your subscription.

Share This