mcpField
P
phaser-particles

phaser-particles

github.com/yakoub-aiDevelopment2969 installsUpdated 9w ago
Repository

About phaser-particles

phaser-particles is a skill with practical guidance for building and tuning particle effects in Phaser 4 games. It covers emitter setup, effect behaviors, performance limits, and runtime checks for game developers.

Key features

  1. 01Burst and continuous emitters
  2. 02Particle motion and appearance configuration
  3. 03Emit zones and death zones
  4. 04Following moving game objects
  5. 05Particle count and performance guidance
  6. 06Runtime particle verification

Use cases

  1. 01Adding hit sparks or pickup bursts
  2. 02Creating rain, smoke, or fire effects
  3. 03Keeping off-screen particles from consuming resources
  4. 04Testing particle counts under heavy load

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/yakoub-ai/phaser4-gamedev --skill phaser-particles

After installing, type/phaser-particlesin Claude Code to confirm it loaded.

Bundled filesreferences/particle-recipes.md

Details

From the project README

Phaser 4 Particles

Particles are how a game reads as responsive. A hit that spawns three sparks feels twice as solid as the same hit without them, for a fraction of the work of any other juice technique.

They are also the most common cause of a Phaser game running at 20fps.


The emitter

const emitter = this.add.particles(x, y, 'texture', config);

One call creates and adds an emitter. It is a normal game object: it has depth, scroll factor, alpha, and can go in a container.

A worked config, with every field that matters in practice:

const sparks = this.add.particles(0, 0, 'flare', {
  // Motion
  speed: { min: 50, max: 150 },      // px/sec; a range gives natural spread
  angle: { min: 0, max: 360 },       // degrees; direction of travel
  gravityY: 300,                     // px/sec² — falling embers, debris
  accelerationX: 0,

  // Appearance over life — {start, end} interpolates across the particle's lifespan
  scale: { start: 1, end: 0 },
  alpha: { start: 1, end: 0 },
  rotate: { start: 0, end: 360 },
  tint: [0xffaa00, 0xff2200],        // an array picks randomly per particle
  blendMode: 'ADD',                  // ADD for light: fire, magic, sparks

  // Timing
  lifespan: 600,                     // ms each particle lives
  frequency: 40,                     // ms between emissions; -1 = explode mode only
  quantity: 2,                       // particles per emission

  // Budget — always set this
  maxParticles: 100,

  emitting: false,                   // create dormant; start it deliberately
});

{ start, end } is the workhorse. Most of what makes a particle effect look deliberate rather than sprayed is scale and alpha shrinking and fading over the lifespan. A particle that pops out of existence at full size looks like a bug.


Emit once, or emit continuously

Two modes, and mixing them up is why an effect either never appears or never stops.

Burst — an explosion, a hit spark, a pickup pop:

const burst = this.add.particles(0, 0, 'spark', {
  speed: { min: 100, max: 300 },
  lifespan: 400,
  scale: { start: 0.8, end: 0 },
  emitting: false,                    // dormant until told otherwise
});

burst.explode(20, x, y);              // 20 particles, right here, right now

Continuous — a torch, a smoke plume, rain:

const smoke = this.add.particles(x, y, 'smoke', {
  frequency: 100,
  quantity: 1,
  lifespan: 2000,
  emitting: true,
});

smoke.stop();      // stop emitting; existing particles live out their lifespan
smoke.start();
smoke.pause();     // freeze everything, including live particles
smoke.resume();

stop() and pause() are different and the difference is visible: stop() lets the plume dissipate naturally, pause() freezes it mid-air.

Cleaning up: an emitter created per explosion and never destroyed is a leak. Either create one emitter and re-explode() it, or destroy after the last particle dies:

// Reuse — the right default
this.hitSparks.explode(8, enemy.x, enemy.y);

// Or fire-and-forget, cleaned up properly
const e = this.add.particles(x, y, 'spark', { lifespan: 400, emitting: false });
e.explode(20);
this.time.delayedCall(500, () => e.destroy());

Following a moving object

emitter.startFollow(player, 0, -10);   // offsetX, offsetY
emitter.stopFollow();

Use this rather than setting the emitter position in update(). startFollow tracks between frames, so a fast-moving object leaves a continuous trail instead of a dotted line of clumps at each frame's position.


Zones

Emit zones — where particles are born:

// Along the edge of a shape: a ring, a magic circle
emitter.addEmitZone({
  type: 'edge',
  source: new Phaser.Geom.Circle(0, 0, 50),
  quantity: 32,
});

