Physics for the game developer — from a single falling dot to fire, explosions, and orbital systems.
Open a particle system codebase and it can feel overwhelming — emitters, pools, force fields, blend modes. But strip it all away and there's one idea underneath everything:
A particle is a position that changes over time according to a velocity that changes over time according to an acceleration.
That sentence is the whole physics engine. In code, a particle is a struct with three fields. On each frame you apply two updates in order:
velocity += acceleration * dt position += velocity * dtThat's Euler integration — the roughest possible numerical integrator, and the one every game particle system uses because it's fast and the error doesn't matter when particles only live for a second or two.
Gravity is just an acceleration with a fixed downward direction: ay = 9.8 m/s². Nothing more exotic than that. In a 2D canvas where y increases downward, you add to vy each frame. The particle accelerates, moves, and traces a parabolic arc.
// The complete single-particle state const p = { x: 200, y: 50, // position (pixels) vx: 80, vy: -20, // velocity (pixels/sec) }; const GRAVITY = 320; // px/s², roughly 9.8 m/s² scaled to screen function update(dt) { p.vy += GRAVITY * dt; // integrate acceleration → velocity p.x += p.vx * dt; // integrate velocity → position p.y += p.vy * dt; }
Hit the canvas below and watch a single particle launch from your click position. The trail shows its path. Try launching from different spots — the parabola shape never changes, only the starting velocity does.
Notice what happens at gravity = 0: the particle drifts in a straight line forever. That's inertia — Newton's first law. Every particle effect you build is just this: particles in inertial motion, nudged by forces.
The fading trail above is drawn by clearing the canvas to a semi-transparent black each frame instead of fully opaque black. The "memory" of previous positions persists as fade-out ghosts. This is a common trick — it costs nothing and gives motion a sense of history.
One particle is a toy. A hundred is a system. The question shifts from "how does one particle move?" to "how do we manage many?"
The answer in every production particle system is a pool: a pre-allocated array of particle slots, each marked either alive or dead. When the emitter wants to spawn a new particle, it finds the first dead slot and initializes it. When a particle's lifetime expires, it's marked dead — its slot is available for the next spawn. No allocation, no garbage collection, just slot flipping.
const POOL_SIZE = 500; const pool = Array.from({ length: POOL_SIZE }, () => ({ alive: false, x: 0, y: 0, vx: 0, vy: 0 })); function spawn(x, y, vx, vy) { const slot = pool.find(p => !p.alive); if (!slot) return; // pool exhausted — drop this spawn slot.alive = true; slot.x = x; slot.y = y; slot.vx = vx; slot.vy = vy; }
The emitter is a point in space that fires new particles at some rate — say, 60 per second. Each spawned particle gets a random velocity spread around some base direction. That spread is what makes a fountain look like a fountain instead of a laser beam.
vx = baseVx + (Math.random() - 0.5) * spread vy = baseVy + (Math.random() - 0.5) * spreadPull the Lifetime slider to its minimum. The fountain stays the same shape but particles vanish almost immediately. Now pull it high — the same fountain becomes a dense cloud. Lifetime is one of the most powerful controls for the visual feel of an effect.
The burst mode is just the same emitter fired all at once (rate = ∞ for one frame, then 0). Click anywhere on the canvas to trigger one. Explosions are just bursts.
The fountain is shooting upward and gravity pulls it back down. What happens if you set gravity to 0? You'd get a cone expanding outward forever — like stars ejected from a supernova. What if gravity is negative (upward)? You'd get a drain effect: particles swirling upward, narrowing as they accelerate. The same emitter, the same spread math — only the force changes the story.
So far particles have one force: gravity, a constant downward acceleration. Real effects layer multiple forces. Each force just adds an acceleration contribution each frame.
Wind is a horizontal acceleration — same idea as gravity, different direction. The particles drift sideways. Combine wind and gravity and you get the slanting rain of every storm scene.
Drag (air resistance) is the force that makes smoke billow instead of shooting off like bullets. It's a force that opposes velocity, proportional to speed. The formula is simple:
acceleration_drag = -velocity * drag_coefficientOr equivalently, you multiply velocity by a factor slightly less than 1 each frame:
// Drag: exponential velocity decay p.vx *= (1 - drag * dt); p.vy *= (1 - drag * dt); // Wind: constant horizontal push p.vx += wind * dt; // Gravity: constant downward pull p.vy += gravity * dt;
The interaction of drag and gravity is what makes particles reach a terminal velocity — the speed at which drag exactly cancels gravity. Heavy particles (low drag) fall fast; light ones (high drag) drift slowly. Adjust both sliders below to feel the difference.
The turbulence slider adds a small random velocity jitter each frame — not a real physical force, just noise. But it's what separates smoke from confetti. Physical accuracy isn't the goal; visual plausibility is. Games use cheap tricks everywhere.
In games, the question is never "is this physically accurate?" It's "does the player believe it?" Those are surprisingly different questions.
Every particle has a limited lifespan. Tracking that lifespan lets you do one of the most powerful things in particle design: make properties evolve over the particle's lifetime.
Each particle carries a life value that starts at 1.0 and counts down to 0.0 over its lifetime. This single float is the key to everything that changes:
// Each frame: p.life -= dt / p.maxLife; // counts 1.0 → 0.0 if (p.life <= 0) p.alive = false; // Use life to derive everything else: const t = p.life; // 1 = just born, 0 = about to die const alpha = t; // fade out linearly const size = lerp(0, 6, t); // shrink over time const color = lerpColor(endColor, startColor, t);
The lerp function is everywhere in particle work: lerp(a, b, t) = a + (b-a)*t. It interpolates between two values based on the lifetime progress. Color lerping is just three lerps, one per channel.
The demo below shows the same fountain with different color-over-lifetime presets. Notice how much personality each one adds — the same physics, radically different feel.
The "Smoke" preset is instructive: particles start small and dark, grow larger and lighter over their lives. This mimics how real smoke expands and fades. The physics are identical to the fire particles — only the color and size curves differ. The artist's job in a particle system is almost entirely about tuning these curves.
Linear lerp produces mechanical, robotic motion. Game particle systems usually use easing curves on the lifetime — squaring the value (ease-in), or using 1 - (1-t)² (ease-out). Squaring the alpha produces a particle that stays bright for most of its life and fades quickly near the end — much more natural than a linear fade.
Particles that pass through floors and walls look wrong instantly. Adding collision detection is straightforward for flat surfaces: check if the particle has crossed the boundary, move it back, and reflect its velocity.
The key quantity is the restitution coefficient (bounciness) — a value from 0 (perfectly inelastic, dead stop) to 1 (perfectly elastic, no energy lost). Multiply the outgoing velocity component by this value after reflection:
// Floor collision at y = floorY if (p.y >= floorY) { p.y = floorY; // push back above floor p.vy = -p.vy * restitution; // reflect and lose energy p.vx *= friction; // friction slows horizontal motion } // Wall collisions are the same idea, horizontal axis if (p.x <= 0 || p.x >= width) { p.vx = -p.vx * restitution; }
At restitution = 0, particles splat on the floor — think mud, blood, wet paint. At restitution = 0.9, they bounce energetically for a long time. At restitution = 1.0, they never stop — physically correct but visually exhausting in games. Most real effects use values between 0.2 and 0.7.
The "Hot Sparks" preset uses a high restitution with small particles, adding a short wake trail. The sparks bounce and scatter like metal sparks off a grinder. The same physics but very different character than the rain — because rain has restitution near zero (drops don't bounce; they splat).
Try adding a mental drag force to the rain demo. With drag, raindrops fall at terminal velocity and don't accelerate all the way down. Real raindrops hit the ground at around 9 m/s regardless of how high they started — because drag limits their speed. In the marbles preset, drag would make them feel like they're falling through oil rather than air. The gravity and restitution values are the same; drag is what changes the narrative.
Everything from the previous five sections combines into three canonical game effects. Each is built from the same primitives — pools, emitters, forces, lifetime curves — just configured differently.
An explosion is a single burst emission: all particles spawn simultaneously from one point with velocities spread in all directions. The key parameter is the velocity distribution — a uniform circle gives a clean ring; a gaussian distribution gives a denser center with falloff; adding an outward bias gives the classic shockwave look.
A shockwave layer is often added as a separate ring-shaped effect with a very short lifetime that expands rapidly outward — separate from the particle burst but timed together.
Fire is a continuous emitter, not a burst. Particles spawn from a base area, drift upward (negative gravity or a strong upward base velocity), and use a fire color curve: white-hot at birth, yellow, orange, then fade to red and transparent at death. Adding slight horizontal turbulence makes it flicker. Smoke particles often emit from the top of the fire region with their own inverse color curve — dark at birth, lighter gray as they rise and expand.
A gravity well applies a point-attractor force: at each frame, compute the vector from each particle to the well center, normalize it, and add a scaled version of it to the particle's velocity. The force falls off with distance (usually 1/r² or 1/r). Particles spiral inward and either vanish when they get too close, or orbit if their tangential velocity is high enough.
The demo below lets you switch between all three. In gravity well mode, click to reposition the well center.
Each of these effects has hundreds of parameters you could tune. But the underlying system is the same six lines of physics from §1: position, velocity, acceleration, and a force list. The visual variety comes entirely from how you configure the emitter and color curves.
A great particle artist doesn't write different physics for every effect. They write one physics loop and spend their craft on the configuration.
Everything here runs on the CPU — fine for a few hundred particles. At tens of thousands you move to GPU particles: particles stored in textures, physics computed in a fragment shader (using the same UV math from §2 and §3 of any shader primer), and the draw step handled by instanced rendering. The physics equations don't change at all. Only the hardware they run on does.
Sub-emitters (particles spawning particles — sparks off of an explosion leaving smoke trails), sprite sheets for non-point particles, and collision with arbitrary shapes are natural next steps. But they're all built on the same foundation you've been playing with here.