Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Make a Neon Vector Shooter in XNA: Basic Gameplay

$
0
0

In this series of tutorials, I’ll show you how to make a neon twin stick shooter like Geometry Wars, which we will call Shape Blaster, in XNA. The goal of these tutorials is not to leave you with an exact replica of Geometry Wars, but rather to go over the necessary elements that will allow you to create your own high-quality variant.

I encourage you to expand upon and experiment with the code given in these tutorials. We’ll cover these topics across the series:

  1. Set up the basic gameplay, creating the player’s ship and handling input, sound and music.
  2. Finish implementing the gameplay mechanics by adding enemies, handling collision detection, and tracking the player’s score and lives.
  3. Add a bloom filter, which is the effect that will give the graphics a neon glow.
  4. Add crazy, over-the-top particle effects.
  5. Add the warping background grid.

Here’s what we’ll have by the end of the series:

Warning: Loud!

And here’s what we’ll have by the end of this first part:

Warning: Loud!

The music and sound effects you can hear in these videos were created by RetroModular, and you can read about how he did so over at Audiotuts+.

The sprites are by Jacob Zinman-Jeanes, our resident Tuts+ designer. All the artwork can be found in the source file download zip.

Shape_Blaster_Sprites
The font is Nova Square, by Wojciech Kalinowski.

Let’s get started.


Overview

In this tutorial we will create a twin-stick shooter; the player will control the ship with the keyboard, the keyboard and mouse, or the two thumbsticks of a gamepad. 

We use a number of classes to accomplish this:

  • Entity: The base class for enemies, bullets, and the player’s ship. Entities can move and be drawn.
  • Bullet and PlayerShip.
  • EntityManager: Keeps track of all entities in the game and performs collision detection.
  • Input: Helps manage input from keyboard, mouse, and gamepad.
  • Art: Loads and holds references to the textures needed for the game.
  • Sound: Loads and holds references to the sounds and music.
  • MathUtil and Extensions: Contains some helpful static methods and extension methods.
  • GameRoot: Controls the main loop of the game. This is the Game1 class XNA automatically generates, renamed.

The code in this tutorial aims to be simple and easy to understand. It will not have every feature or a complicated architecture designed to support every possible need. Rather, it will do only what it needs to do. Keeping it simple will make it easier for you to understand the concepts, and then modify and expand them into your own unique game.


Entities and the Player’s Ship

Create a new XNA project. Rename the Game1 class to something more suitable. I called it GameRoot.

Now let’s start by creating a base class for our game entities.

abstract class Entity
{
	protected Texture2D image;
	// The tint of the image. This will also allow us to change the transparency.
	protected Color color = Color.White;

	public Vector2 Position, Velocity;
	public float Orientation;
	public float Radius = 20;	// used for circular collision detection
	public bool IsExpired;		// true if the entity was destroyed and should be deleted.

	public Vector2 Size
	{
		get
		{
			return image == null ? Vector2.Zero : new Vector2(image.Width, image.Height);
		}
	}

	public abstract void Update();

	public virtual void Draw(SpriteBatch spriteBatch)
	{
		spriteBatch.Draw(image, Position, null, color, Orientation, Size / 2f, 1f, 0, 0);
	}
}

All our entities (enemies, bullets and the player’s ship) have some basic properties such as an image and a position. IsExpired will be used to indicate that the entity has been destroyed and should be removed from any lists holding a reference to it.

Next we create an EntityManager to track our entities and to update and draw them.

static class EntityManager
{
	static List<Entity> entities = new List<Entity>();

	static bool isUpdating;
	static List<Entity> addedEntities = new List<Entity>();

	public static int Count { get { return entities.Count; } }

	public static void Add(Entity entity)
	{
		if (!isUpdating)
			entities.Add(entity);
		else
			addedEntities.Add(entity);
	}