// Anywhere inside a shape: rain across the top of the screen
emitter.addEmitZone({
  type: 'random',
  source: new Phaser.Geom.Rectangle(0, 0, 800, 10),
});

Death zones — where particles are destroyed:

emitter.addDeathZone({
  type: 'onEnter',                                  // or 'onLeave'
  source: new Phaser.Geom.Rectangle(0, 400, 800, 200),
});

onLeave with a screen-sized rectangle is how you stop off-screen particles costing anything.

Gravity wells — attract or repel:

emitter.createGravityWell({ x: 400, y: 300, power: 3, epsilon: 100, gravity: 100 });

Vortexes, black holes, a magnet pickup pulling coins in.


Performance — read this before shipping

Particles are the most common cause of frame-rate collapse in a Phaser game, and the collapse arrives long after the code was written, because it only shows up under load.

Always set maxParticles. An emitter without a cap and a short frequency will happily allocate until the frame budget is gone. This is the single highest-value line in any emitter config.

maxParticles: 100,       // hard ceiling

Sizing it: lifespan / frequency * quantity is the steady-state count. At lifespan: 2000, frequency: 40, quantity: 2 that is 100 live particles from one emitter — and if you have ten such emitters, a thousand.

Other levers, roughly in order of payoff:

LeverWhy
Shorter lifespanFewer live particles for the same visual density
Death zones on onLeaveOff-screen particles cost exactly as much as visible ones
Fewer, larger particles20 big sparks read better than 200 tiny ones and cost 10× less
One emitter, reusedEmitter churn costs more than the particles do
Smaller texturesParticle textures rarely need to exceed 32×32
Avoid ADD on mobileAdditive blending is fill-rate hungry, and mobile is fill-rate bound
stopAfterEmits N particles then stops on its own — no cleanup logic to forget

Mobile budget: a few hundred simultaneous particles across the whole game, not per effect. Halve your desktop counts and check on a real device — see skills/phaser-mobile/references/device-profiles.md.

For thousands of similar sprites, particles are the wrong tool — see SpriteGPULayer in skills/phaser-fx/references/renderer-and-shaders.md.


Why particles aren't showing

In rough order of likelihood:

SymptomCause
Nothing at allemitting: false and you never called start() or explode()
Nothing at allThe texture key does not exist — check the loader, and the playtest asset check
Nothing at allfrequency: -1 (explode-only) with no explode() call
InvisibleEmitter depth is below the background — emitter.setDepth(n)
Invisiblealpha: { start: 0 } — the start value, not the end, is zero
One frame then gonelifespan far too short
Wrong placeEmitter x/y are world coordinates; inside a container they are local
Not followingPosition set in update() instead of startFollow()
Frame rate collapseNo maxParticles

Verifying particles at runtime

Particle bugs type-check perfectly — a wrong texture key is a string, and an emitter that never starts is valid code. Prove it ran:

// playtest/particles-check.mjs
export default [
  { name: 'hit sparks fire on damage', action: 'eval',
    code: `scene('GameScene').hitEnemy(scene('GameScene').enemies[0])` },
  { name: 'particles actually spawned', action: 'expect',
    expect: { expression: `scene('GameScene').hitSparks.getAliveParticleCount()`, atLeast: 1 } },

  { name: 'and they are capped', action: 'sample',
    expression: `scene('GameScene').hitSparks.getAliveParticleCount()`,
    duration: 3000, interval: 100,
    expect: { stat: 'max', atMost: 100 } },

  { name: 'frame rate survives the effect', action: 'sample',
    expression: `game.loop.actualFps`, duration: 3000, interval: 100,
    expect: { stat: 'min', atLeast: 50 } },
];

The max assertion on the alive count is the one that catches an uncapped emitter before a player does. Run it while the game is under its heaviest particle load, not at rest.


Additional Resources

Reference Files
  • references/particle-recipes.md — Complete configs for the effects games actually need: explosion, fire, smoke, rain, snow, sparks, blood, magic, dust, muzzle flash, confetti, trails — each with the reasoning for its numbers.
Related
  • skills/phaser-fx/ — filters and blend modes; ADD particles plus a bloom action is the standard "magic" look
  • skills/phaser-analyze/references/performance-playbook.md — when particles cost frames
  • skills/phaser-mobile/references/device-profiles.md — mobile particle budgets

Comments

Markdown supported