Advertisement

Make a Neon Vector Shooter in XNA: Particle Effects

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →
This post is part of a series called Cross-Platform Vector Shooter: XNA.
Make a Neon Vector Shooter in XNA: Bloom and Black Holes
Make a Neon Vector Shooter in XNA: The Warping Grid

In this series of tutorials, I'll show you how to make a neon twin stick shooter, like Geometry Wars, 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.


Overview

In the series so far, we've set up the gameplay and added bloom. Next up, we'll add particle effects.

Warning: Loud!

Particle effects are created by making a large number of small particles. They are very versatile and can be used to add flair to nearly any game. In Shape Blaster we will make explosions using particle effects. We will also use particle effects to create exhaust fire for the player's ship, and to add visual flair to the black holes. Plus, we'll look at how to make particles interact with the gravity from the black holes.

The ParticleManager Class

We'll start by creating a ParticleManager class that will store, update, and draw all the particles. We'll make this class general enough that it can easily be reused in other projects. To keep the ParticleManager general, it won't be responsible for how the particles look or move; we'll handle that elsewhere.

Particles tend to be created and destroyed rapidly and in large numbers. We will use an object pool to avoid creating large amounts of garbage. This means we will allocate a large number of particles up front, and then keep reusing these same particles. We will also make ParticleManager have a fixed capacity. This will simplify it and help ensure we don't exceed our performance or memory limitations by creating too many particles. When the maximum number of particles is exceeded, we will start replacing the oldest particles with new ones.

We'll make the ParticleManager a generic class. This will allow us to store custom state information for the particles without hard coding it into the ParticleManager itself. We'll also create a nested Particle class.

public class ParticleManager<T>
{
	public class Particle
	{
		public Texture2D Texture;
		public Vector2 Position;
		public float Orientation;

		public Vector2 Scale = Vector2.One;

		public Color Color;
		public float Duration;
		public float PercentLife = 1f;
		public T State;
	}
}

The Particle class has all the information required to display a particle and manage its lifetime. The generic parameter, T State, is there to hold any additional data we may need for our particles. What data is needed will vary depending on the particle effects desired; it could be used to store velocity, acceleration, rotation speed, or anything else you may need.

To help manage the particles, we'll need a class that functions as a circular array, meaning that indices that would normally be out of bounds will instead wrap around to the beginning of the array. This will make it easy to replace the oldest particles first if we run out of space for new particles in our array. We add the following as a nested class in ParticleManager.

private class CircularParticleArray
{
	private int start;
	public int Start
	{
		get { return start; }
		set { start = value % list.Length; }
	}

	public int Count { get; set; }
	public int Capacity { get { return list.Length; } }
	private Particle[] list;

	public CircularParticleArray(int capacity)
	{
		list = new Particle[capacity];
	}

	public Particle this[int i]
	{
		get { return list[(start + i) % list.Length]; }
		set { list[(start + i) % list.Length] = value; }
	}
}

We can set the Start property to adjust where index zero in our CircularParticleArray corresponds to in the underlying array, and Count will be used to track how many active particles are in the list. We will ensure that the particle at index zero is always the oldest particle. If we replace the oldest particle with a new one, we will simply increment Start, which essentially rotates the circular array.

Now that we have our helper classes, we can start filling out the ParticleManager class. We'll need some member variables, and a constructor.

// This delegate will be called for each particle.
private Action<Particle> updateParticle;
private CircularParticleArray particleList;

public ParticleManager(int capacity, Action<Particle> updateParticle)
{
	this.updateParticle = updateParticle;
	particleList = new CircularParticleArray(capacity);

	// Populate the list with empty particle objects, for reuse.
	for (int i = 0; i < capacity; i++)
		particleList[i] = new Particle();
}

The first variable declared, updateParticle, will be a custom method that updates the particles appropriately for the desired effect. A game can have multiple ParticleManagers that update differently if necessary. We also create a CircularParticleList and fill it with empty particles. The constructor is the only place the ParticleManager allocates memory.

Next we add the CreateParticle() method, which creates a new particle using the next unused particle in the pool, or the oldest particle if there are no unused particles.

