Video icon 64
Learn to Code. Start your free trial today.
Advertisement

Make a Neon Vector Shooter for iOS: 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: iOS.
Make a Neon Vector Shooter for iOS: Virtual Gamepads and Black Holes
Make a Neon Vector Shooter for iOS: The Warping Grid

In this series of tutorials, I'll show you how to make a Geometry Wars-inspired twin-stick shooter, with neon graphics, crazy particle effects, and awesome music, for iOS using C++ and OpenGL ES 2.0. In this part, we'll add explosions and visual flair.

Overview

In the series so far, we've set up the gameplay and added virtual gamepad controls. 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.

Change to Release Builds for Speed Gains

Up until now, you've probably been building and running Shape Blaster using all of the defaults debug build of the project. While this is okay and great when you're debugging your code, debugging turns off most speed and math optimizations that can be done, as well as keeping enabled all assertions in the code.

In fact, if you run the code in debug mode from here on out, you'll notice the frame rate start dropping dramatically. This is due to us targeting a device that has a reduced amount of RAM, CPU clockspeed, and smaller 3D hardware compared to a desktop computer or even a laptop.

So at this point you can optionally turn off debugging, and turn on "release" mode. Release mode gives us full compiler and math optimization, as well as removing unused debugging code and assertions.

Once you open the project, choose the Product menu, Scheme, then Edit Scheme....

menu

The following dialog window will open. Choose Run on the left side of the dialog, and from Build Configuration, change the pop-up item from debug to release.

ios-vector-shooter-build-dialog

You'll notice the speed gains immediately. The process is easily reversed if you need to debug the program again: just choose debug instead of release and you're done.

Tip: Note though that any scheme change like this requires a full recompile of the program.

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, but will still require some customization from project to project. To keep the ParticleManager as general as possible, 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 Particle class:

class Particle
{
public:
	ParticleState   mState;
	tColor4f        mColor;
	tVector2f       mPosition;
	tVector2f       mScale;
	tTexture*       mTexture;
	float           mOrientation;
	float           mDuration;
	float           mPercentLife;

public:
	Particle()
	:   mScale(1,1),
		mPercentLife(1.0f) { }
};

The Particle class has all the information required to display a particle and manage its lifetime. ParticleState 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. For this, we add the following as a nested class in ParticleManager:

class CircularParticleArray
{
protected:
	std::vector<Particle>   mList;
	size_t                  mStart;
	size_t                  mCount;

public:
	CircularParticleArray(int capacity)
	{
		mList.resize((size_t)capacity);
	}

	size_t  getStart()          { return mStart; }
	void    setStart(size_t value) { mStart = value % mList.size(); }
	size_t  getCount() { return mCount; }
	void    setCount(size_t value) { mCount = value; }
	size_t  getCapacity() { return mList.size(); }

	Particle& operator [](const size_t i)
	{
		return mList[(mStart + i) % mList.size()];
	}

	const Particle& operator [](const size_t i) const
	{
		return mList[(mStart + i) % mList.size()];
	}
};

We can set the mStart member to adjust where index zero in our CircularParticleArray corresponds to in the underlying array, and mCount 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 mStart, which essentially rotates the circular array.

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

CircularParticleArray   mParticleList;

ParticleManager::ParticleManager(int capacity)
: mParticleList(capacity)
{
}

We create mParticleList and fill it with empty particles. The constructor is the only place where 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.

