/******************************************************************************************* * * raylib [shapes] example - particles * * Example complexity rating: [★☆☆☆] 1/4 * * Example originally created with raylib 5.6, last time updated with raylib 5.6 * * Example contributed by Jordi Santonja (@JordSant) * * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, * BSD-like license that allows static linking with closed source software * * Copyright (c) 2025 Jordi Santonja (@JordSant) * ********************************************************************************************/ #include "raylib.h" #include // Required for: calloc(), free() #include // Required for: cosf(), sinf() #define MAX_PARTICLES 300 // Max number particles //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- typedef enum ParticleType { WATER = 0, SMOKE, FIRE } ParticleType; static const char particleTypesChar[3][10] = { "WATER", "SMOKE", "FIRE" }; typedef struct Particle { Vector2 position; // Particle position on screen Vector2 velocity; // Particle current speed and direction bool alive; // Particle alive: inside screen and life time float lifeTime; // Particle life time ParticleType type; // Particle type (WATER, SMOKE, FIRE) float radius; // Particle radius Color color; // Particle color } Particle; typedef struct CircularBuffer { int head; // Index for the next write int tail; // Index for the next read Particle* buffer; // Particle buffer array } CircularBuffer; //---------------------------------------------------------------------------------- // Module Functions Declaration //---------------------------------------------------------------------------------- static Particle* AddToCircularBuffer(CircularBuffer* circularBuffer); static void UpdateParticles(CircularBuffer* circularBuffer, int screenWidth, int screenHeight); static void UpdateCircularBuffer(CircularBuffer* circularBuffer); static void DrawParticles(CircularBuffer* circularBuffer); //------------------------------------------------------------------------------------ // Program main entry point //------------------------------------------------------------------------------------ int main(void) { // Initialization //-------------------------------------------------------------------------------------- const int screenWidth = 800; const int screenHeight = 450; InitWindow(screenWidth, screenHeight, "raylib [shapes] example - particles"); // Definition of particles Particle* particles = (Particle*)RL_CALLOC(MAX_PARTICLES, sizeof(Particle)); // Particle array CircularBuffer circularBuffer = { 0, 0, particles }; // Particle emitter parameters int emissionRate = 2; ParticleType currentType = WATER; Vector2 emitterPosition = { screenWidth / 2.0f, screenHeight / 2.0f }; SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- // Main game loop while (!WindowShouldClose()) // Detect window close button or ESC key { // Update //---------------------------------------------------------------------------------- // Emit new particles: when emissionRate is 1, emit every frame if ((rand() % emissionRate) == 0) { Particle* newParticle = AddToCircularBuffer(&circularBuffer); // If buffer is full, newParticle is NULL if (newParticle != NULL) { // Fill particle properties newParticle->position = emitterPosition; newParticle->alive = true; newParticle->lifeTime = 0.0f; newParticle->type = currentType; float speed = (float)(rand() % 10) / 5.0f; switch (currentType) { case WATER: newParticle->radius = 5.0f; newParticle->color = BLUE; break; case SMOKE: newParticle->radius = 7.0f; newParticle->color = GRAY; break; case FIRE: newParticle->radius = 10.0f; newParticle->color = YELLOW; speed /= 10.0f; break; default: newParticle->radius = 5.0f; newParticle->color = WHITE; break; } float direction = (float)(rand() % 360); newParticle->velocity = (Vector2){ speed * cosf(direction * DEG2RAD), speed * sinf(direction * DEG2RAD) }; } } // Update the parameters of each particle UpdateParticles(&circularBuffer, screenWidth, screenHeight); // Remove dead particles from the circular buffer UpdateCircularBuffer(&circularBuffer); // Change Particle Emission Rate (UP/DOWN arrows) if (IsKeyPressed(KEY_UP) && (emissionRate > 1)) --emissionRate; if (IsKeyPressed(KEY_DOWN)) ++emissionRate; // Change Particle Type (LEFT/RIGHT arrows) if (IsKeyPressed(KEY_RIGHT)) (currentType == FIRE) ? currentType = WATER : ++currentType; if (IsKeyPressed(KEY_LEFT)) (currentType == WATER) ? currentType = FIRE : --currentType; //---------------------------------------------------------------------------------- // Draw //---------------------------------------------------------------------------------- BeginDrawing(); ClearBackground(RAYWHITE); // Call the function with a loop to draw all particles DrawParticles(&circularBuffer); // Draw UI and Instructions DrawRectangle(5, 5, 315, 75, Fade(SKYBLUE, 0.5f)); DrawRectangleLines(5, 5, 315, 75, BLUE); DrawText("CONTROLS:", 15, 15, 10, BLACK); DrawText("UP/DOWN: Change Particle Emission Rate", 15, 35, 10, BLACK); DrawText("LEFT/RIGHT: Change Particle Type (Water, Smoke, Fire)", 15, 55, 10, BLACK); DrawText(TextFormat("Emission Rate: %d | Type: %s", emissionRate, particleTypesChar[currentType]), 15, 95, 10, DARKGRAY); DrawFPS(screenWidth - 80, 10); EndDrawing(); //---------------------------------------------------------------------------------- } // De-Initialization //-------------------------------------------------------------------------------------- RL_FREE(particles); // Free particles array data CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } //---------------------------------------------------------------------------------- // Module Functions Definition //---------------------------------------------------------------------------------- static Particle* AddToCircularBuffer(CircularBuffer* circularBuffer) { Particle* particle = NULL; // Check if buffer full if (((circularBuffer->head + 1) % MAX_PARTICLES) != circularBuffer->tail) { // Add new particle to the head position and advance head particle = &circularBuffer->buffer[circularBuffer->head]; circularBuffer->head = (circularBuffer->head + 1) % MAX_PARTICLES; } return particle; } static void UpdateParticles(CircularBuffer* circularBuffer, int screenWidth, int screenHeight) { for (int i = circularBuffer->tail; i != circularBuffer->head; i = (i + 1) % MAX_PARTICLES) { // Update particle life and positions circularBuffer->buffer[i].lifeTime += 1.0f / 60.0f; // 60 FPS -> 1/60 seconds per frame switch (circularBuffer->buffer[i].type) { case WATER: circularBuffer->buffer[i].position.x += circularBuffer->buffer[i].velocity.x; circularBuffer->buffer[i].velocity.y += 0.2f; // Gravity circularBuffer->buffer[i].position.y += circularBuffer->buffer[i].velocity.y; break; case SMOKE: circularBuffer->buffer[i].position.x += circularBuffer->buffer[i].velocity.x; circularBuffer->buffer[i].velocity.y -= 0.05f; // Upwards circularBuffer->buffer[i].position.y += circularBuffer->buffer[i].velocity.y; circularBuffer->buffer[i].radius += 0.5f; // Increment radius: smoke expands circularBuffer->buffer[i].color.a -= 4; // Decrement alpha: smoke fades if (circularBuffer->buffer[i].color.a < 4) // If alpha transparent, particle dies circularBuffer->buffer[i].alive = false; break; case FIRE: // Add a little horizontal oscillation to fire particles circularBuffer->buffer[i].position.x += circularBuffer->buffer[i].velocity.x + cosf(circularBuffer->buffer[i].lifeTime * 10.0f); circularBuffer->buffer[i].velocity.y -= 0.005f; // Upwards circularBuffer->buffer[i].position.y += circularBuffer->buffer[i].velocity.y; circularBuffer->buffer[i].radius -= 0.05f; // Decrement radius: fire shrinks circularBuffer->buffer[i].color.g -= 1; // Decrement green: fire turns reddish starting from yellow if (circularBuffer->buffer[i].radius <= 0.02f) // If radius too small, particle dies circularBuffer->buffer[i].alive = false; break; default: break; } // Disable particle when out of screen if ((circularBuffer->buffer[i].position.x < 0) || (circularBuffer->buffer[i].position.x > screenWidth) || (circularBuffer->buffer[i].position.y < 0) || (circularBuffer->buffer[i].position.y > screenHeight)) circularBuffer->buffer[i].alive = false; } } static void UpdateCircularBuffer(CircularBuffer* circularBuffer) { // Update circular buffer: advance tail over dead particles while ((circularBuffer->tail != circularBuffer->head) && !circularBuffer->buffer[circularBuffer->tail].alive) { circularBuffer->tail = (circularBuffer->tail + 1) % MAX_PARTICLES; } } static void DrawParticles(CircularBuffer* circularBuffer) { for (int i = circularBuffer->tail; i != circularBuffer->head; i = (i + 1) % MAX_PARTICLES) { if (circularBuffer->buffer[i].alive) { DrawCircleV(circularBuffer->buffer[i].position, circularBuffer->buffer[i].radius, circularBuffer->buffer[i].color); } } }