ClanLib logo

Simple Game Development with ClanLib 6.0

Part 5

Particles and More OpenGL with ClanLib

    Last tutorial, we looked at how to set up OpenGL with ClanLib, as well as how to load CL_Textures through CL_ResourceManager. This time I will extend that example much further and show how we can modify parameters inside the game without recompiling. A word of warning - this tutorial presents little new CL code until the end, but readers looking for a practical example of CL & GL in use should find this section interesting nonetheless.

Particle Systems
Screenshot of the exhaust trail we're working towards

    Particles are found everywhere in nature and in games that seek to emulate it (however crudely). We can consider atmospheric phenomenon like rain, snow, and clouds to be particle systems, as well as things like dust, smoke, exhaust, and shrapnel. You can get a good definition of particle systems as well as some good background on their representation in computer graphics here.

    In computer games, these particle systems are usually simplified to collections of single sprites that have been individually modified in color, size, or transparency. We'll be using single textured quads for our particles which is about as simple as you can get.

    Fig 22.

    struct Particle {
    
    	Vector2 location, velocity;
    	
    	float size;
    	float r, g, b, a;
    	float totalLife, lifeTimeRemaining;
    };

    The above particle structure is pretty general, so we can use it for almost any kind of effect. If you want, you can extend this structure through inheritance to add more variables and it will still work in the code that follows.

    One popular way of implementing particle systems is to have a source where the particles originate called the emitter and seperately a system that modifies them as they progress through their lives called an affector. However, since all our particles will be in discreet systems (none cross from being, say, a star to being a puff of smoke) we can safely group our emitters and affectors together. Our next class will contain both our array of particles and the functions used to create and modify them. For simplicity, we'll call it Emitter.

    Fig 23.
    class Emitter {
    
    public:
    	Emitter();
    	~Emitter();
    
    	// These functions must be implemented by classes that extend Emitter
    	virtual Particle createParticle() = 0;
    	virtual void update() = 0;
    	virtual void affectParticle() = 0;
    
    	void paintParticles();
    
    	void setActive() { emitting = false; };
    	void setInactive() { emitting = true; };
    	bool isEmitting() { return emitting; };
    
    	Vector2 getLocation() { return location; };
    	
    	CL_Texture *particleImage;
    
    protected:
    	Particle particleArray[MAX_PARTICLES];	// Particles for this emitter
    	int particleArrayIndex;			// Pointer into the particle array
    
    	Vector2 location;			// The location of this emitter
    	
    	bool emitting;
    	float particleLifetime;
    };

    Since each particle in our systems will be represented by the same sprite, we can group that CL_Texture pointer into the Emitter class. MAX_PARTICLES can be defined anywhere, so I put it in with the other #define statements in the common.h header file.

    Notice the '= 0' statements after a few of the function definitions - these tell the compiler two things: this class cannot be instantiated or created as an object, and any class that extends the Emitter class must implement these functions. For anyone familiar with java, this is like an Interface or Abstract base class. Using this as a foundation, let's start designing the exhaust system for our spinning rocket ship.