void ParticleManager::createParticle(tTexture* texture, const tVector2f& position, const tColor4f& tint, float duration, const tVector2f& scale, const ParticleState& state, float theta)
{
	size_t index;

	if (mParticleList.getCount() == mParticleList.getCapacity())
	{
		index = 0;
		mParticleList.setStart(mParticleList.getStart() + 1);
	}
	else
	{
		index = mParticleList.getCount();
		mParticleList.setCount(mParticleList.getCount() + 1);
	}

	Particle& ref = mParticleList[index];

	ref.mTexture = texture;
	ref.mPosition = position;
	ref.mColor = tint;

	ref.mDuration = duration;
	ref.mPercentLife = 1.0f;
	ref.mScale = scale;
	ref.mOrientation = theta;
	ref.mState = 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 mCount 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:

void ParticleManager::update()
{
	size_t removalCount = 0;

	for (size_t i = 0; i < mParticleList.getCount(); i++)
	{
		Particle& ref = mParticleList[i];
		ref.mState.updateParticle(ref);
		ref.mPercentLife -= 1.0f / ref.mDuration;

		Swap(mParticleList, i - removalCount, i);

		if (ref.mPercentLife < 0)
		{
			removalCount++;
		}
	}

	mParticleList.setCount(mParticleList.getCount() - removalCount);
}

void ParticleManager::Swap(typename ParticleManager::CircularParticleArray& list, size_t index1, size_t index2) const
{
	Particle temp = list[index1];
	list[index1] = list[index2];
	list[index2] = temp;
}

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

void ParticleManager::draw(tSpriteBatch* spriteBatch)
{
	for (size_t i = 0; i < mParticleList.getCount(); i++)
	{
		Particle particle = mParticleList[(size_t)i];

		tPoint2f origin = particle.mTexture->getSurfaceSize() / 2;
		spriteBatch->draw(2, particle.mTexture, tPoint2f((int)particle.mPosition.x, (int)particle.mPosition.y), tOptional<tRectf>(),
						 particle.mColor,
						 particle.mOrientation, origin, particle.mScale);
	}
}

The ParticleState Class

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.

class ParticleState
{
public:
	enum ParticleType
	{
		kNone = 0,
		kEnemy,
		kBullet,
		kIgnoreGravity
	};

public:
	tVector2f       mVelocity;
	ParticleType    mType;
	float           mLengthMultiplier;

public:
	ParticleState();
	ParticleState(const tVector2f& velocity, ParticleType type, float lengthMultiplier = 1.0f);

	ParticleState   getRandom(float minVel, float maxVel);
	void            updateParticle(Particle& particle);
};

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. Let's add the following method to ParticleState:

void            ParticleState::updateParticle(Particle& particle)
{
	tVector2f vel = particle.mState.mVelocity;

	particle.mPosition += vel;
	particle.mOrientation = Extensions::toAngle(vel);

	// denormalized floats cause significant performance issues
	if (fabs(vel.x) + fabs(vel.y) < 0.00000000001f)
	{
		vel = tVector2f(0,0);
	}

	vel *= 0.97f;	// Particles gradually slow down
	particle.mState.mVelocity = vel;
}

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.

Enemy Explosions

In GameRoot, declare a new ParticleManager and call its update() and draw() methods:

// in GameRoot
protected:
	ParticleManager     mParticleManager;

public:
	ParticleManager*    getParticleManager()
	{
		return &mParticleManager;
	}
 
// in GameRoot's constructor
GameRoot::GameRoot()
:   mParticleManager(1024 * 20),
	mViewportSize(800, 600),
	mSpriteBatch(NULL)
{
}
 
// in GameRoot::onRedrawView()
mParticleManager.update();
mParticleManager.draw(mSpriteBatch);

Also, we'll declare a new instance of the tTexture class in the Art class called mLineParticle for the particle's texture. We'll load it like we do the other game's sprites:

//In Art's constructor
mLineParticle   = new tTexture(tSurface("laser.png"));

Now let's make enemies explode. We'll modify the Enemy::wasShot() method as follows:

void Enemy::wasShot()
{
	mIsExpired = true;

	for (int i = 0; i < 120; i++)
	{
		float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1, 10));
		ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kEnemy, 1);

		tColor4f color(0.56f, 0.93f, 0.56f, 1.0f);
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), mPosition, color, 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 that 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).

void            ParticleState::updateParticle(Particle& particle)
{
	tVector2f vel = particle.mState.mVelocity;

	particle.mPosition += vel;
	particle.mOrientation = Extensions::toAngle(vel);

	float speed = vel.length();
	float alpha = tMath::min(1.0f, tMath::min(particle.mPercentLife * 2, speed * 1.0f));
	alpha *= alpha;

	particle.mColor.a = alpha;

	particle.mScale.x = particle.mState.mLengthMultiplier * tMath::min(tMath::min(1.0f, 0.2f * speed + 0.1f), alpha);

	// denormalized floats cause significant performance issues
	if (fabs(vel.x) + fabs(vel.y) < 0.00000000001f)
	{
		vel = tVector2f(0,0);
	}

	vel *= 0.97f;	// Particles gradually slow down
	particle.mState.mVelocity = vel;
}

