Making a simple 2d particle engine - Nathan Smith
Particle engines can be very useful in adding
interest to a game.
The two effects I will look at in this tutorial is rain and snow,
but the particle engine should be flexible enough to add other nice
effects.
First what is a particle? A particle is single small object that
can be modelled to behave in a certain way, examples include a drop
of rain, a snow flake, or even a falling leaf.
Start the particle structure with a x,y for its position on the screen,
then add a dx,dy (delta x, delta y) these are the amount the particle
moves each logic update. I have a rx,ry for the amount of randomness
in the particles movement. For the rain we need to leave a trail so
I like to include a tx,ty for the end point of the trail. We also need
a color variable. And finally the life variable, this is a counter that
counts how many frames the particle will live for.
However that is just one particle how do we store a large number
of them? The best way is to use a unordered linked list, but you can
also use a simple array if you know for certain that there will never
be over a certain number of particle alive at any given time.
typedef struct particleThis structure will store all the information we need for both the rain and the snow effect. However the first step is to create a particle:
{
double x,y, // location of the particle on the screen
dx,dy, // speed and direction of movement
rx,ry, // the amount of randomness in the movement
tx,ty; // the location this particle was last at.
int color, // the particle's color
type, // the drawing type of the particle
life; // This is a counter!
// When the counter hits zero we remove the particle.
struct particle *next; // a link to the next particle.
} particle;
#define RAIN_PARTICLE 0 // here are just some types for the drawing types :)
#define SNOW_PARTICLE1 1
#define SNOW_PARTICLE2 2
struct particle *create_particle(struct particle *parent)The basic idea is we will end up with a chain of particles something like this:
{
struct particle *tmp;
tmp = (struct particle *)malloc(sizeof(struct particle));
tmp->next = parent;
return tmp;
}
particle 1 -> particle 2 -> particle 3 -> ... -> particle n -> null
With the next pointer in the structure linking to the next particle in the chain.
The function can be used like this:
lastparticle = create_particle(lastparticle);And this will add a new particle on to the linked list in front of the last particle. However this function only allocates the memory for our particle. It doesn't define any information about it.
Also remember that because this function allocates memory, this memory must be freed once we are finished with it or the program may run out of memory and it might crash.
In order to have proper rain we will add a new function that will define all the information we need on a brand new particle of rain.
struct particle *create_rain_drop(struct particle *parent,double direction)As you can see it just sets up all the of fields in a newly created structure and returns it. In fact it can be used in the same way as the create_particle function. So in many ways this can be looked at, as an extention of the create_particle function.
{
struct particle *tmp;
double r;
tmp = create_particle (parent); // We allocate the memory for the particle here.
// place the rain in a random place on the screen
tmp->x = float(rand()%(SCREEN_W+40))-20;
tmp->y = float(rand()%SCREEN_H)-60;
// decide speed of the particle
r = ((double)(rand()%30))/10+20; // good rain needs to be fast! ;)
// set the speed & direction
tmp->dx = r*sin(direction);
tmp->dy = -r*cos(direction);
// rx & ry should be 0 since we don't want the rain to wobble.
tmp->rx = 0;
tmp->ry = 0;
// don't forget to add life to the particle.
tmp->life = rand()%30+60;
// I like my rain grey coloured ;)
tmp->color = makecol(220,220,220);
// and finally set the drawing type
tmp->type = RAIN_PARTICLE;
return tmp;
}
To create a snow particle we the same thing but with a few changes.
struct particle *create_snow_flake(struct particle *parent)We now we have the functions for creating particles... However what happens once a particle dies or when we are done with the program? Well we need to remove it from the list of particles. Here is a function to help us do it:
{
struct particle *tmp;
double r,direction;
int c;
tmp = create_particle (parent); // once again start by creating the particle
// place the snow in a random place on the screen
tmp->x = float(rand()%(SCREEN_W+20))-20;
tmp->y = float(rand()%(SCREEN_H+20))-20;
// decide speed and direction of this particle of snow
r = float(rand()%15)/15+1; // notice the snow is much slower than the rain was
direction=rand()%64+64;
// set the speed & direction
tmp->dx = r*sin(direction);
tmp->dy = -r*cos(direction);
// snow is very light so it tends to be blown about, so we add some randomness
// to its movement.
tmp->rx = 1;
tmp->ry = 1;
// don't forget to add life to the particle.
tmp->life = rand()%30+60;
// snow should be all white.. but I made it a random shade of grey to make it look
// more... interesting.
c = rand()%128+128;
tmp->color=makecol(c,c,c);
// and finally set the drawing type
// randomly set it to either of the two flake types.
tmp->type = rand()%2+SNOW_PARTICLE1;
return tmp;
}
struct particle *clean_particles(struct particle *t,int all)Now that was a complex function, but it is as important as the function to create the new particles!
{
struct particle *tmp;
struct particle *top;
top=t;
tmp=NULL;
while(t) { // basically we are looping through all the particles
// in the list.
if(t->life<0||all) { // is the particle is dead or if we are deleting all particles
if(tmp) { // then delete it!
tmp->next=t->next;
free(t);
t=tmp->next;
}
else {
top=t->next;
free(t);
t=top;
}
}
else {
tmp=t;
t=t->next;
}
}
return top; // return the list back, possibly empty :)
}
The next step is to draw the particles on the screen!! ;) Now each type of particle must be drawn in a different way
void draw_particle(BITMAP *bmp, struct particle *t)Another way to have done the snow would be to have draw a filled circle. Which might have looked nicer, I'll leave that up to you to try. The draw_particle function only draws a single particle though, and not all of them so we will need to loop through whole list again:
{
switch(t->type) {
case RAIN_PARTICLE: // for rain just draw a simple line
line(bmp, (int)t->x, (int)t->y, (int)t->tx, (int)t->ty, t->color);
break;
case SNOW_PARTICLE1: // just draw a single pixel for a small flake of snow
putpixel(bmp, (int)t->x, (int)t->y, t->color);
break;
case SNOW_PARTICLE2:
putpixel(bmp, (int)t->x, (int)t->y, t->color);
putpixel(bmp, (int)t->x-1, (int)t->y, t->color);
putpixel(bmp, (int)t->x+1, (int)t->y, t->color);
putpixel(bmp, (int)t->x, (int)t->y-1, t->color);
putpixel(bmp, (int)t->x, (int)t->y+1, t->color);
break;
}
}
void draw_particles(BITMAP *bmp, struct particle *t)Woah that was easy eh? its fairly easy when you don't have to worry about deleting stuff ;)
{
while(t) { // once again we are just looping through all the particles
draw_particle(bmp,t);
t=t->next;
}
}
However if you stopped at this point you would find your particles wouldn't move!!! they would just be frozen. :( So we need a function that will update the particles and move them around etc...
void update_particle(struct particle *t)Some particle engines also change the color each logic update, but I have never liked that one. Notice this function also subtracts one from the particle's life counter... in the case of rain & snow.. a particle is meant to vanish when it touchs the ground.. but we just fake it.
{
// first store the old positon as the end of the trail
t->tx = t->x; // first things first! store the current position of the particle
t->ty = t->y;
// then update the position using its speed & direction
t->x += t->dx;
t->y += t->dy;
// now add any randomness
if(rx>0) t->x += rand() % (t->rx*2) - t->rx;
if(ry>0) t->y += rand() % (t->ry*2) - t->ry;
// subtract one from the particles life.
t->life--;
}
As with the draw_particle, we need a function to call update_particle on ALL the particles in a list.
void update_particles(struct particle *t)As you can see from the examples the adding of new particle types is very easy, and by having one particle engine for all your different particle effects you reduce the coding effort.
{
while(t) { // and once again we are just looping through all the particles
update_particle(t);
t=t->next;
}
}
This does have one major flaw, if you are using the weather over a 2d scrollable map, when the map is scrolled the particles are not scrolled with it. This causes an ugly effect! However by making a small change to the update_particle function....
void update_particle(struct particle *t,int scroll_x,int scroll_y)As long as the scroll amount isn't too big... it won't cause any problems.. but if the scroll_x and scroll_y suddenly jumps... it will leave gaps on the edges of the screen.
{
...
t->x += scroll_x + dx;
t->y += scroll_y + dy;
...
}
Here is an example main game loop
#include <allegro.h>well I hope someone can learn from this and hopefully add some interesting effects to their games.
...
insert the particle stuff here or in a header or something ;)
...
void main()
{
particle *list = NULL; // make sure it starts by being NULL!!!
BITMAP *buffer;
allegro_init();
install_keyboard();
set_color_depth(16);
set_gfx_mode(GFX_AUTODETECT,640,480,0,0); // should really check the mode was set!
// but I'm lazy.
buffer = create_bitmap(640,480);
while(!key[KEY_ESC]) {
list=clean_particles(list,FALSE); // remove any old particles.
list=create_rain_drop(list);
update_particles(list); // update the particles and move them around!
clear(buffer); // clear the buffer and make it black
draw_particles(buffer,list); // draw them to the buffer
blit(buffer,screen,0,0,0,0,640,480); // draw the buffer to the screen.
}
list=clean_particles(list,TRUE); // free any other particles that are left.
}
Please email me at white_door@dread.nl if you need any help.
Here a few other nice particle effects that you might want to try
- Clouds
- Hail (small chunks of fast falling ice)
- Leaves (a leaf falling off a tree requires some animation to be drawn, but otherwise it can be easily done)
And here are some other effects that could work but would take a little more work
- Stars (stars that scroll at different speeds with you map would require small additions to the particle structure)
- Explosions/Fire (would require a blur function to mix the particles together, quite easy for 256 color modes but a true color blur takes more work)