public void CreateParticle(Texture2D texture, Vector2 position, Color tint, float duration, Vector2 scale, T state, float theta = 0)
{
	Particle particle;
	if (particleList.Count == particleList.Capacity)
	{
		// if the list is full, overwrite the oldest particle, and rotate the circular list
		particle = particleList[0];
		particleList.Start++;
	}
	else
	{
		particle = particleList[particleList.Count];
		particleList.Count++;
	}

	// Create the particle
	particle.Texture = texture;
	particle.Position = position;
	particle.Tint = tint;

	particle.Duration = duration;
	particle.PercentLife = 1f;
	particle.Scale = scale;
	particle.Orientation = theta;
	particle.State = state;
}

Particles may be destroyed at any time. We need to remove these particles while ensuring the other particles remain in the same order. We can do this by iterating through the list of particles while keeping track how many have been destroyed. As we go, we move each active particle in front of all the destroyed particles by swapping it with the first destroyed particle. Once all the destroyed particles are at the end of the list, we deactivate them by setting the list's Count variable to the number of active particles. Destroyed particles will remain in the underlying array, but won't be updated or drawn.

ParticleManager.Update() handles updating each particle and removing destroyed particles from the list.

public void Update()
{
	int removalCount = 0;
	for (int i = 0; i < particleList.Count; i++)
	{
		var particle = particleList[i];
		updateParticle(particle);
		particle.PercentLife -= 1f / particle.Duration;

		// sift deleted particles to the end of the list
		Swap(particleList, i - removalCount, i);

		// if the particle has expired, delete this particle
		if (particle.PercentLife < 0)
			removalCount++;
	}
	particleList.Count -= removalCount;
}

private static void Swap(CircularParticleArray list, int index1, int index2)
{
	var temp = list[index1];
	list[index1] = list[index2];
	list[index2] = temp;
}

The final thing to implement in ParticleManager is drawing the particles.

public void Draw(SpriteBatch spriteBatch)
{
	for (int i = 0; i < particleList.Count; i++)
	{
		var particle = particleList[i];

		Vector2 origin = new Vector2(particle.Texture.Width / 2, particle.Texture.Height / 2);
		spriteBatch.Draw(particle.Texture, particle.Position, null, particle.Color, particle.Orientation, origin, particle.Scale, 0, 0);
	}
}

The ParticleState Struct

The next thing to do is make a custom class or struct to customize how the particles will look in Shape Blaster. There will be several different types of particles in Shape Blaster that behave slightly differently, so we'll start by creating an enum for the particle type. We'll also need variables for the particle's velocity and initial length.

public enum ParticleType { None, Enemy, Bullet, IgnoreGravity }

public struct ParticleState
{
	public Vector2 Velocity;
	public ParticleType Type;
	public float LengthMultiplier;
}

Now we're ready to write the particle's update method. It's a good idea to make this method fast since it might have to be called for a large number of particles.

We'll start simple. Add the following method to ParticleState.

public static void UpdateParticle(ParticleManager.Particle particle)
{
	var vel = particle.State.Velocity;

	particle.Position += vel;
	particle.Orientation = vel.ToAngle();

	// denormalized floats cause significant performance issues
	if (Math.Abs(vel.X) + Math.Abs(vel.Y) < 0.00000000001f)
		vel = Vector2.Zero;

	vel *= 0.97f;		// particles gradually slow down
	x.State.Velocity = vel;
}

Enemy Explosions

We'll come back and improve this method in a moment. First, let's create some particle effects so we can actually test out our changes. In GameRoot, declare a new ParticleManager and call its Update() and Draw() methods.

// in GameRoot
public static ParticleManager ParticleManager { get; private set; }

// in GameRoot.Initialize()
ParticleManager = new ParticleManager(1024 * 20, ParticleState.UpdateParticle);

// in GameRoot.Update()
ParticleManager.Update();

// in GameRoot.Draw()
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive);
ParticleManager.Draw();
spriteBatch.End();

Also, declare a new Texture2D called LineParticle for the particle's texture in the Art class, and load the texture like we did for the other sprites.

Now let's make enemies explode. Modify the Enemy.WasShot() method as follows.

public void WasShot()
{
	IsExpired = true;

	for (int i = 0; i < 120; i++)
	{
		float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f));
		var state = new ParticleState()
		{
			Velocity = rand.NextVector2(speed, speed),
			Type = ParticleType.Enemy,
			LengthMultiplier = 1f
		};

		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, Color.LightGreen, 190, 1.5f, state);
	}
}

This creates 120 particles that will shoot outwards with different speeds in all directions. The random speed is weighted such that particles are more likely to travel near the maximum speed. This will cause more particles to be at the edge of the explosion as it expands. The particles last 190 frames, or just over three seconds.