The explosions look much better now, but they are all the same color.

explosion-mono

Monochromatic explosions are a good start, but can we do better?

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.

tColor4f ColorUtil::HSVToColor(float h, float s, float v)
{
	if (h == 0 && s == 0)
	{
		return tColor4f(v, v, v, 1.0f);
	}

	float c = s * v;
	float x = c * (1 - abs(int32_t(h) % 2 - 1));
	float m = v - c;

	if (h < 1) return tColor4f(c + m, x + m, m, 1.0f);
	else if (h < 2) return tColor4f(x + m, c + m, m, 1.0f);
	else if (h < 3) return tColor4f(m, c + m, x + m, 1.0f);
	else if (h < 4) return tColor4f(m, x + m, c + m, 1.0f);
	else if (h < 5) return tColor4f(x + m, m, c + m, 1.0f);
	else return tColor4f(c + m, m, x + m, 1.0f);
}

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:

void Enemy::wasShot()
{
	mIsExpired = true;
	
	float hue1 = Extensions::nextFloat(0, 6);
	float hue2 = fmodf(hue1 + Extensions::nextFloat(0, 2), 6.0f);
	tColor4f color1 = ColorUtil::HSVToColor(hue1, 0.5f, 1);
	tColor4f color2 = ColorUtil::HSVToColor(hue2, 0.5f, 1);

	for (int i = 0; i < 120; i++)
	{
		float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1, 10));
		ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kEnemy, 1);

		tColor4f color = Extensions::colorLerp(color1, color2, Extensions::nextFloat(0, 1));
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), mPosition, 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 select randomly from 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.

Let's modify Bullet::update() as follows:

if (!tRectf(0, 0, GameRoot::getInstance()->getViewportSize()).contains(tPoint2f((int32_t)mPosition.x, (int32_t)mPosition.y)))
{
	mIsExpired = true;


	for (int i = 0; i < 30; i++)
	{
		GameRoot::getInstance()->getParticleManager()->createParticle(
																	 Art::getInstance()->getLineParticle(),
																	 mPosition,
																	 tColor4f(0.67f, 0.85f, 0.90f, 1), 50, 1,
																	 ParticleState(Extensions::nextVector2(0, 9), ParticleState::kBullet, 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, so any particles heading off-screen will be bounced back.

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

tVector2f pos = particle.mPosition;
int width = (int)GameRoot::getInstance()->getViewportSize().width;
int height = (int)GameRoot::getInstance()->getViewportSize().height;

// collide with the edges of the screen
if (pos.x < 0)
{
	vel.x = (float)fabs(vel.x);
}
else if (pos.x > width)
{
	vel.x = (float)-fabs(vel.x);
}

if (pos.y < 0)
{
	vel.y = (float)fabs(vel.y);
}
else if (pos.y > height)
{
	vel.y = (float)-fabs(vel.y);
}

Player's Ship Explosion

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

void PlayerShip::kill()
{
	PlayerStatus::getInstance()->removeLife();
	mFramesUntilRespawn = PlayerStatus::getInstance()->getIsGameOver() ? 300 : 120;

	tColor4f explosionColor = tColor4f(0.8f, 0.8f, 0.4f, 1.0f);

	for (int i = 0; i < 1200; i++)
	{
		float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1.0f, 10.0f));
		tColor4f color = Extensions::colorLerp(tColor4f(1,1,1,1), explosionColor, Extensions::nextFloat(0, 1));
		ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kNone, 1);

		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), mPosition, 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 ParticleState::kNone.

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(). Let's add the following lines:

if (particle.mState.mType != kIgnoreGravity)
{
	for (std::list<BlackHole*>::iterator j = EntityManager::getInstance()->mBlackHoles.begin(); j != EntityManager::getInstance()->mBlackHoles.end(); j++)
	{
		tVector2f dPos = (*j)->getPosition() - pos;
		float distance = dPos.length();
		tVector2f n = dPos / distance;
		vel += 10000.0f * n / (distance * distance + 10000.0f);

		// add tangential acceleration for nearby particles
		if (distance < 400)
		{
			vel += 45.0f * tVector2f(n.y, -n.x) / (distance + 100.0f);
		}
	}
}

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:
    1. It makes the particles spiral clockwise in towards the black hole.
    2. 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 it 90° counter-clockwise, take (-V.Y, V.X).

Producing Particles

A black hole will produce two types of particles. First, it will periodically spray out particles that will orbit around it. 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 = fmodf(3.0f / 1000.0f * tTimer::getTimeMS(), 6);
tColor4f color = ColorUtil::HSVToColor(hue, 0.25f, 1);
const int numParticles = 150;
float startOffset = Extensions::nextFloat(0, tMath::PI * 2.0f / numParticles);

for (int i = 0; i < numParticles; i++)
{
	tVector2f sprayVel = MathUtil::fromPolar(tMath::PI * 2.0f * i / numParticles + startOffset, Extensions::nextFloat(8, 16));
	tVector2f pos = mPosition + 2.0f * sprayVel;
	ParticleState state(sprayVel, ParticleState::kIgnoreGravity, 1.0f);

	GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), pos, color, 90, 1.5f, state);
}

