Tutorial 4 of 4 — Final

Particle Systems: Visual Polish

20 min read Intermediate Stunning Effects

Why Particles Matter

Particle systems add the "juice" that makes games feel alive. They're small visual effects that respond to gameplay, providing satisfying feedback.

The Particle Class

A particle is a simple object with position, velocity, and a lifespan. It updates each frame and fades away over time.

particle.js
class Particle {
    constructor(x, y, options = {}) {
        this.x = x;
        this.y = y;
        
        // Velocity (randomized spread)
        const angle = options.angle ?? Math.random() * Math.PI * 2;
        const speed = options.speed ?? 2 + Math.random() * 3;
        this.vx = Math.cos(angle) * speed;
        this.vy = Math.sin(angle) * speed;
        
        // Physics
        this.gravity = options.gravity ?? 0.1;
        this.friction = options.friction ?? 0.98;
        
        // Appearance
        this.radius = options.radius ?? 3 + Math.random() * 3;
        this.color = options.color ?? '#FF6B35';
        
        // Lifespan (in frames)
        this.life = options.life ?? 60;
        this.maxLife = this.life;
    }
    
    update() {
        // Apply physics
        this.vy += this.gravity;
        this.vx *= this.friction;
        this.vy *= this.friction;
        
        this.x += this.vx;
        this.y += this.vy;
        
        // Decrease life
        this.life--;
    }
    
    draw(ctx) {
        // Fade based on remaining life
        const alpha = this.life / this.maxLife;
        
        ctx.globalAlpha = alpha;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius * alpha, 0, Math.PI * 2);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.globalAlpha = 1;
    }
    
    get isDead() {
        return this.life <= 0;
    }
}
Key Insight

The alpha value (life/maxLife) goes from 1.0 to 0.0, creating a smooth fade-out effect. We also shrink the radius for extra polish.

The Particle System

A ParticleSystem manages multiple particles, spawning new ones and removing dead ones efficiently.

particle-system.js
class ParticleSystem {
    constructor() {
        this.particles = [];
    }
    
    // Spawn a burst of particles at position
    emit(x, y, count, options = {}) {
        for (let i = 0; i < count; i++) {
            this.particles.push(new Particle(x, y, options));
        }
    }
    
    // Spawn particles in a specific direction
    emitDirectional(x, y, count, baseAngle, spread, options = {}) {
        for (let i = 0; i < count; i++) {
            const angle = baseAngle + (Math.random() - 0.5) * spread;
            this.particles.push(new Particle(x, y, { ...options, angle }));
        }
    }
    
    update() {
        // Update all particles
        for (const p of this.particles) {
            p.update();
        }
        
        // Remove dead particles
        this.particles = this.particles.filter(p => !p.isDead);
    }
    
    draw(ctx) {
        for (const p of this.particles) {
            p.draw(ctx);
        }
    }
    
    get count() {
        return this.particles.length;
    }
}

// Global instance
const particles = new ParticleSystem();

Effect Recipes

💥 Explosion

explosion.js
function createExplosion(x, y) {
    // Main burst
    particles.emit(x, y, 30, {
        speed: 4 + Math.random() * 4,
        gravity: 0.15,
        friction: 0.95,
        color: '#FF6B35',
        life: 40 + Math.random() * 20
    });
    
    // Inner hot core
    particles.emit(x, y, 15, {
        speed: 2 + Math.random() * 2,
        gravity: -0.05,  // Float up
        friction: 0.92,
        color: '#FFEB3B',
        life: 25
    });
}

✨ Sparkles (Pickup/Coin)

sparkle.js
function createSparkle(x, y) {
    particles.emit(x, y, 12, {
        speed: 1 + Math.random() * 2,
        gravity: -0.02,  // Float up gently
        friction: 0.96,
        color: '#00FFFF',
        radius: 2 + Math.random() * 2,
        life: 30 + Math.random() * 20
    });
}

💨 Dust Cloud (Landing)

dust.js
function createDust(x, y) {
    // Emit to both sides
    particles.emitDirectional(x, y, 5, Math.PI, 0.5, {  // Left
        speed: 2 + Math.random() * 1.5,
        gravity: 0.02,
        friction: 0.9,
        color: '#8888AA',
        radius: 4 + Math.random() * 3,
        life: 20
    });
    
    particles.emitDirectional(x, y, 5, 0, 0.5, {  // Right
        speed: 2 + Math.random() * 1.5,
        gravity: 0.02,
        friction: 0.9,
        color: '#8888AA',
        radius: 4 + Math.random() * 3,
        life: 20
    });
}

🌟 Trail Effect

trail.js
// Call this every frame while moving
function createTrail(x, y, movementAngle) {
    // Emit behind the moving object
    const trailAngle = movementAngle + Math.PI;  // Opposite direction
    
    particles.emitDirectional(x, y, 1, trailAngle, 0.3, {
        speed: 0.5 + Math.random() * 0.5,
        gravity: 0,
        friction: 0.95,
        color: '#A855F7',
        radius: 3,
        life: 15
    });
}

Interactive Demo

Click anywhere to create effects. Switch between effect types using the buttons:

Particle Playground
Particles: 0 Click anywhere to spawn effects!

Performance Tips

particle-pool.js
class ParticlePool {
    constructor(maxParticles = 500) {
        this.particles = [];
        this.pool = [];  // Recycled particles
        this.maxParticles = maxParticles;
    }
    
    emit(x, y, count, options = {}) {
        for (let i = 0; i < count; i++) {
            if (this.particles.length >= this.maxParticles) break;
            
            // Try to reuse from pool
            let particle = this.pool.pop();
            
            if (particle) {
                // Reset existing particle
                particle.reset(x, y, options);
            } else {
                // Create new if pool empty
                particle = new Particle(x, y, options);
            }
            
            this.particles.push(particle);
        }
    }
    
    update() {
        for (let i = this.particles.length - 1; i >= 0; i--) {
            const p = this.particles[i];
            p.update();
            
            if (p.isDead) {
                // Move to pool for reuse
                this.pool.push(p);
                this.particles.splice(i, 1);
            }
        }
    }
}

Integration Example

Here's how to integrate particles into a game loop:

game-integration.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const particles = new ParticleSystem();

// Game objects
const player = { x: 100, y: 300, wasGrounded: false };
const enemies = [];

function update() {
    // Update game logic...
    
    // Emit dust when player lands
    if (player.isGrounded && !player.wasGrounded) {
        createDust(player.x, player.y + player.height);
    }
    player.wasGrounded = player.isGrounded;
    
    // Emit explosion when enemy dies
    for (const enemy of enemies) {
        if (enemy.health <= 0) {
            createExplosion(enemy.x, enemy.y);
        }
    }
    
    // Update particle system
    particles.update();
}

function render() {
    ctx.fillStyle = '#0A0A12';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // Draw game objects...
    
    // Draw particles ON TOP for best effect
    particles.draw(ctx);
}

function gameLoop() {
    update();
    render();
    requestAnimationFrame(gameLoop);
}

gameLoop();

Congratulations!

Physics Tutorial Series Complete!

You've learned the core of game physics:

  1. Physics Basics: Velocity, acceleration, gravity, bouncing
  2. Collision Response: Detection, separation, elastic collisions
  3. Platformer Physics: Variable jump, coyote time, jump buffering
  4. Particle Systems: Visual effects and polish
What's Next?

Apply these techniques to build your own games! Check out the Game Showcase to see these concepts in action, or visit the Playground to experiment with code.