Advertisement
Implementation

Make Your Game Pop With Particle Effects and Quadtrees

by

So you want explosions, fire, bullets, or magic spells in your game? Particle systems make great simple graphical effects to spice up your game a little. You can wow the player even more by making particles interact with your world, bouncing off of the environment and other players. In this tutorial we'll implement some simple particle effects, and from here we'll move on to making the particles bounce off of the world around them.

We'll also optimise things by implementing a data structure called a quadtree. Quadtrees allow you to check for collisions much more quickly than you could without one, and they are simple to implement and understand.

Note: Although this tutorial is written using HTML5 and JavaScript, you should be able to use the same techniques and concepts in almost any game development environment.

In order to see the in-article demos, be sure to read this article in Chrome, Firefox, IE 9, or any other browser that supports HTML5 and Canvas.

Notice how the particles change color as they fall, and how they bounce off the shapes.

What Is a Particle System?

A particle system is a simple way of generating effects like fire, smoke, and explosions.

You create a particle emitter, and this launches small "particles" which you can display as pixels, boxes, or small bitmaps. They follow simple Newtonian physics and change color as they move, resulting in dynamic, customizable, graphical effects.


The Start of a Particle System

Our particle system will have a few tunable parameters:

  • How many particles it spits out every second.
  • How long a particle can "live".
  • The colors each particle will transition through.
  • The position and angle the particles will spawn from.
  • How fast the particles will go when they spawn.
  • How much gravity should effect particles.

If every particle spawned exactly the same, we'd just have a stream of particles, not a particle effect. So let's also allow configurable variability. This gives us a few more parameters for our system:

  • How much their launching angle can vary.
  • How much their initial velocity can vary.
  • How much their lifetime can vary.

We end up with a particle system class which starts like this:

function ParticleSystem(params) {
	//Default parameters
	this.params = {
		//Where particles spawn from
		pos : new Point(0, 0),
		
		//How many particles spawn every second
		particlesPerSecond : 100,

		//How long each particle lives (and how much this can vary)
		particleLife: 0.5,
		lifeVariation: 0.52,

		//The gradient of colors the particle will travel through
		colors: new Gradient([ new Color(255, 255, 255, 1), new Color(0, 0, 0, 0) ]),

		//The angle the particle will fire off at (and how much this can vary)
		angle: 0,
		angleVariation: Math.PI * 2,

		//The velocity range the particle will fire off at
		minVelocity : 20,
		maxVelocity : 50,

		//The gravity vector applied to each particle
		gravity: new Point(0, 30.8),

		//An object to test for collisions against, and bounce damping factor
		//for said collisions
		collider : null,
		bounceDamper: 0.5
	};

	//Override our default parameters with supplied parameters
	for (var p in params) {
		this.params[p] = params[p];
	}

	this.particles = [];
}


Making the System Flow

Every frame we need to do three things: create new particles, move existing particles, and draw the particles.

Creating Particles

Creating particles is pretty simple. If we are creating 300 particles per second, and it's been 0.05 seconds since the last frame, we create 15 particles for the frame (which averages to 300 per second).

We should have a simple loop that looks like this:

var newParticlesThisFrame = this.params.particlesPerSecond * frameTime;
for (var i = 0; i < newParticlesThisFrame; i++) {
	this.spawnParticle((1.0 + i) / newParticlesThisFrame * frameTime);
}

Our spawnParticle() function creates a new particle based upon our system's parameters:

ParticleSystem.prototype.spawnParticle = function(offset) {
	//We want to fire the particle off at a random angle and a random velocity
	//within the parameters dictated for this system
	var angle = randVariation(this.params.angle, this.params.angleVariation);
	var speed = randRange(this.params.minVelocity, this.params.maxVelocity);
	var life = randVariation(this.params.particleLife, this.params.particleLife * this.params.lifeVariation);

	//Our initial velocity will be moving at the speed we chose above in the 
	//direction of the angle we chose
	var velocity = new Point().fromPolar(angle, speed);

	//If we created every single particle at "pos", then every particle 
	//created within one frame would start at the same place.  
	//Instead, we act as if we created the particle continuously between 
	//this frame and the previous frame, by starting it at a certain offset
	//along its path.
	var pos = this.params.pos.clone().add(velocity.times(offset));

	//Contruct a new particle object from the parameters we chose
	this.particles.push(new Particle(this.params, pos, velocity, life));
};