You can now run the game and watch enemies explode. However, there are still some improvements to be made for the particle effects.

The first issue is the particles disappear abruptly once their duration runs out. It would be nicer if they could smoothly fade out. But let's go a bit further than this and make the particles glow brighter when they are moving fast. Also, it looks nice if we lengthen fast moving particles and shorten slow moving ones.

Modify the ParticleState.UpdateParticle() method as follows (changes are highlighted).

public static void UpdateParticle(ParticleManager.Particle particle)
{
	var vel = particle.State.Velocity;

	particle.Position += vel;
	particle.Orientation = vel.ToAngle();

	float speed = vel.Length();
	float alpha = Math.Min(1, Math.Min(particle.PercentLife * 2, speed * 1f));
	alpha *= alpha;

	particle.Color.A = (byte)(255 * alpha);

	particle.Scale.X = particle.State.LengthMultiplier * Math.Min(Math.Min(1f, 0.2f * speed + 0.1f), alpha);

	if (Math.Abs(vel.X) + Math.Abs(vel.Y) < 0.00000000001f)	// denormalized floats cause significant performance issues
		vel = Vector2.Zero;

	vel *= 0.97f;		// particles gradually slow down
	x.State.Velocity = vel;
}

The explosions look much better now, but they are all the same color. We can give them more variety by choosing random colors. One method of producing random colors is to choose the red, blue and green components randomly, but this will produce a lot of dull colors and we'd like our particles to have a neon light appearance. We can have more control over our colors by specifying them in the HSV color space. HSV stands for hue, saturation, and value. We'd like to pick colors with a random hue but a fixed saturation and value. We need a helper function that can produce a color from HSV values.

static class ColorUtil
{
	public static Color HSVToColor(float h, float s, float v)
	{
		if (h == 0 && s == 0)
			return new Color(v, v, v);

		float c = s * v;
		float x = c * (1 - Math.Abs(h % 2 - 1));
		float m = v - c;

		if (h < 1) return new Color(c + m, x + m, m);
		else if (h < 2) return new Color(x + m, c + m, m);
		else if (h < 3) return new Color(m, c + m, x + m);
		else if (h < 4) return new Color(m, x + m, c + m);
		else if (h < 5) return new Color(x + m, m, c + m);
		else return new Color(c + m, m, x + m);
	}
}

Now we can modify Enemy.WasShot() to use random colors. To make the explosion color less monotonous, we'll pick two nearby key colors for each explosion and linearly interpolate between them by a random amount for each particle.

public void WasShot()
{
	IsExpired = true;

	float hue1 = rand.NextFloat(0, 6);
	float hue2 = (hue1 + rand.NextFloat(0, 2)) % 6f;
	Color color1 = ColorUtil.HSVToColor(hue1, 0.5f, 1);
	Color color2 = ColorUtil.HSVToColor(hue2, 0.5f, 1);

	for (int i = 0; i < 120; i++)
	{
		float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f));
		var state = new ParticleState()
		{
			Velocity = rand.NextVector2(speed, speed),
			Type = ParticleType.Enemy,
			LengthMultiplier = 1
		};

		Color color = Color.Lerp(color1, color2, rand.NextFloat(0, 1));
		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, color, 190, 1.5f, state);
	}
}

The explosions should look like the animation below.

Enemy Explosion

You can play around with the color generation to suit your preferences. An alternative technique that works well is to hand pick a number of color patterns for explosions and choose randomly among your pre-chosen color schemes.

Bullet Explosions

We can also make the bullets explode when they reach the edge of the screen. We'll essentially do the same thing we did for enemy explosions.

Add a static Random member to the Bullet class.

private static Random rand = new Random();

Then modify Bullet.Update() as follows.

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

	for (int i = 0; i < 30; i++)
		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, Color.LightBlue, 50, 1,
			new ParticleState() { Velocity = rand.NextVector2(0, 9), Type = ParticleType.Bullet, LengthMultiplier = 1 });

}

You may notice that giving the particles a random direction is wasteful, because at least half the particles will immediately head off-screen (more if the bullet explodes in a corner). We could do some extra work to ensure particles are only given velocities opposite to the wall they are facing. Instead, though, we'll take a cue from Geometry Wars and make all particles bounce off the walls. Any particles heading off-screen will be bounced back.

Add the following lines to ParticleState.UpdateParticle() anywhere between the first and last lines.

