Particle Systems: Visual Polish
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.
Explosions
Impact & destruction
Sparks
Collisions & pickups
Smoke & Dust
Landing & movement
Fire
Continuous effects
Trails
Motion & speed
Snow & Rain
Ambient atmosphere
The Particle Class
A particle is a simple object with position, velocity, and a lifespan. It updates each frame and fades away over time.
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;
}
}
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.
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
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)
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)
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
// 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:
Performance Tips
- Particle Pool: Reuse dead particles instead of creating new ones
- Limit Count: Cap maximum particles (e.g., 500) to prevent slowdowns
- Simple Shapes: Circles are faster than images
- Batch Rendering: Draw particles with same color together
- Skip Physics: For simple effects, just update position without gravity/friction
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:
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!
You've learned the core of game physics:
- Physics Basics: Velocity, acceleration, gravity, bouncing
- Collision Response: Detection, separation, elastic collisions
- Platformer Physics: Variable jump, coyote time, jump buffering
- Particle Systems: Visual effects and polish
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.