We choose our initial velocity from a random angle and speed. We then use the fromPolar() method to create a Cartesian velocity vector from the angle/speed combination.

Basic trigonometry yields the fromPolar method:

Point.prototype.fromPolar = function(ang, rad) {
	this.x = Math.cos(ang) * rad;
	this.y = Math.sin(ang) * rad;

	return this;
};

If you need to brush up on trigonometry a little, all of the trigonometry we are using is derived from the Unit Circle.

Particle Movement

Particle movement follows basic Newtonian laws. Particles all have a velocity and position. Our velocity is acted on by the force of gravity, and our position changes proportionally to gravity. Finally we need to keep track of each particle's life, otherwise particles would never die, we'd end up having too many, and the system would grind to a halt. All of these actions occur proportionally to the time between frames.

Particle.prototype.step = function(frameTime) {
	this.velocity.add(this.params.gravity.times(frameTime));
	this.pos.add(this.velocity.times(frameTime));
	this.life -= frameTime;
};

Drawing Particles

Finally we have to draw our particles. How you implement this in your game will vary greatly on a platform to platform basis, and how advanced you want the rendering to be. This can be as simple as placing a single colored pixel, to moving a pair of triangles for each particle, drawn by a complex GPU shader.

In our case, we'll take advantage of the Canvas API to draw a small rectangle for the particle.

Particle.prototype.draw = function(ctx, frameTime) {
	//No need to draw the particle if it is out of life.
	if (this.isDead())
		return;

	//We want to travel through our gradient of colors as the particle ages
	var lifePercent = 1.0 - this.life / this.maxLife;
	var color = this.params.colors.getColor(lifePercent);
	
	//Set up the colors
	ctx.globalAlpha = color.a;
	ctx.fillStyle = color.toCanvasColor();

	//Fill in the rectangle at the particle's position
	ctx.fillRect(this.pos.x - 1, this.pos.y - 1, 3, 3);
};

Color interpolation depends on whether the platform you are using supplies a color class (or representation format), whether it provides an interpolator for you, and how you want to approach the entire issue. I wrote a small gradient class which allows easy interpolation between multiple colors, and a small color class which provides the functionality to interpolate between any two colors.

Color.prototype.interpolate = function(percent, other) {
	return new Color(
		this.r + (other.r - this.r) * percent,
		this.g + (other.g - this.g) * percent,
		this.b + (other.b - this.b) * percent,
		this.a + (other.a - this.a) * percent);
};


Gradient.prototype.getColor = function(percent) {
	//Floating point color location within the array
	var colorF = percent * (this.colors.length - 1);

	//Round down; this is the specified color in the array 
	//below our current color
	var color1 = parseInt(colorF);
	//Round up; this is the specified color in the array 
	//above our current color
	var color2 = parseInt(colorF + 1);

	//Interpolate between the two nearest colors (using above method)
	return this.colors[color1].interpolate(
			(colorF - color1) / (color2 - color1),
			this.colors[color2]
		);
};

Here's our particle system in action!

Bouncing Particles

As you can see in the demo above, now we have some basic particle effects. They lack any interaction with the environment around them, though. To make these effects part of our game world, we're going to have them bounce off of the walls around them.

To start off, the particle system will now take a collider as a parameter. It will be the collider's job to tell a particle whether it has crashed into anything. The step() method of a particle now looks like this:

Particle.prototype.step = function(frameTime) {
	//Save our last position
	var lastPos = this.pos.clone();

	//Move
	this.velocity.add(this.params.gravity.times(frameTime));
	this.pos.add(this.velocity.times(frameTime));


	//Can this particle bounce?
	if (this.params.collider) {
		
		//Check if we hit anything
		var intersect = this.params.collider.getIntersection(
					new Line(lastPos, this.pos)
				);
		if (intersect != null) {
			//If so, we reset our position, and update our velocity 
			//to reflect the collision
			this.pos = lastPos;
			this.velocity = intersect.seg.reflect(this.velocity).times(this.params.bounceDamper);
		}
	}

	this.life -= frameTime;
};

