🌾 FarmHeart

← Back to Blog

Building Particle Systems for Browser Games

ParticlesWebGLEffects · 10 min read

Rain streaking across the screen. Sparks flying from a blacksmith's anvil. Dust kicking up as a tractor plows a field. Particle systems turn a flat browser game into something that feels alive. And with the right approach, you can run thousands of particles at 60fps in a browser tab.

What Is a Particle System?

A particle system is a collection of small, short-lived objects (particles) that are emitted from a source, move according to simple rules, and die after a set lifetime. Each particle has position, velocity, color, size, and age. The emitter controls spawn rate, direction spread, initial velocity range, and lifetime.

The magic of particles is emergence — simple rules on individual particles produce complex, organic-looking effects when thousands of them interact. No two frames look identical, which is what makes rain look like rain instead of a repeating sprite animation.

Canvas 2D vs. WebGL Particles

For small particle counts (under 500), Canvas 2D is perfectly fine. Draw colored circles with ctx.arc(), apply alpha fade-out based on age, and you've got campfire sparks or floating pollen.

For serious particle effects — rain across the entire screen, dense fog, explosion debris — you need WebGL. The GPU can transform and render tens of thousands of point sprites per frame without breaking a sweat, while the CPU would choke on the same workload through Canvas 2D.

ApproachMax Particles (60fps)Best For
Canvas 2D circles~300-500Simple sparkles, dust motes
Canvas 2D images~100-200Textured particles (leaves, snowflakes)
WebGL point sprites~50,000+Rain, fire, dense effects
WebGL instanced quads~20,000+Textured, rotatable particles

The Core Particle Loop

Every particle system follows the same lifecycle:

  1. Emit — spawn new particles at a configurable rate. Randomize initial properties within ranges (velocity ±20%, angle ±15°, lifetime ±30%).
  2. Update — each frame, advance every particle: add velocity to position, apply gravity/wind, age by dt, interpolate color/size/alpha based on age ratio.
  3. Cull — remove particles whose age exceeds their lifetime. Don't use splice() — swap the dead particle with the last alive particle and decrement your count.
  4. Render — draw all alive particles in a single batch.

Object Pooling: The Performance Key

The number one performance killer in JavaScript particle systems is garbage collection. If you new Particle() every spawn and let dead particles get GC'd, you'll see frame stutters every few seconds when the collector runs.

Instead, pre-allocate a fixed-size array of particle objects at startup. "Spawning" means resetting an inactive particle's properties. "Killing" means marking it inactive. Zero allocations, zero GC pressure, buttery smooth.

Pool sizing: Allocate 2x your expected max visible particles. If you're emitting 100 rain drops per second with a 2-second lifetime, pool 400 particles. The memory cost is negligible — a particle is just 8 floats (~64 bytes).

Common Effects and How to Build Them

Rain

Emit thin vertical lines (or stretched point sprites) from above the viewport. Gravity pulls them down fast. Short lifetime (0.5–1s). Color: white-blue at low alpha. Add occasional splash particles on impact at y = ground that expand outward and fade quickly. Wind: add a constant horizontal velocity component.

Fire

Emit from a small area at the base. Initial velocity: upward with random spread. Color interpolation: yellow → orange → red → transparent over the particle's life. Size: starts medium, shrinks. Add additive blending (gl.blendFunc(gl.SRC_ALPHA, gl.ONE)) for that glowing look. Emit smoke particles above the flame zone: gray, slow-rising, expanding, longer lifetime.

Dust/Dirt

Burst emit (all at once, not continuous) when something impacts the ground. Spread: hemisphere upward from impact point. Color: brown/tan. Size: small, growing slightly. Gravity: low (they float). Alpha: fade out over 1–2 seconds. Good for footsteps, tilling soil, and harvesting.

Magic/Sparkle

Emit in a circle or spiral pattern. No gravity. Color: cycle through hue over lifetime. Size: pulsing (use sin(age * frequency)). Trail: store previous positions and draw connecting lines or render multiple fading copies. This is where globalCompositeOperation = "lighter" in Canvas 2D really shines.

GPU-Side Particle Updates

For maximum performance, move the entire particle simulation to the GPU. Store particle data in vertex buffer attributes (position, velocity, birth time, lifetime), and let the vertex shader compute the current position each frame based on elapsed time. The CPU only needs to upload the current time as a uniform — zero per-particle CPU work.

This technique, sometimes called "stateless particles," works for effects with predictable physics (gravity, constant wind). For effects that need collision detection or dynamic forces, keep the simulation on the CPU and upload updated positions each frame via bufferSubData().

Integration Tips for Farming Games

Watch Particles in Action

FarmHeart features dynamic weather, crop effects, and seasonal particles — all in your browser.

Play FarmHeart Now