The Rocket Trail

    We'll implement the exhaust emitter in a class conveniently called Exhaust that inherits from Emitter. Our inherited Exhaust class only requires one additional member, a pointer to a float value telling us which direction the rocket ship is pointing so the exhaust can shoot the opposite direction. To fill this member in, we will need to add ExhaustObject.rocketDirection = &ShipObject.theta to our Particles main() function.

    Fig 24.
    class Exhaust : public Emitter {
    
    public:
    	float *rocketDirection;		// Direction opposite particle velocity
    
    	Particle createParticle();	// Fill in abstract functions
    	void update();
    	void affectParticle();
    };

    Logically, the first function we should implement is the createParticle() one. Note the strange expressions containing rand() to create the particle velocities - they were created simply by trial and error, along with all the other values, seeing what looks the best and making small adjustments.

    Fig 25.
    Particle Exhaust::createParticle() {
    
    	Particle newParticle;
    	float rocketDirectionValue = (*rocketDirection) * 180.0f/float(PI);
    
    	// Set the particle initial color values
    	newParticle.r = 1; newParticle.g = 1; newParticle.b = .5f; newParticle.a = 1;
    
    	// Set particle initial size
    	newParticle.size = 15;
    
    	// Set lifetime/time remaining to half a second
    	newParticle.totalLife = .5f;
    	newParticle.lifeTimeRemaining = newParticle.totalLife;
    
    	// Location
    
    	// Copy the emitter/ship location
    	newParticle.location = this->location;
    
    	// Add an offset so the exhaust comes from the rear of the ship
    	newParticle.location += Vector2(rocketDirectionValue + 90, 50);
    
    	// Velocity
    
    	// Give the exhaust some velocity in the reverse of where the ship is headed
    	newParticle.velocity = Vector2(rocketDirectionValue + 90, float(rand() % 10)/10 + 1.5);
    
    	// This offsets the direction the particle is ejected at, giving a spray effect
    	newParticle.velocity += (Vector2(rocketDirectionValue - (rand() % 65 ), 1.25f));
    
    	return newParticle;
    }

    Now that we have a function to create particles for us, we have to have a way to update them as they burn through their lives. I have chosen to seperate the update() function from affectParticle() one; update() will handle housekeeping chores, like incrementing the particle's lifeTimeRemaining value and creating new particles when it is time. affectParticle() will do the fancy things like change the particle's size and color individually.

    Fig 26.
    void Exhaust::update() {
    
    	// We will add a new particle every .01 second
    	float rateOfCreation = 1.0f/100.0f; 
    
    	// More time has elapsed since we last added a particle
    	timeLastParticleAdded += .01f;
    
    	// Create particles
    
    	// If enough time has elapsed since we last added and the emitter is active..
    	if (timeLastParticleAdded > rateOfCreation && emitting) {
    
    		// .. reset the clock
    		timeLastParticleAdded = 0;
    
    		// And add a new particle
    		particleArray[particleArrayIndex++] = createParticle();
    
    		// If we have reached the end of the array, go to the beginning
    		if (particleArrayIndex == MAX_PARTICLES)
    			particleArrayIndex = 0;
    	}
    
    	// Update particles
    
    	for (int i=0; i < MAX_PARTICLES; i++) {
    
    		// Ignore particles that are not active (have no more time remaining)
    		if (particleArray[i].lifeTimeRemaining <= 0) continue;
    
    		// Add the velocity to find the particle's new location
    		particleArray[i].location += particleArray[i].velocity;
    
    		// The particle dies a little each update
    		particleArray[i].lifeTimeRemaining -= .01f;
    
    		// Apply affector function
    		affectParticle(&particleArray[i]);
    	}
    }

    The Emitter base class contains the paintParticles() function - since it is totally straightforward I will leave out the complete code listing. One thing we do have to change for the rendering of our particles is OpenGL's blending function. We used glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) for the drawing of our rocket ship, but for particles it is better to use glBlendFunc(GL_SRC_ALPHA, GL_ONE) so that the particle's brightness is cumulative with any particles beneath it.

The Fun Part

    Now that are particles can be created and updated, we can add them to our Particles main function and try them out. Since we haven't created the affector function, they will not change, but they will go through their life cycle without changing.

    I chose to use a static array for simplicity sake, but it does have a drawback. If we were to create 200 particles per second each with a lifetime of 2 seconds and our MAX_PARTICLES were set to just 50, our array would quickly run out of open slots and begin overwriting particles that should still be active.

    The way around this is simple enough, just make sure that you have a big enough MAX_PARTICLES value so that the particles can die naturally before being overwritten by new ones. In my above example, a MAX_PARTICLES of 400 should be sufficient. Because our particles are only 44 bytes each, we could put enough particles on the screen to choke even fast video cards long before array size becomes an issue.

    Now that we've got all our other ducks in a row, we can focus on the affector function at the heart of our particle effects.

    Fig 27.
    void Exhaust::affectParticle(Particle *particleToUpdate) {
    
    	// Directly access the particle we're modifying 
    	Particle *particle = particleToUpdate;
    
    	// If the particle is dead, we can skip it
    	if (particle->lifeTimeRemaining <= 0) return;
    
    	// Affect the particle
    
    	// It is useful to change particle attributes based on the percentage 
    	// of how long it has left versus it has been alive
    	float percentLifeLeft = particle->lifeTimeRemaining / particle->totalLife;
    
    	// Jiggle the value around so there aren't definite boundaries between steps
    	float percentLifeLeftModified = percentLifeLeft + float(rand() % 4) / 20.0f - .25f;
    
    	// The following if-else structure does the real job of affecting the particles
    	// Instead of using formulas to recalculate the color, size, and velocity of the
    	// particle I've chosen to use a simple system of steps; as the particle moves
    	// through life its attributes change. 
    
    	if (percentLifeLeftModified > .8f) {			// >80 percent of life left
    		particle->r = 1; particle->g = 1; particle->b = 1;
    
    	} else if (percentLifeLeftModified > .6f) {		// >60 percent of life left
    		particle->r = .9f; particle->g = .9f; particle->b = .6f;
    
    	} else if (percentLifeLeftModified > .4f) {		// >40 percent of life left
    		particle->r = .8f; particle->g = .55f; particle->b = .4f;
    		particle->velocity.magnitude *= .99f;
    		particle->size += (particle->size/72);
    
    	} else if (percentLifeLeftModified > .18f) {		// >20 percent of life left
    		particle->r = .8f; particle->g = .5f; particle->b = .4f;
    		particle->size += (particle->size/36);
    		particle->velocity.magnitude *= .98f;
    
    	} else {						// >0 percent of life left
    		particle->r = .5f; particle->g = .5f; particle->b = .5f;
    		particle->size += (particle->size/24);
    		particle->velocity.magnitude *= .97f;
    	}
    
    	// Fade the particle linearly as it runs out of time
    	particle->a = percentLifeLeft; 
    }

    Now that we are set to modify the particles, our effect should be nearly complete. Of course, you will want to play around with the values in the if-else structure to get just the effect you like. I only settled on the current effect after about 3 dozen trials, so don't expect to get the one you want on the first try.