Now every time the particle moves, we ask the collider whether its path of movement has "collided" via the getIntersection() method. If so, we reset its position (so that it is not inside whatever it intersected), and reflect the velocity.

A basic "collider" implementation could look like this:

//Takes a collection of line segments representing the game world
function Collider(lines) {
	this.lines = lines;
}

//Returns any line segment intersected by "path", otherwise null
Collider.prototype.getIntersection = function(path) {
	for (var i = 0; i < this.lines.length; i++) {
		var intersection = this.lines[i].getIntersection(path);
		if (intersection)
			return intersection;	
	}

	return null;
};

Notice a problem? Every particle needs to call collider.getIntersection() and then every getIntersection call needs to check against every "wall" in the world. If you have 300 particles (kind of a low number), and 200 walls in your world (not unreasonable either), you are performing 60,000 line intersection tests! This could grind your game to a halt, especially with more particles (or more complex worlds).


Faster Collision Detection With Quadtrees

The problem with our simple collider is that it checks every wall for every particle. If our particle is in the upper-right hand quadrant of the screen, we shouldn't need to waste time checking whether it crashed into walls that are only in the bottom or left of the screen. So ideally we want to cut out any checks for intersections outside of the top-right quadrant:

Particle effects and quadtrees
We just check for collisions between the blue dot and the red lines.

That's just a quarter of the checks! Now let's go even further: if the particle is in the upper-left quadrant of the upper-right quadrant of the screen, we should only need to check those walls in the same quadrant:

Particle effects and quadtrees

Quadtrees allow you to do exactly this! Rather than test against all walls, you split walls into the quadrants and sub-quadrants they occupy, so you only need to check a few quadrants. You can easily go from 200 checks per particle to just 5 or 6.

The steps to create a quadtree are as follows:

  1. Start with a rectangle that fills the entire screen.
  2. Take the current rectangle, count how many "walls" fall inside it.
  3. If you have more than three lines (you can choose a different number), split the rectangle into four equal quadrants. Repeat Step 2 with each quadrant.
  4. After repeating Steps 2 and 3, you end up with a "tree" of rectangles, with none of the smallest rectangles containing more than three lines (or whatever you chose).
Particle effects and quadtrees
Building a quadtree. The numbers represent the number of lines within the quadrant, red being too high and needing to subdivide.

To build our quadtree we take a set of "walls" (line segments) as a parameter, and if too many are contained within our rectangle, we subdivide into smaller rectangles, and the process repeats.

QuadTree.prototype.addSegments = function(segs) {
	for (var i = 0; i < segs.length; i++) {
		if (this.rect.overlapsWithLine(segs[i])) {
			this.segs.push(segs[i]);
		}
	}

	if (this.segs.length > 3) {
		this.subdivide();
	}
};

QuadTree.prototype.subdivide = function() {
	var w2 = this.rect.w / 2,
	    h2 = this.rect.h / 2,
	    x = this.rect.x,
	    y = this.rect.y;

	this.quads.push(new QuadTree(x, y, w2, h2));
	this.quads.push(new QuadTree(x + w2, y, w2, h2));
	this.quads.push(new QuadTree(x + w2, y + h2, w2, h2));
	this.quads.push(new QuadTree(x, y + h2, w2, h2));

	for (var i = 0; i < this.quads.length; i++) {
		this.quads[i].addSegments(this.segs);
	}

	this.segs = [];
};

You can see the full QuadTree class here:

/**
 * @constructor
 */
function QuadTree(x, y, w, h) {
	this.thresh = 4;
	this.segs = [];
	this.quads = [];

	this.rect = new Rect2D(x, y, w, h);
}

QuadTree.prototype.addSegments = function(segs) {
	for (var i = 0; i < segs.length; i++) {
		if (this.rect.overlapsWithLine(segs[i])) {
			this.segs.push(segs[i]);
		}
	}

	if (this.segs.length > this.thresh) {
		this.subdivide();
	}
};