	public static void Update()
	{
		isUpdating = true;

		foreach (var entity in entities)
			entity.Update();

		isUpdating = false;

		foreach (var entity in addedEntities)
			entities.Add(entity);

		addedEntities.Clear();

		// remove any expired entities.
		entities = entities.Where(x => !x.IsExpired).ToList();
	}

	public static void Draw(SpriteBatch spriteBatch)
	{
		foreach (var entity in entities)
			entity.Draw(spriteBatch);
	}
}

Remember, if you modify a list while iterating over it, you will get an exception. The above code takes care of this by queuing up any entities added during updating in a separate list, and adding them after it finishes updating the existing entities.

Making Them Visible

We will need to load some textures if we want to draw anything. We’ll make a static class to hold references to all our textures.

static class Art
{
	public static Texture2D Player { get; private set; }
	public static Texture2D Seeker { get; private set; }
	public static Texture2D Wanderer { get; private set; }
	public static Texture2D Bullet { get; private set; }
	public static Texture2D Pointer { get; private set; }
	public static void Load(ContentManager content)
	{
		Player = content.Load<Texture2D>("Player");
		Seeker = content.Load<Texture2D>("Seeker");
		Wanderer = content.Load<Texture2D>("Wanderer");
		Bullet = content.Load<Texture2D>("Bullet");
		Pointer = content.Load<Texture2D>("Pointer");
	}
}

Load the art by calling Art.Load(Content) in GameRoot.LoadContent(). Also, a number of classes will need to know the screen dimensions, so add the following properties to GameRoot:

public static GameRoot Instance { get; private set; }
public static Viewport Viewport { get { return Instance.GraphicsDevice.Viewport; } }
public static Vector2 ScreenSize { get { return new Vector2(Viewport.Width, Viewport.Height); } }

And in the GameRoot constructor, add:

Instance = this;

Now we’ll start writing the PlayerShip class.

class PlayerShip : Entity
{
	private static PlayerShip instance;
	public static PlayerShip Instance 
	{
		get
		{
			if (instance == null)
				instance = new PlayerShip();

			return instance;
		}
	}

	private PlayerShip()
	{
		image = Art.Player;
		Position = GameRoot.ScreenSize / 2;
		Radius = 10;
	}

	public override void Update()
	{
		// ship logic goes here
	}
}

We made PlayerShip a singleton, set its image, and placed it in the center of the screen.

Finally, let’s add the player ship to the EntityManager and update and draw it. Add the following code in GameRoot:

// in Initialize()
EntityManager.Add(PlayerShip.Instance);

// in Update()
EntityManager.Update();

// in Draw()
GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin(SpriteSortMode.Texture, BlendState.Additive);
EntityManager.Draw(spriteBatch);
spriteBatch.End();

We draw the sprites with additive blending, which is part of what will give them their neon look. If you run the game at this point you should see your ship in the center of the screen. However, it doesn’t yet respond to input. Let’s fix that.


Input

For movement, the player can use WASD on the keyboard, or the left thumbstick on a gamepad. For aiming, they can use the arrow keys, the right thumbstick, or the mouse. We won’t require the player to hold the mouse button to shoot because it’s uncomfortable to continuously hold the button. This leaves us with a little problem: how do we know whether the player is aiming with the mouse, keyboard, or gamepad?

We’ll use the following system: we’ll add keyboard and gamepad input together. If the player moves the mouse, we switch to mouse aiming. If the player presses the arrow keys or uses the right thumbstick, we turn off mouse aiming.

One thing to note: pushing a thumbstick forward will return a positive y value. In screen coordinates, y values increase going downwards. We want to invert the y axis on the controller so that pushing the thumbstick up will aim or move us towards the top of the screen.

We’ll make a static class to keep track of the various input devices and take care of switching between the different types of aiming.

static class Input
{
	private static KeyboardState keyboardState, lastKeyboardState;
	private static MouseState mouseState, lastMouseState;
	private static GamePadState gamepadState, lastGamepadState;

	private static bool isAimingWithMouse = false;

	public static Vector2 MousePosition { get { return new Vector2(mouseState.X, mouseState.Y); } }