var pos = x.Position;
int width = (int)GameRoot.ScreenSize.X;
int height = (int)GameRoot.ScreenSize.Y;

// collide with the edges of the screen
if (pos.X < 0) 	vel.X = Math.Abs(vel.X); else if (pos.X > width)
	vel.X = -Math.Abs(vel.X);
if (pos.Y < 0) 	vel.Y = Math.Abs(vel.Y); else if (pos.Y > height)
	vel.Y = -Math.Abs(vel.Y);

Player's Ship Explosion

We'll make a really big explosion when the player is killed. Modify PlayerShip.Kill() like so:

public void Kill()
{
	framesUntilRespawn = 60;

	Color yellow = new Color(0.8f, 0.8f, 0.4f);

	for (int i = 0; i < 1200; i++)
	{
		float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f));
		Color color = Color.Lerp(Color.White, yellow, rand.NextFloat(0, 1));
		var state = new ParticleState()
		{
			Velocity = rand.NextVector2(speed, speed),
			Type = ParticleType.None,
			LengthMultiplier = 1
		};

		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, color, 190, 1.5f, state);
	}
}

This is similar to the enemy explosions, but we use more particles and always use the same color scheme. The particle type is also set to ParticleType.None.

In the demo, particles from enemy explosions slow down faster than particles from the player's ship exploding. This makes the player's explosion last a bit longer and look a bit more epic.


Black Holes Revisited

Now that we have particle effects, let's revisit the black holes and make them interact with particles.

Effect on Particles

Black holes should affect particles in addition to other entities, so we need to modify ParticleState.UpdateParticle(). Add the following lines.

if (x.State.Type != ParticleType.IgnoreGravity)
{
	foreach (var blackHole in EntityManager.BlackHoles)
	{
		var dPos = blackHole.Position - pos;
		float distance = dPos.Length();
		var n = dPos / distance;
		vel += 10000 * n / (distance * distance + 10000);

		// add tangential acceleration for nearby particles
		if (distance < 400)
			vel += 45 * new Vector2(n.Y, -n.X) / (distance + 100);
	}
}

Here, n is the unit vector pointing towards the black hole. The attractive force is a modified version of the inverse square function. The first modification is that the denominator is \(distance^2 + 10,000\). This causes the attractive force to approach a maximum value instead of tending towards infinity as the distance becomes very small. When the distance is much greater than 100 pixels, \(distance^2\) becomes much greater than 10,000. Therefore, adding 10,000 to \(distance^2\) has a very small effect, and the function approximates a normal inverse square function. However, when the distance is much smaller than 100 pixels, the distance has a small effect on the value of the denominator, and the equation becomes approximately equal to:

vel += n;

The second modification is adding a sideways component to the velocity when the particles get close enough to the black hole. This serves two purposes. First, it makes the particles spiral clockwise in towards the black hole. Second, when the particles get close enough, they will reach equilibrium and form a glowing circle around the black hole.

Tip: To rotate a vector, V, 90° clockwise, take (V.Y, -V.X). Similarly, to rotate 90° counter-clockwise, take (-V.Y, V.X).

Producing Particles

Black holes will produce two types of particles. First, they will periodically spray out particles that will orbit around them. Second, when a black hole is shot, it will spray out special particles that are not affected by its gravity.

Add the following code to the BlackHole.WasShot() method.

float hue = (float)((3 * GameRoot.GameTime.TotalGameTime.TotalSeconds) % 6);
Color color = ColorUtil.HSVToColor(hue, 0.25f, 1);
const int numParticles = 150;
float startOffset = rand.NextFloat(0, MathHelper.TwoPi / numParticles);

for (int i = 0; i < numParticles; i++)
{
	Vector2 sprayVel = MathUtil.FromPolar(MathHelper.TwoPi * i / numParticles + startOffset, rand.NextFloat(8, 16));
	Vector2 pos = Position + 2f * sprayVel;
	var state = new ParticleState()
	{
		Velocity = sprayVel,
		LengthMultiplier = 1,
		Type = ParticleType.IgnoreGravity
	};

	GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, color, 90, 1.5f, state);
}

This works mostly the same way as the other particle explosions. One difference is that we pick the hue of the color based on the total elapsed game time. If you shoot the black hole multiple times in rapid succession, you will see the hue of the explosions gradually rotate. This looks less messy than using random colors while still allowing variation.

For the orbiting particle spray, we need to add a variable to the BlackHole class to track the direction in which we are currently spraying particles.