QuadTree.prototype.getIntersection = function(seg) {
	if (!this.rect.overlapsWithLine(seg))
		return null;

	for (var i = 0; i < this.segs.length; i++) {
		var s = this.segs[i];
		var inter = s.getIntersection(seg);
		if (inter) {
			var o = {};
			return s;
		}
	}

	for (var i = 0; i < this.quads.length; i++) {
		var inter = this.quads[i].getIntersection(seg);
		if (inter)
			return inter;
	}

	return null;
};

QuadTree.prototype.subdivide = function() {
	var w2 = this.rect.w / 2,
	    h2 = this.rect.h / 2,
	    x = this.rect.x,
	    y = this.rect.y;

	this.quads.push(new QuadTree(x, y, w2, h2));
	this.quads.push(new QuadTree(x + w2, y, w2, h2));
	this.quads.push(new QuadTree(x + w2, y + h2, w2, h2));
	this.quads.push(new QuadTree(x, y + h2, w2, h2));

	for (var i = 0; i < this.quads.length; i++) {
		this.quads[i].addSegments(this.segs);
	}

	this.segs = [];
};

QuadTree.prototype.display = function(ctx, mx, my, ibOnly) {

	var inBox = this.rect.containsPoint(new Point(mx, my));

	ctx.strokeStyle = inBox ? '#FF44CC' : '#000000';

	if (inBox || !ibOnly) {
		ctx.strokeRect(this.rect.x, this.rect.y, this.rect.w, this.rect.h);
		for (var i = 0; i < this.quads.length; i++) {
			this.quads[i].display(ctx, mx, my, ibOnly);
		}
	}

	if (inBox) {
		ctx.strokeStyle = '#FF0000';
		for (var i = 0 ; i < this.segs.length; i++) {
			var s = this.segs[i];
			ctx.beginPath();
			ctx.moveTo(s.a.x, s.a.y);
			ctx.lineTo(s.b.x, s.b.y);
			ctx.stroke();
		}
	}
};

Testing for intersection against a line segment is performed in a similar manner. For every rectangle we do the following:

  1. Start with the largest rectangle in the quadtree.
  2. Check whether the line segment intersects or is inside the current rectangle. If it doesn't, don't bother doing any more testing down this path.
  3. If the line segment does fall within the current rectangle or intersect it, check if the current rectangle has any child-rectangles. If it does, go back to Step 2, but using each of the child rectangles.
  4. If the current rectangle doesn't have child rectangles but it is a leaf node (that is, it only has line segments as children), test the target line segment against those line segments. If one is an intersection, return the intersection. We are done!
Particle effects and quadtrees
Searching a Quadtree. We start at the largest rectangle and search smaller and smaller ones, until finally testing individual line segments. With the quadtree, we only perform four rectangle tests and two line tests, instead of testing against all 21 line segments. The difference only grows more dramatic with larger data-sets.
QuadTree.prototype.getIntersection = function(seg) {
	if (!this.rect.overlapsWithLine(seg))
		return null;

	for (var i = 0; i < this.segs.length; i++) {
		var s = this.segs[i];
		var inter = s.getIntersection(seg);
		if (inter) {
			var o = {};
			return s;
		}
	}

	for (var i = 0; i < this.quads.length; i++) {
		var inter = this.quads[i].getIntersection(seg);
		if (inter)
			return inter;
	}

	return null;
};

Once we pass a QuadTree object to our particle system as the "collider" we get lightning-fast look-ups. Check out the interactive demo below - use your mouse to see which line segments the quadtree would need to test against!


Hover over a (sub-)quadrant to see which line segments it contains.

Food for Thought

The particle system and quadtree presented in this article are rudimentary teaching systems. Some other ideas you may want to consider when implementing these yourself:

  • You might want to hold objects besides line segments in the quadtree. How would you expand it to include circles? Squares?
  • You might want a way to retrieve individual objects (to notify them that they've been hit by a particle), while still retrieving reflectable segments.
  • The physics equations suffer from discrepancies that Euler equations build up over time with unstable frame-rates. While this won't generally matter for a particle system, why not read up on more advanced equations of motion? (Take a look at this tutorial, for instance.)
  • There are many ways you can store the list of particles in memory. An array is simplest but may not be the best choice given that particles are often removed from the system and new ones often inserted. A linked list may fit better but have poor cache locality. The best representation for particles may depend upon the framework or language you are using.










Related Posts