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 (meaning you can test them right here in the browser), 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:
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:
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:
- Start with a rectangle that fills the entire screen.
- Take the current rectangle, count how many “walls” fall inside it.
- 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.
- 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).
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 = []; };
Testing for intersection against a line segment is performed in a similar manner. For every rectangle we do the following:
- Start with the largest rectangle in the quadtree.
- 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.
- 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.
- 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!
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.