It's Full Of Stars!

    Now that our rocket ship and exhaust plume are complete, we can finish the example by filling in a scrolling starfield in the background. Implementing this effect differs quite a bit from the exhaust one, but thanks to the generality of our emitter class we can work within the same framework. I won't go over this effect in this tutorial but you can read about how it is coded in the source at the bottom.

    Fig 28.
Tweakin' The Effects

    While you're working on the finding the best input parameters for your particles, it may be helpful (depending on the time needed for compiliation) to store the parameters in CL resource file. For instance, say you're trying to find a good color combination for an explosion effect. Instead of changing the colors in the code and recompiling 20 times, you can change the color parameters in the resource file and run the program 20 times without recompiling. We'll be modifying the affectParticle() function of our Exhaust class so it can read colors from a resource file. Here's the new layout for our resource file:

    Fig 29.
    section Graphics
    {
    	ship = ship.tga (type=surface);
    	exhaust = exhaust.tga (type=surface);
    	star = star.tga (type=surface);
    }
    section ExplosionColors
    {
    	section Stage1
    	{
    		red = 100 (type=integer);
    		green = 100 (type=integer);
    		blue = 100 (type=integer);
    	}
    	...(Stage2-4)...
    	section Stage5
    	{
    		red = 50 (type=integer);
    		green = 50 (type=integer);
    		blue = 50 (type=integer);
    	}
    }
    Fig 30.
    // Now we can access each of these values in the following manner:
    int myInt = CL_Integer("resourceString", resourceManagerPointer)
    
    // We can create the resource strings on the fly. For example, the source includes
    // an array of structs with red, green, and blue float values. Here we can read the 
    // values directly from the resource file in a loop. Since CL doesn't allow floats 
    // in resource files, we have to scale the color values down to the 0.0-1.0 range
    // by dividing by 100.
    
    CL_String resID = "ExplosionColors/stage";
    
    // Read in resource values
    for (int i=0; i < 5; i++) {
    	stage[i].r = float (CL_Integer(resID+CL_String(i+1)+"/red", manager)) / 100;
    	stage[i].g = float (CL_Integer(resID+CL_String(i+1)+"/green", manager)) / 100;
    	stage[i].b = float (CL_Integer(resID+CL_String(i+1)+"/blue", manager)) / 100;
    }

    Once the values are read into our stage structs, we have to modify the affectParticle() function to use these values instead of the hard-coded ones we were using before. In the source, I create a second affect function called resourceAffectParticle() so you can see the differences in the two.

    You can use this strategy for all the parameters of your particles, or really any value that you use in your game. You can also use it to read in almost any kind of hierarchical data structure since resource sections can be nested. Even use it to store player data by creating a function that outputs data in CL's resource file format. The flexibility is really quite handy.

    That's it for this part of the series. Next time we'll look at put basic networking functionality into your games.

    Download Binary Spinning Ship + Particles Application
    Download Visual C++ Workspace for Part 5

    Return to tutorial main