This works in 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:

protected:
    int mHitPoints;
    float mSprayAngle;

BlackHole::BlackHole(const tVector2f& position)
:   mSprayAngle(0)
{
...
}

Now we'll 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 ((tTimer::getTimeMS() / 250) % 2 == 0)
{
	tVector2f sprayVel = MathUtil::fromPolar(mSprayAngle, Extensions::nextFloat(12, 15));
	tColor4f color = ColorUtil::HSVToColor(5, 0.5f, 0.8f);
	tVector2f pos = mPosition + 2.0f * tVector2f(sprayVel.y, -sprayVel.x) + Extensions::nextVector2(4, 8);
	ParticleState state(sprayVel, ParticleState::kEnemy, 1.0f);

	GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), pos, color, 190, 1.5f, state);
}

// rotate the spray direction
mSprayAngle -= tMath::PI * 2.0f / 50.0f;

This will cause the black holes to spray spurts of purple particles that will form a 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, we will have the ship emit additional particles that look like this:

glow

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

void PlayerShip::MakeExhaustFire()
{
	if (mVelocity.lengthSquared() > 0.1f)
	{
		mOrientation = Extensions::toAngle(mVelocity);

		float cosA = cosf(mOrientation);
		float sinA = sinf(mOrientation);
		tMatrix2x2f rot(tVector2f(cosA, sinA),
						tVector2f(-sinA, cosA));

		float t = tTimer::getTimeMS() / 1000.0f;

		tVector2f baseVel = Extensions::scaleTo(mVelocity, -3);

		tVector2f perpVel = tVector2f(baseVel.y, -baseVel.x) * (0.6f * (float)sinf(t * 10.0f));
		tColor4f sideColor(0.78f, 0.15f, 0.04f, 1);
		tColor4f midColor(1.0f, 0.73f, 0.12f, 1);
		tVector2f pos = mPosition + rot * tVector2f(-25, 0);	// position of the ship's exhaust pipe.
		const float alpha = 0.7f;

		// middle particle stream
		tVector2f velMid = baseVel + Extensions::nextVector2(0, 1);
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(),
																	  pos, tColor4f(1,1,1,1) * alpha, 60.0f, tVector2f(0.5f, 1),
																	  ParticleState(velMid, ParticleState::kEnemy));
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getGlow(), pos, midColor * alpha, 60.0f, tVector2f(0.5f, 1),
																	  ParticleState(velMid, ParticleState::kEnemy));

		// side particle streams
		tVector2f vel1 = baseVel + perpVel + Extensions::nextVector2(0, 0.3f);
		tVector2f vel2 = baseVel - perpVel + Extensions::nextVector2(0, 0.3f);
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(),
																	  pos, tColor4f(1,1,1,1) * alpha, 60.0f, tVector2f(0.5f, 1),
																	  ParticleState(vel1, ParticleState::kEnemy));
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(),
																	  pos, tColor4f(1,1,1,1) * alpha, 60.0f, tVector2f(0.5f, 1),
																	  ParticleState(vel2, ParticleState::kEnemy));

		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getGlow(),
																	  pos, sideColor * alpha, 60.0f, tVector2f(0.5f, 1),
																	  ParticleState(vel1, ParticleState::kEnemy));
		GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getGlow(),
																	  pos, sideColor * alpha, 60.0f, tVector2f(0.5f, 1),
																	  ParticleState(vel2, ParticleState::kEnemy));
	}
}

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 semi-transparent, 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