What are Particle Effects?
Puffs of dirt kicked up from tires, smoke after explosions, empty shell casings ejecting from automatic rifles, and splashes of water are all particle effects. Shattered pieces of scored gems in a puzzle game, splatters of blood from gunfire, broken glass, chunks of wood, snowflakes, sparks, and shards of plants are all particle effects.
Particle effects are categorically different from most other objects in a videogame. Note the following differences:
- Particles don’t influence gameplay. They don’t knock things over, respond to being stepped on by enemies, or inflict damage. A game that has particle effects, generally speaking, would play out exactly the same without the particle effects.
- The game needs to move and draw many particles at once. Dozens or even hundreds of particles may be constantly present – perhaps as rocket thrust, boat wake, or damage smoke – and during intense action the game may need to handle many thousands of particles. This means they must be small in memory and require minimal processing.
- Particle effects are dynamic. Particles spin, rotate, fade, bounce, drift, fall, and sometimes even produce sounds (when they start, fizzle out, or hit the ground).
- The number of particles must be limited to protect the game’s framerate. Drawing and moving thousands of particles every second can pose a serious risk to game performance.
- Particles spawn with some randomness. Because many particles can be spawned from a single event, introducing slight randomness into the initial location, velocity, and lifetime avoids particles overlapping one another, increasing the odds that each one using processing and rendering time gets seen.
- Particles only last a few seconds. What makes particles exciting is their movement. Once the particle’s action is complete it needs to be made available for a future effect.
Benefits of a Fixed-Size Array
There are dozens of container structures that are more complex, robust, and flexible than fixed-size (i.e. normal, basic, no frills) arrays – linked lists, vectors, and so on. Why do I recommend using fixed-size arrays for particle effects?
- Arrays are extremely fast to access.
- Arrays avoid overhead from memory allocation or deallocation, and in so doing also avoid cumulative memory fragmentation.
- The usual downside to using fixed-size arrays – that they have a maximum number of elements allowed at once – is desirable for particle effects! This ensures the number of particle effects are kept within a sane limit.
Code, Applied to Asteroids Source
In main.cpp of the sample Asteroids starter source…
After:
#define ROCK_NUM 25
Add:
#define PFX_NUM 1000 // maximum number of particle effects
After:
obj_typ ast[ROCK_NUM];
Add:
struct pfx_typ {
float xv, yv; // component velocity
float x, y; // position
int life; // frames until it vanishes
int color; // color of this particle
};
int pfx_alive = 0;
pfx_typ pfx[PFX_NUM];
Before:
void drawThings() {
Add:
void pfxJet(int spawnNum, obj_typ *fromObj) {
int toSpawn;
if( pfx_alive + spawnNum >= PFX_NUM ) {
toSpawn = PFX_NUM-1 - pfx_alive;
} else {
toSpawn = spawnNum;
}
float cosPower = -cos(fromObj->ang) * 0.5;
float sinPower = -sin(fromObj->ang) * 0.5;
int colorForThisSet;
colorForThisSet = 224+(rand()&0x1F); // shade from 224 to 255
if(rand() & 0x01 == 1) {
// bold reds
colorForThisSet = makecol(colorForThisSet, 0, 0);
} else {
// bold yellow
colorForThisSet = makecol(colorForThisSet, colorForThisSet, 0);
}
for(int i=0;ix = fromObj->x - 3 + (rand()&0x07);
effect->y = fromObj->y - 3 + (rand()&0x07);
effect->xv = fromObj->xv+float(-127+(rand()&0xFF))*0.002+cosPower;
effect->yv = fromObj->yv+float(-127+(rand()&0xFF))*0.002+sinPower;
effect->life = 60+(rand()&0x7F);
effect->color = colorForThisSet;
pfx_alive++; // raise the particle count to include the newest one
}
}
Before:
drawLine( &ship , makecol(255, 255, 0) );
Add:
// move and draw particles in the same pass // remember that we may have a TON of particles, // so it's good to only iterate over the list // as few times as possible for(int i=0;ilife > 0) { // move and draw it effect->x += effect->xv; effect->y += effect->yv; circlefill(screenBuffer, int(effect->x), int(effect->y), int(effect->life>>6), // radius as life/64 effect->color); } else { // remove it from the list pfx_alive--; // decrease number of live particles if(pfx_alive >= 0) { // providing we had 1+ particle left: pfx[i] = pfx[pfx_alive]; // copy last particle onto this one i--; // reconsider this particle } } }
After:
ship.yv += sin(ship.ang) * thrustPower;
Add:
pfxJet(10,&ship);
That’s it! The code includes a number of strange optimizations – none of which I benchmarked (feel free to inform me if I’ve been silly to cache the array accesses into pointers, etc.). A few examples of minor optimizations shown here are bit shifting for division, bitwise AND as alternative to modulo to limit rand, and end-swapping in an array to avoid gaps without element shifting. Because particles can number in the thousands, particle programming is one of the areas where otherwise outdated or trivial optimizations (that otherwise should be avoided to keep code readable) can make a significant difference.
Modified Source
Here’s an updated example main.cpp file with the above changes made, in case the above insertion hints proved tricky to follow: Asteroids main.cpp with particles.
(Note that this requires core.cpp and core.h from the main starter source.)
(Originally posted as GameDevLessons.com Vol. 9 Sec. 2)
Permalink: http://www.hobbygamedev.com/articles/vol9/particle-effects-code/
[...] Programming Asteroids (Related: Particle Effects Code) [...]
[...] Vol 9 Int – Particle Effects [...]