Hope Game
Logo Glusoft
Glusoft

Creating a particle system

The particle class

Each particle has some properties such as :


For the class particle we will use the attributes :
GPU_Image *id; // id of the particle
GPU_Rect rect; // region of the source image to use
float cur_lifetime; // current lifetime
float lifetime; // total lifetime
float x; // first vertex coordinate x
float y; // first vertex coordinate y
float x2; // second vertex coordinate x
float y2;  // second vertex coordinate y
float vx; // speed along the x-axis
float vy; // speed along the y-axis
float ax; // acceleration along the x-axis
float ay; // acceleration along the y-axis
float rot; // angle rotation of the first point
float th; // angle rotation of the second point 
size_t set; // true if the particle is allocated
SDL_Color color; // color of the particle

For the default constructor we need to set everything to 0 :
Particle() {
	this->id = NULL;
	this->rect = GPU_Rect{0, 0, 0, 0};
	this->lifetime = 0;
	this->cur_lifetime = 0;
	this->x = 0;
	this->y = 0;
	this->x2 = 0;
	this->y2 = 0;
	this->vx = 0;
	this->vy = 0;
	this->ax = 0;
	this->ay = 0;
	this->rot = 0;
	this->set = 0;
	this->color = SDL_Color{ 255, 0, 0, 255 };
}

Then the real constructor with all the parameters:
Particle(GPU_Image* id, GPU_Rect rect, float lifetime, float x, float y, float x2, float y2, float vx, float vy, float ax, float ay, float rot, float th, SDL_Color color) {
	this->id = id;
	this->rect = rect;
	this->lifetime = lifetime;
	this->cur_lifetime = lifetime;
	this->x = x;
	this->y = y;
	this->x2 = x;
	this->y2 = y;
	this->vx = vx;
	this->vy = vy;
	this->ax = ax;
	this->ay = ay;
	this->rot = rot;
	this->th = th;
	this->set = 1;
	this->color = color;
}

We also need a function to reset the default parameters:
void reset() {
	this->id = NULL;
	this->rect = GPU_Rect{ 0, 0, 0, 0 };
	this->lifetime = 0;
	this->cur_lifetime = 0;
	this->x = 0;
	this->y = 0;
	this->x2 = 0;
	this->y2 = 0;
	this->vx = 0;
	this->vy = 0;
	this->ax = 0;
	this->ay = 0;
	this->rot = 0;
	this->set = 0;
	this->color = SDL_Color{ 255, 0, 0, 255 };
}

Then when we will update the particles a function with all the parameters (again) :
void update(GPU_Image* id, GPU_Rect rect, float lifetime, float x, float y, float x2, float y2, float vx, float vy, float ax, float ay, float rot, float th, SDL_Color color) {
	this->id = id;
	this->rect = rect;
	this->lifetime = lifetime;
	this->cur_lifetime = lifetime;
	this->x = x;
	this->y = y;
	this->x2 = x;
	this->y2 = y;
	this->vx = vx;
	this->vy = vy;
	this->ax = ax;
	this->ay = ay;
	this->rot = rot;
	this->th = th;
	this->set = 1;
	this->color = color;
}

A group of particles

To make this effect we need a lot of particles, so we create a class Firework :

class Firework {
public:
	Firework(GPU_Image* image, float x, float y, float resX, float resY, size_t max, float emitFreq);
	~Firework();
	void update(float timeElapsed);
	void render(GPU_Target * screen, double timeElapsed);
	float lifetime;
	float vel; // velocity
	float acc; // acceleration
	float ratio; //
	float emitFreq; //frequence emission
	size_t max; // number maximum of particles
	const Particle* particles; // An array of particles
private:
	GPU_Image* image; // image of the particle
	float x; // position x 
	float y; // position y
	size_t resX; // width of the window
	size_t resY; // height of the window
	float timeEmit; // delay for the emission
};

The constructor and destructor of the class Firework are simple:
Firework::Firework(GPU_Image* image, float x, float y, float resX, float resY, size_t max, float emitFreq) {
	this->image = image;
	this->x = x;
	this->y = y;
	this->resX = resX;
	this->resY = resY;
	this->max = max;
	this->emitFreq = emitFreq;
	this->particles = new Particle[max];
}

Firework::~Firework() {
	delete[](particles);
}