	public static void Update()
	{
		lastKeyboardState = keyboardState;
		lastMouseState = mouseState;
		lastGamepadState = gamepadState;

		keyboardState = Keyboard.GetState();
		mouseState = Mouse.GetState();
		gamepadState = GamePad.GetState(PlayerIndex.One);

		// If the player pressed one of the arrow keys or is using a gamepad to aim, we want to disable mouse aiming. Otherwise,
		// if the player moves the mouse, enable mouse aiming.
		if (new[] { Keys.Left, Keys.Right, Keys.Up, Keys.Down }.Any(x => keyboardState.IsKeyDown(x)) || gamepadState.ThumbSticks.Right != Vector2.Zero)
			isAimingWithMouse = false;
		else if (MousePosition != new Vector2(lastMouseState.X, lastMouseState.Y))
			isAimingWithMouse = true;
	}

	// Checks if a key was just pressed down
	public static bool WasKeyPressed(Keys key)
	{
		return lastKeyboardState.IsKeyUp(key) && keyboardState.IsKeyDown(key);
	}

	public static bool WasButtonPressed(Buttons button)
	{
		return lastGamepadState.IsButtonUp(button) && gamepadState.IsButtonDown(button);
	}

	public static Vector2 GetMovementDirection()
	{
			
		Vector2 direction = gamepadState.ThumbSticks.Left;
		direction.Y *= -1;	// invert the y-axis

		if (keyboardState.IsKeyDown(Keys.A))
			direction.X -= 1;
		if (keyboardState.IsKeyDown(Keys.D))
			direction.X += 1;
		if (keyboardState.IsKeyDown(Keys.W))
			direction.Y -= 1;
		if (keyboardState.IsKeyDown(Keys.S))
			direction.Y += 1;

		// Clamp the length of the vector to a maximum of 1.
		if (direction.LengthSquared() > 1)
			direction.Normalize();

		return direction;
	}

	public static Vector2 GetAimDirection()
	{
		if (isAimingWithMouse)
			return GetMouseAimDirection();

		Vector2 direction = gamepadState.ThumbSticks.Right;
		direction.Y *= -1;

		if (keyboardState.IsKeyDown(Keys.Left))
			direction.X -= 1;
		if (keyboardState.IsKeyDown(Keys.Right))
			direction.X += 1;
		if (keyboardState.IsKeyDown(Keys.Up))
			direction.Y -= 1;
		if (keyboardState.IsKeyDown(Keys.Down))
			direction.Y += 1;

		// If there's no aim input, return zero. Otherwise normalize the direction to have a length of 1.
		if (direction == Vector2.Zero)
			return Vector2.Zero;
		else
			return Vector2.Normalize(direction);
	}

	private static Vector2 GetMouseAimDirection()
	{
		Vector2 direction = MousePosition - PlayerShip.Instance.Position;

		if (direction == Vector2.Zero)
			return Vector2.Zero;
		else
			return Vector2.Normalize(direction);
	}

	public static bool WasBombButtonPressed()
	{
		return WasButtonPressed(Buttons.LeftTrigger) || WasButtonPressed(Buttons.RightTrigger) || WasKeyPressed(Keys.Space);
	}
}

Call Input.Update() at the beginning of GameRoot.Update() for the input class to work.

Tip: You may notice I included a method for bombs. We won’t implement bombs now but that method is there for future use.

You may also notice in GetMovementDirection() I wrote direction.LengthSquared() > 1. Using LengthSquared() is a small performance optimization; computing the square of the length is a bit faster than computing the length itself because it avoids the relatively slow square root operation. You’ll see code using the squares of lengths or distances throughout the program. In this particular case, the performance difference is negligible, but this optimization can make a difference when used in tight loops.

Moving

We are now ready to make the ship move. Add this code to the PlayerShip.Update() method:

