Lesson 19
Particles and screen shake: make actions feel like they happened.
Game juice is feedback. A coin burst, a damage flash or a tiny shake tells the player that the game heard them.
1. Particles are tiny temporary objects
A particle usually has position, velocity, color, size and life. It moves for a short time, fades, then gets removed. That is enough for coin bursts, dust, hit sparks, jump puffs and explosion debris.
const particles = [];
const MAX_PARTICLES = 220;
function addParticle(x, y, vx, vy, color, life, size) {
if (particles.length >= MAX_PARTICLES) particles.shift();
particles.push({
x, y, vx, vy,
color,
life,
maxLife: life,
size: size || 3
});
}
Source pattern: Supagames dungeon and platformer games use particle arrays for damage, jumps, rewards, deaths and victory bursts.
The cap matters. Without it, a busy scene can create thousands of particles and slow the browser. Good feedback should feel lively, not melt the frame rate.
2. Update backward when removing
Particles disappear often, so update the array backward. That way `splice` does not skip the next particle. Gravity and friction can make the same system work for sparks, smoke and dust.
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.97;
p.vy = p.vy * 0.97 + 0.08;
p.life--;
p.size *= 0.98;
if (p.life <= 0 || p.size < 0.2) {
particles.splice(i, 1);
}
}
}
Change only a few values per effect. Coin particles can rise slightly, debris can fall, magic can drift slowly and sparks can move fast with short life.
3. Draw with alpha from life
Particles look better when they fade. Compute alpha from life divided by max life. Reset `globalAlpha` after drawing so the rest of the game does not accidentally become transparent.
function drawParticles(ctx) {
for (const p of particles) {
ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
For pixel art, rectangles may fit better than circles. For magic, add glow with `shadowBlur`, but use it sparingly because shadows are more expensive.
4. Screen shake should be short and meaningful
Screen shake is powerful because it moves the whole world. Use it only for impact: death, explosion, heavy landing, boss slam. A constant shake makes the game harder to read and can be uncomfortable.
let screenShake = 0;
function addShake(amount) {
screenShake = Math.max(screenShake, amount);
}
function renderWorld(ctx) {
ctx.save();
if (screenShake > 0) {
const ox = (Math.random() - 0.5) * screenShake;
const oy = (Math.random() - 0.5) * screenShake;
ctx.translate(ox, oy);
screenShake *= 0.86;
if (screenShake < 0.4) screenShake = 0;
}
drawLevel(ctx);
drawActors(ctx);
drawParticles(ctx);
ctx.restore();
}
Apply shake to the world, not to fixed UI. Health bars and buttons should stay readable while the arena reacts.
5. Floating text explains numbers
Damage numbers and reward text turn invisible arithmetic into visible feedback. They are particles with text instead of circles. Use them for upgrades, gold, critical hits and quest rewards.
const floatingTexts = [];
function addFloatingText(x, y, text, color) {
floatingTexts.push({ x, y, text, color, life: 60, vy: -1.2 });
}
function updateFloatingTexts() {
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const t = floatingTexts[i];
t.y += t.vy;
t.life--;
if (t.life <= 0) floatingTexts.splice(i, 1);
}
}
Do not show every tiny number in a busy game. Use text when the player needs confirmation: "UPGRADED", "+20", "BLOCKED", "CRITICAL".
6. Juice follows meaning
Effects should reinforce design. Jump particles appear at the feet. Hit sparks appear at contact. A boss slam shakes the arena. A collect burst moves upward. When effects match the action, the game feels responsive. When effects are random, they become glitter.
Start with three events: player takes damage, player collects reward, player wins. Add particles, sound and animation to those. Then expand. Juice is best when it is a language the player learns, not a confetti machine.
7. Make effects scale with importance
Not every event deserves the same effect. A coin can use five small particles. A level complete moment can use thirty particles, a short sound and a floating text label. A player death can use a stronger burst, screen shake and a pause before restart. This hierarchy teaches the player what matters. If a tiny pickup has the same visual force as defeating a boss, the game loses emotional range.
function rewardBurst(x, y, value) {
const count = value >= 50 ? 30 : value >= 10 ? 14 : 6;
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const s = 1 + Math.random() * (value >= 50 ? 5 : 3);
addParticle(x, y, Math.cos(a) * s, Math.sin(a) * s - 1, "#ffd93d", 24, 3);
}
if (value >= 50) addShake(5);
addFloatingText(x, y - 12, "+" + value, "#ffd93d");
}
Scaling also protects performance. You spend visual budget on moments the player will notice, rather than filling every frame with effects that compete for attention.