The method to render the particles, simply update the position, iterate over the particles allocated, and render :
void Firework::render(GPU_Target *screen, double time) {
	update(time);

	for (size_t i = 0; i < max; ++i) {
		if (particles[i].set == 1) {
			GPU_SetColor(image, SDL_Color{ particles[i].color.r, particles[i].color.g, particles[i].color.b, (Uint8)(255 * particles[i].cur_lifetime / particles[i].lifetime) });
			GPU_BlitTransformX(image, &particles[i].rect, screen, particles[i].x, particles[i].y, image->w / 4.0f, image->w / 4.0f, particles[i].rot, 0.2f, 0.2f);
		}
	}
}

The update function is more complex because we need to update the position and lifetime of the particle and also emit new particles.

To update the particles we iterate over all particles allocated :
for (size_t i = 0; i < max; ++i) {
	if (particles[i].set == 1) {
		particles[i].x = particles[i].x + time * particles[i].vx + 0.5f*time*time*particles[i].ax;
		particles[i].y = particles[i].y + time * particles[i].vy + 0.5f*time*time*particles[i].ay;
		particles[i].cur_lifetime = particles[i].cur_lifetime - time;

		if (particles[i].cur_lifetime <= 0) {
			particles[i].reset();
		}
	}
}

After that we create a new explosion but the position and the color need to be random.
timeEmit = timeEmit + time;
GPU_Rect rect{ 0.0f, 0.0f, image->w, image->h };
float r = 1;
size_t nb_explosion = 4 + rand()%13;

while (emitFreq < timeEmit) {
	for (size_t i = 0; i < nb_explosion; i++) {
		size_t maxEmitSimul = 50 + rand()%100;
		size_t nbEmit = 0;
		float xO = rand() % resX;
		float yO = rand() % resY;

		Uint8 color_r = rand() % 255;
		Uint8 color_g = rand() % 255;
		Uint8 color_b = rand() % 255;
		
		float th = (float)((rand() % 360)*M_PI / 180.0f);

		for (size_t i = 0; i < max; ++i) {
			if (nbEmit >= maxEmitSimul) {
				break;
			}
			else if (particles[i].set == 0) {
				float th2 = (float)((rand() % 360)*M_PI / 180.0f);
				float posX = xO + r * cos(th2);
				float posY = yO + r * sin(th2);

				particles[i].update(image, rect, lifetime, posX, posY, xO, yO, (vel + (rand() % 10) / 50.f)*cos(th2) , (vel + (rand() % 10) / 50.f)*sin(th2), acc*cos(th2), acc*sin(th2), (float)(rand() % 360), th2, SDL_Color{ color_r, color_g, color_b, 255 });
				++nbEmit;
			}
		}
	}
	timeEmit = timeEmit - emitFreq;
}

The main function

The main function simply create a Firework with "good" parameters, and then limit the rendering to 30 fps.

typedef std::chrono::high_resolution_clock Clock;

int main(int argc, char *argv[]) {
	SDL_Init(SDL_INIT_VIDEO);

	GPU_Target *screen = GPU_InitRenderer(GPU_RENDERER_OPENGL_3, 1920, 1080, GPU_DEFAULT_INIT_FLAGS);

	if (screen == NULL || ogl_LoadFunctions() == ogl_LOAD_FAILED) {
		std::cout << "error initialization OpenGL\n";
	}

	GPU_Image* image = GPU_LoadImage("particle.psd");
	double fps = 0;

	Firework gp(image, 1920/2.0f, 1080/2.0f, 19020, 1080, 75000, 10);

	gp.lifetime = 1000;
	gp.vel = 0.00f;
	gp.acc = 0.0025f;

	double timeElapsed = 0;
	SDL_Event event;
	bool done = false;

	while (!done) {
		double elapsedNano = 0;
		auto t1 = Clock::now();

		while (SDL_PollEvent(&event)) {
			if (event.type == SDL_KEYDOWN) {
				if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
					done = 1;
				}
			}	
		}

		GPU_Clear(screen);
		gp.render(screen, timeElapsed);
		GPU_Flip(screen);

		auto t2 = Clock::now();

		elapsedNano = (double)(std::chrono::duration_cast(t2 - t1).count());

		if (elapsedNano > 0) {
			double diff = ((1000000000.f / 30.f) - elapsedNano) / 1000000.f; // milli

			if (diff > 0) {
				SDL_Delay((Uint32)diff);
			}
		}

		auto t3 = Clock::now();

		timeElapsed = (double)(std::chrono::duration_cast(t3 - t1).count())/ 1000000.f;
		fps = 1000.f / timeElapsed;
	}

	GPU_FreeImage(image);
	GPU_Quit();

	return 0;
}

You can download the project: ParticleEditor.7z

Dark theme