const float speed = 8;
Velocity = speed * Input.GetMovementDirection();
Position += Velocity;
Position = Vector2.Clamp(Position, Size / 2, GameRoot.ScreenSize - Size / 2);
			
if (Velocity.LengthSquared() > 0)
	Orientation = Velocity.ToAngle();

This will make the ship move at a speed up to eight pixels per frame, clamp its position so it can’t go off-screen, and rotate the ship to face the direction it’s moving.

ToAngle() is a simple extension method defined in our Extensions class like so:

public static float ToAngle(this Vector2 vector)
{
	return (float)Math.Atan2(vector.Y, vector.X);
}

Shooting

If you run the game now, you should be able to fly the ship around. Now let’s make it shoot.

First, we need a class for bullets.

class Bullet : Entity
{
	public Bullet(Vector2 position, Vector2 velocity)
	{
		image = Art.Bullet;
		Position = position;
		Velocity = velocity;
		Orientation = Velocity.ToAngle();
		Radius = 8;
	}

	public override void Update()
	{
		if (Velocity.LengthSquared() > 0)
			Orientation = Velocity.ToAngle();

		Position += Velocity;

		// delete bullets that go off-screen
		if (!GameRoot.Viewport.Bounds.Contains(Position.ToPoint()))
			IsExpired = true;
	}
}

We want a brief cooldown period between bullets, so add the following fields to the PlayerShip class.

const int cooldownFrames = 6;
int cooldownRemaining = 0;

Also, add the following code to PlayerShip.Update().

var aim = Input.GetAimDirection();
if (aim.LengthSquared() > 0 && cooldownRemaining <= 0)
{
	cooldownRemaining = cooldownFrames;
	float aimAngle = aim.ToAngle();
	Quaternion aimQuat = Quaternion.CreateFromYawPitchRoll(0, 0, aimAngle);

	float randomSpread = rand.NextFloat(-0.04f, 0.04f) + rand.NextFloat(-0.04f, 0.04f);
	Vector2 vel = MathUtil.FromPolar(aimAngle + randomSpread, 11f);

	Vector2 offset = Vector2.Transform(new Vector2(25, -8), aimQuat);
	EntityManager.Add(new Bullet(Position + offset, vel));

	offset = Vector2.Transform(new Vector2(25, 8), aimQuat);
	EntityManager.Add(new Bullet(Position + offset, vel));
}

if (cooldownRemaining > 0)
	cooldownRemaining--;

This code creates two bullets that travel parallel to each other. It adds a small amount of randomness to the direction. This makes the shots spread out a little bit like a machine gun. We add two random numbers together because this makes their sum more likely to be centered (around zero) and less likely to send bullets far off. We use a quaternion to rotate the initial position of the bullets in the direction they’re travelling.

We also used two new helper methods:

  • Random.NextFloat() returns a float between a minimum and maximum value.
  • MathUtil.FromPolar() creates a Vector2 from an angle and magnitude.
// in Extensions
public static float NextFloat(this Random rand, float minValue, float maxValue)
{
	return (float)rand.NextDouble() * (maxValue - minValue) + minValue;
}

// in MathUtil
public static Vector2 FromPolar(float angle, float magnitude)
{
	return magnitude * new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
}

Custom Cursor

There’s one more thing we should do now that we have the Input class. Let’s draw a custom mouse cursor to make it easier to see where the ship is aiming. In GameRoot.Draw, simply draw Art.Pointer at the mouse’s position.

spriteBatch.Begin(SpriteSortMode.Texture, BlendState.Additive);
EntityManager.Draw(spriteBatch);

// draw the custom mouse cursor
spriteBatch.Draw(Art.Pointer, Input.MousePosition, Color.White);
spriteBatch.End();

Conclusion

If you test the game now, you’ll be able to move the ship around with the WASD keys or or left thumbstick, and aim the continuous stream of bullets with the arrow keys, mouse, or right thumbstick.

In the next part, we will complete the gameplay by adding enemies and a score.


Viewing all articles
Browse latest Browse all 728

Trending Articles