Particle SystemsThis is the hot topic. Everyone is always saying, "So, does it have particle systems?" Well, particle systems can be very complex or very simple. Basically, particle systems are physics models that model small particles. They are great for explosions, vapor trails, and general light shows in your game. You have already learned a lot about physics modeling and I'm sure can create your own particle system. However, just to get you started, I'm going to show you how to create a very quick and simple system based on pixel-sized particles. Let's say that we want to use particles for explosions, and maybe vapor trails. Since a particle system is nothing more than n particles, let's just focus on the model of a single particle. What Every Particle NeedsIf you wanted, you could model collision response, momentum transfer, and all that stuff, but most particle systems have extremely simple models. The following are the general features of a garden variety particle:
When you start a particle, you will want to give it a position, initial velocity, color, and a life span at the very least. Also, the particle might be a glowing cinder, so there might be color animation involved. Additionally, you may want to have some global forces that act on all particles, like gravity and wind. You may want to have functions that create collections of particles with the desired initial conditions that you're looking for, like explosions or vapor trails. And of course, you may want to give particles the ability to bounce off objects with some physical realism. However, most of the time particles just tunnel through everything and no one cares! Designing a Particle EngineTo design a particle system you need three separate elements:
Let's begin with the data structure. I'll assume an 8-bit display since, in animation, it's easier to work with bytes than RGB colors. Also, color effects are easier to implement in 8-bit color. Converting the particle engine to 16-bit isn't bad, but lots of the effects will be lost, so I have decided to keep it 8-bit for illustrative purposes. Anyway, here's a first attempt at a single particle: // a single particle typedef struct PARTICLE_TYP { int state; // state of the particle int type; // type of particle effect float x,y; // world position of particle float xv,yv; // velocity of particle int curr_color; // the current rendering color of particle int start_color; // the start color or range effect int end_color; // the ending color of range effect int counter; // general state transition timer int max_count; // max value for counter } PARTICLE, *PARTICLE_PTR; Let's add in some globals to handle external effects such as gravity in the Y direction and wind force in the X direction. float particle_wind = 0; // assume it operates in the X direction float particle_gravity = 0; // assume it operates in the Y direction Let's define some useful constants that we might need to accomplish some of the effects: // defines for particle system #define PARTICLE_STATE_DEAD 0 #define PARTICLE_STATE_ALIVE 1 // types of particles #define PARTICLE_TYPE_FLICKER 0 #define PARTICLE_TYPE_FADE 1 // color of particle #define PARTICLE_COLOR_RED 0 #define PARTICLE_COLOR_GREEN 1 #define PARTICLE_COLOR_BLUE 2 #define PARTICLE_COLOR_WHITE 3 #define MAX_PARTICLES 128 // color ranges (based on my palette) #define COLOR_RED_START 32 #define COLOR_RED_END 47 #define COLOR_GREEN_START 96 #define COLOR_GREEN_END 111 #define COLOR_BLUE_START 144 #define COLOR_BLUE_END 159 #define COLOR_WHITE_START 16 #define COLOR_WHITE_END 31 Hopefully, you can see my thinking here. I want to have particles that are either red, green, blue, or white, so I took a palette and figured out the color indices for the ranges. If you wanted to use 16-bit color then you would have to manually interpolate RGB from the starting value to some ending value—I'll keep it simple. Also, you see that I'm counting on making two types of particles: fading and flickering. The fading particles will just fade away, but the flickering ones will flicker away, like sparks. Finally, I'm happy with our little particles, so let's create storage for them: PARTICLE particles[MAX_PARTICLES]; // the particles for the particle engine So let's start writing the functions to process each particle. The Particle Engine SoftwareWe need functions to initialize all the particles, start a particle, process all the particles, and then clean up all the particles when we're done. Let's start with the initialization functions: void Init_Reset_Particles(void) { // this function serves as both an init and reset for the particles // loop thru and reset all the particles to dead for (int index=0; index<MAX_PARTICLES; index++) { particles[index].state = PARTICLE_STATE_DEAD; particles[index].type = PARTICLE_TYPE_FADE; particles[index].x = 0; particles[index].y = 0; particles[index].xv = 0; particles[index].yv = 0; particles[index].start_color = 0; particles[index].end_color = 0; particles[index].curr_color = 0; particles[index].counter = 0; particles[index].max_count = 0; } // end if } // end Init_Reset_Particles Init_Reset_Particles() just makes all particles zeros and gets them ready for use. If you wanted to do anything special, this would be the place to do it. The next function we need is something to start a particle with a given set of initial conditions. We will worry how to arrive at the initial conditions in a moment, but for now I want to hunt for an available particle, and if found, start it up with the sent data. Here's the function to do that: void Start_Particle(int type, int color, int count, float x, float y, float xv, float yv) { // this function starts a single particle int pindex = -1; // index of particle // first find open particle for (int index=0; index < MAX_PARTICLES; index++) if (particles[index].state == PARTICLE_STATE_DEAD) { // set index pindex = index; break; } // end if // did we find one if (pindex==-1) return; // set general state info particles[pindex].state = PARTICLE_STATE_ALIVE; particles[pindex].type = type; particles[pindex].x = x; particles[pindex].y = y; particles[pindex].xv = xv; particles[pindex].yv = yv; particles[pindex].counter = 0; particles[pindex].max_count = count; // set color ranges, always the same switch(color) { case PARTICLE_COLOR_RED: { particles[pindex].start_color = COLOR_RED_START; particles[pindex].end_color = COLOR_RED_END; } break; case PARTICLE_COLOR_GREEN: { particles[pindex].start_color = COLOR_GREEN_START; particles[pindex].end_color = COLOR_GREEN_END; } break; case PARTICLE_COLOR_BLUE: { particles[pindex].start_color = COLOR_BLUE_START; particles[pindex].end_color = COLOR_BLUE_END; } break; case PARTICLE_COLOR_WHITE: { particles[pindex].start_color = COLOR_WHITE_START; particles[pindex].end_color = COLOR_WHITE_END; } break; break; } // end switch // what type of particle is being requested if (type == PARTICLE_TYPE_FLICKER) { // set current color particles[index].curr_color = RAND_RANGE(particles[index].start_color, particles[index].end_color); } // end if else { // particle is fade type // set current color particles[index].curr_color = particles[index].start_color; } // end if } // end Start_Particle NOTE There is no error detection or even a success/failure sent back. The point is that I don't care; if we can't create one teenie-weenie particle, I think I'll live. However, you might want to add more robust error handling. To start a particle at (10,20) with an initial velocity of (0, -5) (straight up), a life span of 90 frames, colored a fading green, this is what you would do: Start_Particle(PARTICLE_TYPE_FADE, // type PARTICLE_COLOR_GREEN, // color 90, // count, lifespan 10,20, // initial position 0,-5); // initial velocity Of course, the particle system has both gravity and wind that are always acting, so you can set them anytime you want and they will globally affect all particles already online as well as new ones. Thus if you want no wind force but a little gravity, you would do this: particle_gravity = 0.1; // positive is downward particle_wind = 0.0; // could be +/- Now we have to decide how to move and process the particle. Do we want to wrap them around the screen? Or, when they hit the edges, should we kill them? This depends on the type of game; 2D, 3D, scrolling, and so on. For now let's keep it simple and agree that when a particle goes off a screen edge it's terminated. In addition, the movement function should update the color animation, test if the life counter is expired, and kill particles that are off the screen. Here's the movement function that takes into consideration all that, along with the gravity and wind forces: void Process_Particles(void) { // this function moves and animates all particles for (int index=0; index<MAX_PARTICLES; index++) { // test if this particle is alive if (particles[index].state == PARTICLE_STATE_ALIVE) { // translate particle particles[index].x+=particles[index].xv; particles[index].y+=particles[index].yv; // update velocity based on gravity and wind particles[index].xv+=particle_wind; particles[index].yv+=particle_gravity; // now based on type of particle perform proper animation if (particles[index].type==PARTICLE_TYPE_FLICKER) { // simply choose a color in the color range and // assign it to the current color particles[index].curr_color = RAND_RANGE(particles[index].start_color, particles[index].end_color); // now update counter if (++particles[index].counter >= particles[index].max_count) { // kill the particle particles[index].state = PARTICLE_STATE_DEAD; } // end if } // end if else { // must be a fade, be careful! // test if it's time to update color if (++particles[index].counter >= particles[index].max_count) { // reset counter particles[index].counter = 0; // update color if (++particles[index].curr_color > particles[index].end_color) { // transition is complete, terminate particle particles[index].state = PARTICLE_STATE_DEAD; } // end if } // end if } // end else // test if the particle is off the screen? if (particles[index].x > screen_width || particles[index].x < 0 || particles[index].y > screen_height || particles[index].y < 0) { // kill it! particles[index].state = PARTICLE_STATE_DEAD; } // end if } // end if } // end for index } // end Process_Particles The function is self-explanatory—I hope. It translates the particle, applies the external forces, updates the counters and color, tests whether the particle has moved offscreen, and that's it. Next we need to draw the particles. This can be accomplished in a number of ways, but I'm assuming simple pixels and a back buffered display, so here's a function to do that: void Draw_Particles(void) { // this function draws all the particles // lock back surface DDraw_Lock_Back_Surface(); for (int index=0; index<MAX_PARTICLES; index++) { // test if particle is alive if (particles[index].state==PARTICLE_STATE_ALIVE) { // render the particle, perform world to screen transform int x = particles[index].x; int y = particles[index].y; // test for clip if (x >= screen_width || x < 0 || y >= screen_height || y < 0) continue; // draw the pixel Draw_Pixel(x,y,particles[index].curr_color, back_buffer, back_lpitch); } // end if } // end for index // unlock the secondary surface DDraw_Unlock_Back_Surface(); } // end Draw_Particles Getting exited, huh? Want to try it out, don't you? Well, we're almost done. Now we need some functions to create particle effects like explosions and vapor trails. Generating the Initial ConditionsHere's the fun part. You can go wild with your imagination. Let's start off with a vapor trail algorithm. Basically, a vapor trail is nothing more than particles that are emitted from a source positioned at (emit_x, emit_y) with slightly different life spans and starting positions. Here's a possible algorithm: // emit a particle every with a change of 1 in 10 if ((rand()%10) == 1) { Start_Particle(PARTICLE_TYPE_FADE, // type PARTICLE_COLOR_GREEN, // color RAND_RANGE(90,150), // count, lifespan emit_x+RAND_RANGE(-4,4), // initial x emit_y+RAND_RANGE(-4,4), // initial y RAND_RANGE(-2,2), // initial x velocity RAND_RANGE(-2,2)); // initial y velocity } // end if As the emitter moves, so does the emitter source (emit_x, emit_y) and therefore a vapor trail is left. If you want to get really real and give the vapor particles an even more realistic physics model you should take into consideration that the emitter could be in motion and thus any particle emitted would have final velocity = emitted velocity + emitter velocity. You would need to know the velocity of the emitter source, (call it (emit_xv, emit_yv)) and simply add it to the final particle velocity like this: // emit a particle every with a change of 1 in 10 if ((rand()%10) == 1) { Start_Particle(PARTICLE_TYPE_FADE, // type PARTICLE_COLOR_GREEN, // color RAND_RANGE(90,150), // count, lifespan emit_x+RAND_RANGE(-4,4), // initial x emit_y+RAND_RANGE(-4,4), // initial y emit_xv+RAND_RANGE(-2,2), // initial x velocity emit_yv+RAND_RANGE(-2,2)); // initial y velocity } // end if For something a little more exciting, let's model an explosion. An explosion looks something like Figure 13.43. Particles are emitted in a spherical shape in all directions. Figure 13.43. The particles of an explosion.That's easy enough to model. All we need do is start up a random number of particles from a common point with random velocities that are equally distributed in a circular radius. Then if gravity is on, the particle will fall toward Earth and either go off the screen or die out due to its individual life span. Here's the code to create a particle explosion: void Start_Particle_Explosion(int type, int color, int count, int x, int y, int xv, int yv, int num_particles) { // this function starts a particle explosion // at the given position and velocity // note the use of look up tables for sin,cos while(--num_particles >=0) { // compute random trajectory angle int ang = rand()%360; // compute random trajectory velocity float vel = 2+rand()%4; Start_Particle(type,color,count, x+RAND_RANGE(-4,4),y+RAND_RANGE(-4,4), xv+cos_look[ang]*vel, yv+sin_look[ang]*vel); } // end while } // end Start_Particle_Explosion Start_Particle_Explosion() takes the type of particle you want (PARTICLE_TYPE_ FADE, PARTICLE_TYPE_FLICKER), the color of the particles, the desired number of particles, along with the position and velocity of the source. The function then generates all the desired particles. To create other special effects, just write a function. For example, one of the coolest effects that I like in movies is the ring-shaped shock wave when a spaceship is blown away. Creating this is simple. All you need to do is modify the explosion function to start all the particles out with exactly the same velocity, but at different angles. Here's that code: void Start_Particle_Ring(int type, int color, int count, int x, int y, int xv, int yv, int num_particles) { // this function starts a particle explosion at the // given position and velocity // note the use of look up tables for sin,cos // compute random velocity on outside of loop float vel = 2+rand()%4; while(--num_particles >=0) { // compute random trajectory angle int ang = rand()%360; //start the particle Start_Particle(type,color,count, x,y, xv+cos_look[ang]*vel, yv+sin_look[ang]*vel); } // end while } // end Start_Particle_Ring Putting the Particle System TogetherYou now have everything you need to put together some cool particle effects. Just make a call in the initialize phase of your game to Init_Reset_Particles(), then in the main loop make a call to Process_Particles(). Each cycle and the engine will do the rest. Of course, you have to call one of the generator functions to create some particles! Lastly, if you want to improve the system you might want to add better memory management so you can have infinite particles, and you might want to add particle-to-particle collision detection and particle-to-environment collision detection—that would be really cool. As a demo of using the particle system, take a look at DEMO13_10.CPP|EXE (16-bit version not available) on the CD. It is a fireworks display based on the tank projectile demo. Basically, the tank from the previous demo fires projectiles now. Also, note in the demo I jacked the number of particles up to 256. |