private float sprayAngle = 0;

Now add the following to the BlackHole.Update() method.

// The black holes spray some orbiting particles. The spray toggles on and off every quarter second.
if ((GameRoot.GameTime.TotalGameTime.Milliseconds / 250) % 2 == 0)
{
	Vector2 sprayVel = MathUtil.FromPolar(sprayAngle, rand.NextFloat(12, 15));
	Color color = ColorUtil.HSVToColor(5, 0.5f, 0.8f); 	// light purple
	Vector2 pos = Position + 2f * new Vector2(sprayVel.Y, -sprayVel.X) + rand.NextVector2(4, 8);
	var state = new ParticleState()
	{
		Velocity = sprayVel,
		LengthMultiplier = 1,
		Type = ParticleType.Enemy
	};

	GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, color, 190, 1.5f, state);
}

// rotate the spray direction
sprayAngle -= MathHelper.TwoPi / 50f;

This will cause the black holes to spray spurts of purple particles that will form a cool glowing ring that orbits around the black hole, like so:



Ship Exhaust Fire

As dictated by the laws of geometric-neon physics, the player's ship propels itself by jetting a stream of fiery particles out its exhaust pipe. With our particle engine in place, this effect is easy to make and adds visual flair to the ship's movement.

As the ship moves, we create three streams of particles: a center stream that fires straight out the back of the ship, and two side streams whose angles swivel back and forth relative to the ship. The two side streams swivel in opposite directions to make a criss-crossing pattern. The side streams have a redder color, while the center stream has a hotter, yellow-white color. The animation below shows the effect.

ShapeBlaster Exhaust Fire

To make the fire glow more brightly than it would from bloom alone, we will have the ship emit additional particles that look like this:

Glow Particle

These particles will be tinted and blended with the regular particles. The code for the entire effect is shown below.

private void MakeExhaustFire()
{
	if (Velocity.LengthSquared() > 0.1f)
	{
		// set up some variables
		Orientation = Velocity.ToAngle();
		Quaternion rot = Quaternion.CreateFromYawPitchRoll(0f, 0f, Orientation);

		double t = GameRoot.GameTime.TotalGameTime.TotalSeconds;
		// The primary velocity of the particles is 3 pixels/frame in the direction opposite to which the ship is travelling.
		Vector2 baseVel = Velocity.ScaleTo(-3);
		// Calculate the sideways velocity for the two side streams. The direction is perpendicular to the ship's velocity and the
		// magnitude varies sinusoidally.
		Vector2 perpVel = new Vector2(baseVel.Y, -baseVel.X) * (0.6f * (float)Math.Sin(t * 10));
		Color sideColor = new Color(200, 38, 9);	// deep red
		Color midColor = new Color(255, 187, 30);	// orange-yellow
		Vector2 pos = Position + Vector2.Transform(new Vector2(-25, 0), rot);	// position of the ship's exhaust pipe.
		const float alpha = 0.7f;

		// middle particle stream
		Vector2 velMid = baseVel + rand.NextVector2(0, 1);
		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1),
			new ParticleState(velMid, ParticleType.Enemy));
		GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, midColor * alpha, 60f, new Vector2(0.5f, 1),
			new ParticleState(velMid, ParticleType.Enemy));

		// side particle streams
		Vector2 vel1 = baseVel + perpVel + rand.NextVector2(0, 0.3f);
		Vector2 vel2 = baseVel - perpVel + rand.NextVector2(0, 0.3f);
		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1),
			new ParticleState(vel1, ParticleType.Enemy));
		GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1),
			new ParticleState(vel2, ParticleType.Enemy));

		GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, sideColor * alpha, 60f, new Vector2(0.5f, 1),
			new ParticleState(vel1, ParticleType.Enemy));
		GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, sideColor * alpha, 60f, new Vector2(0.5f, 1),
			new ParticleState(vel2, ParticleType.Enemy));
	}
}

There's nothing sneaky going on in this code. We use a sine function to produce the swivelling effect in the side streams by varying their sideways velocity over time. For each stream, we create two overlapping particles per frame: one semitransparent-white LineParticle and a coloured glow particle behind it. Call MakeExhaustFire() at the end of PlayerShip.Update(), immediately before setting the ship's velocity to zero.


Conclusion

With all these particle effects, Shape Blaster is starting to look pretty cool. In the final part of this series, we will add one more awesome effect: the warping background grid.

Advertisement