I got some requests about writing a tutorial on particle systems. Particle systems are fun to write in a way that you can always extend them, in various dimensions. I'll cover some basics here (and you might even disagree about the basics!)
Let's start with some basic code. This tutorial is written on top of my 2d opengl basecode, but the code should be easy enough to follow even if you're not planning to use the library.
glClearColor(0,0,0,1.0);
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
char temp[128];
sprintf(temp, "Particles:%d", count);
fn.drawstring(temp, 0, 0);
drawrect(160, 120, 5, 5, 0xffffffff);
SDL_Delay(10);
SDL_GL_SwapBuffers();
The above code clears the framebuffer, draws a string on the top corner and draws a 5x5 white box about at the center of our 320x240 framebuffer:
Since this isn't too exciting, let's define our particle class.
class particle
{
public:
float x;
float y;
float xi;
float yi;
int life;
};
particle particles[100];
The x and y are the current x and y position, xi and yi represent the motion vector, and 'life' is a counter which says how long the particle will be alive. If 'alive' is zero, we won't render it. That's the plan anyway.
Let's initialize the particles to be at the center of the screen and to move in a random direction horizontally and vertically. We don't really care about life yet:
int i;
for (i = 0; i < 100; i++)
{
particles[i].x = 160;
particles[i].y = 120;
particles[i].xi = gPhysicsRand.genrand_real1() - 0.5;
particles[i].yi = gPhysicsRand.genrand_real1() - 0.5;
particles[i].life = 1000;
count++;
}
In our "physics" code, we'll go through the particle array and move all of the particles that are still alive. We reduce the life by one; don't worry about that yet.
int i;
for (i = 0; i < 100; i++)
{
if (particles[i].life)
{
particles[i].x += particles[i].xi;
particles[i].y += particles[i].yi;
particles[i].life--;
}
}
We'll render all the alive particles as 5x5 white blocks:
for (i = 0; i < 100; i++)
{
if (particles[i].life)
{
drawrect(particles[i].x,particles[i].y,5,5,0xffffffff);
}
}
The result looks something like this.
Now, this still isn't too exciting, so let's add gravity. The physics block gets modified like this: (the 0.025 value is just something I figured out experimentally. Your mileage may vary).
for (i = 0; i < 100; i++)
{
if (particles[i].life)
{
particles[i].x += particles[i].xi;
particles[i].y += particles[i].yi;
particles[i].life--;
particles[i].yi += 0.025;
}
}
This little change causes the particles to fall down:
Since it's rather boring for the particles to just fall off the screen, let's make them bounce a bit:
int i;
for (i = 0; i < 100; i++)
{
if (particles[i].life)
{
particles[i].x += particles[i].xi;
particles[i].y += particles[i].yi;
particles[i].life--;
particles[i].yi+=0.025;
if (particles[i].y > 240)
{
particles[i].yi = -particles[i].yi * 0.5;
}
}
}
The result looks like this:
So, we have a hundred dots that have some physics properties. This doesn't sound too useful as a particle system yet. Let's put that life into use. The initialization changes to a simple clearing of life to zero:
int i;
for (i = 0; i < 100; i++)
{
particles[i].life = 0;
}
And we'll add a new function that spawns a single particle.
void spawn()
{
int i = gPhysicsRand.genrand_int31() % 100;
if (particles[i].life)
return;
particles[i].x = 160;
particles[i].y = 120;
particles[i].xi = gPhysicsRand.genrand_real1() - 0.5;
particles[i].yi = gPhysicsRand.genrand_real1() - 0.5;
particles[i].life = 1000;
count++;
}
Here's a little trick that I've used; instead of making sure all particles we ask for exist, just pick a random slot in the array, and if it's unused, fill it. We're spawning so many particles that missing a few doesn't really matter. You could, instead of this, keep a linked list of particles or search for the first free slot, but both of those have performance overheads, and just having an array of particles will keep the particle count in a sane range.
Of course the negative side (when compared to linked lists) is that you still have to scan the whole particle array, but I think it's a good tradeoff.
I'm naturally just using particles for the visual effect; if your particles have some actual effect, like causing damage or some such, you probably care more.
Anyway, adding those and calling spawn once a physics tick will result with something like this:
Now, if your particles live forever, you're going to run out sooner or later. That's what the life counter is for.
Another trick I've learned: if the life span of the particles is always the same, you may start seeing some animation artefacts. As more particles are spawned, the number of free slots grows smaller until lots of particles start dying, and then it will burst a bunch of particles all of a sudden. To fix this, simply make the particle lifespans somewhat random:
void spawn()
{
int i = gPhysicsRand.genrand_int31() % 100;
if (particles[i].life)
return;
particles[i].x = 160;
particles[i].y = 120;
particles[i].xi = gPhysicsRand.genrand_real1() - 0.5;
particles[i].yi = gPhysicsRand.genrand_real1() - 0.5;
particles[i].life = gPhysicsRand.genrand_int31() % 50 + 100;
count++;
}
You can (and probably should) animate the lifespan of the particles. In here we're shrinking the particles from 11x11 to 1x1 blocks:
for (i = 0; i < 100; i++)
{
if (particles[i].life)
{
int size = (particles[i].life * 10) / 150 + 1;
drawrect(particles[i].x, particles[i].y, size, size, 0xffffffff);
}
}
Which then looks something like this:
We've barely scratched the surface so far, but you can see how things can move from here.
More things to consider:
(As always, comments are appreciated).