Inspired by this thread over at Gamedev, I set out on yet another learning journey to build my own particle engine. After looking at the links on that thread, I remembered that the legendary Lazy Foo has his own article on Particle Engines and headed over there to have a perusal (as you should, his explanation will make a lot more sense than mine…).
So it turns out that as with most of these things, you need to set up the base class first (well, it’s not technically a base class in the normal sense of the word, but it’s the basis of our engine) – a Particle class. Of course as each particle is drawn to the screen that makes it a Sprite in my project. An incredibly simple sprite at that too. One function to manage the sprite on a frame by frame basis and one to calculate if it’s dead. The two private functions are for creating the image filename and ensuring the particles appear in a circle.
class Particle : public Sprite { private: int frame; inline const double distance(const Point2D p1, const Point2D p2) const; std::string chooseImage(); public: Particle(Point2D point); ~Particle() = default; void manage(); bool isDead(); };
Creation of a particle is a fairly simple routine – randomly create points either side of the centre point, ensuring that they’re no more than a distance of 75 from the centre (the radius, ensuring that the particles are in a circular shape). Particles are given a random frame number so they don’t all appear and disappear at the same time. chooseImage() randomly chooses whether the image will be dark grey or light grey.
Particle::Particle(Point2D point) : Sprite(chooseImage()) { //Set offsets short xDir, yDir; switch(rand() % 4) { case 0: xDir = 1; yDir = 1; break; case 1: xDir = -1; yDir = 1; break; case 2: xDir = 1; yDir = -1; break; case 3: xDir = -1; yDir = -1; break; } //ensure that the smoke appears in a circular shape do { position.x = point.x + (xDir * (rand() % 75)); position.y = point.y + (yDir * (rand() % 75)); } while(distance(point, position) > 75); //Initialize animation frame = rand() % 5; }
manage() increments the frame counter each frame and isDead checks whether the frame counter has cleared 10, the maximum number of frames a particle can exist for.
The ParticleEngine class handles the management, creation and destruction of the particles in it’s constructor and managePartices() function. The two iterator functions are so that the particles can be drawn from drawSpriteCntr(T) in the Level class. isRunning() provides a check to see if the engine’s running time has expired.
class ParticleEngine { private: using ParticlePtr = std::shared_ptr<Particle>; const unsigned short numberOfParticles = 500; const unsigned short engineTime = 350; Point2D position; std::vector<ParticlePtr> particles; const int startTime; public: ParticleEngine(Point2D point); ~ParticleEngine() = default; void manageParticles(int groundLevel); bool isRunning(); std::vector<ParticlePtr>::iterator begin() { return std::begin(particles); } std::vector<ParticlePtr>::iterator end() { return std::end(particles); } };
The constructor takes the Point2D passed in and moves it a little bit to centre the explosion on the point of impact, then creates the numberOfParticles stated by the const in the header.
ParticleEngine::ParticleEngine(Point2D point) : startTime(SDL_GetTicks()) { position.x = point.x + 4; position.y = point.y + 10; for( int p = 0; p < numberOfParticles; p++ ) { particles.push_back(std::make_shared<Particle>(position)); } }
manageParticles() iterates through the particles, managing each of them (i.e. incrementing their frame counter). It checks whether they have expired and if so erases the current particle and pushes a new one onto the vector in it’s place.
void ParticleEngine::manageParticles(int groundLevel) { for(auto it = std::begin(particles); it != std::end(particles);) { (*it)->manage(); if((*it)->isDead()) { it = particles.erase(it); particles.push_back(std::make_shared<Particle>(position)); } else ++it; } }
Running the engine in the Level class is relatively simple after everything we’ve now set up. Add a std::vector<std::shared_ptr> to the Level members, then when a building is hit we call these two lines. The first one to position the engine, the second one to create it.
Point2D smokePos = bomb->getPosition(); pEngines.push_back(std::make_shared<ParticleEngine>(smokePos));
Then every ParticleEngine in the vector has to be managed at the end of the logic() function, through the following code, which basically says if the engine is running then manage it, otherwise erase it. Simple.
for(auto it = std::begin(pEngines); it != std::end(pEngines);) { if((*it)->isRunning()) { (*it)->manageParticles(groundLevel); ++it; } else { it = pEngines.erase(it); } }
Finally we have to draw the engines to the screen. Again, thanks to our templated drawSpriteCntr(T) function, and our iterator functions in ParticleEngine, this is simple – iterate through the engines std::vector and draw them all to screen.
for(const auto& pE : pEngines) { drawSpriteCntr(*pE); }
And just like that, we have a working particle engine that produces effects such as this:
Now, no-one is going to mistake this for smoke anytime soon, but for me it does the job just fine. I’m sure I will return to this in an effort to improve it in the future but for now it’s a portable engine class that produces nice smoke effects that can easily be modified to produce other particle effects (fire for instance).
Leave a comment