| name | particles |
| description | Use this skill when creating particle effects in Phaser 4. Covers ParticleEmitter, emission zones, death zones, particle properties, textures, gravity wells, and particle movement. Triggers on: particles, emitter, particle effect, explosion, fire, smoke. |
Particle System
Creating and controlling particle effects in Phaser 4 -- ParticleEmitter creation and configuration, emitter ops (value formats), gravity wells, emission and death zones, flow vs burst modes, following game objects, and particle callbacks.
Key source paths: src/gameobjects/particles/
Related skills: ../sprites-and-images/SKILL.md, ../loading-assets/SKILL.md
Quick Start
const emitter = this.add.particles(400, 300, 'flares', {
frame: 'red',
speed: 200,
lifespan: 2000,
scale: { start: 1, end: 0 },
alpha: { start: 1, end: 0 },
gravityY: 150
});
const burst = this.add.particles(400, 300, 'flares', {
frame: 'blue',
speed: { min: 100, max: 300 },
lifespan: 1000,
scale: { start: 0.5, end: 0 },
emitting: false
});
burst.explode(20);
Core Concepts
ParticleEmitter
ParticleEmitter extends GameObject and is added directly to the display list. It is both a game object (positionable, scalable, maskable) and the emitter itself. There is no separate manager -- this.add.particles() returns a ParticleEmitter instance.
Factory signature:
this.add.particles(x, y, texture, config);
Mixins: AlphaSingle, BlendMode, Depth, Lighting, Mask, RenderNodes, ScrollFactor, Texture, Transform, Visible. So you can call setPosition(), setScale(), setDepth(), setBlendMode(), setMask(), setScrollFactor(), etc.
Particle
A lightweight object owned by its emitter. Key properties: x, y, velocityX/Y, accelerationX/Y, scaleX/Y, alpha, angle, rotation, tint, life (total ms), lifeCurrent (remaining ms), lifeT (0-1 normalized), bounce, delayCurrent, holdCurrent. Particles are pooled internally -- you never create them manually.
EmitterOp Value Formats
Most config properties (speed, scale, alpha, angle, x, y, etc.) accept flexible value formats:
x: 400
x: [100, 200, 300, 400]
x: { min: 100, max: 700 }
x: { min: 100, max: 700, int: true }
x: { random: [100, 700] }
scale: { start: 0, end: 1 }
scale: { start: 0, end: 1, ease: 'bounce.out' }
scale: { start: 4, end: 0.5, random: true }
x: { values: [50, 500, 200, 800], interpolation: 'catmull' }
x: { steps: 32, start: 0, end: 576 }
x: { steps: 32, start: 0, end: 576, yoyo: true }
x: {
onEmit: (particle, key, t, value) => value,
onUpdate: (particle, key, t, value) => value
}
x: (particle, key, t, value) => value + 50
Emit-only (no onUpdate): angle, delay, hold, lifespan, quantity, speedX, speedY.
Emit + Update (support start/end, onUpdate): accelerationX/Y, alpha, bounce, maxVelocityX/Y, moveToX/Y, rotate, scaleX/Y, tint, x, y.
Flow vs Explode (Burst)
Flow mode (frequency >= 0): emits quantity particles every frequency ms. Default is frequency: 0 (every frame) with emitting: true.
Explode mode (frequency = -1): emits a batch all at once, then stops.
emitter.flow(100, 5);
emitter.flow(100, 5, 50);
emitter.explode(30, 200, 400);
emitter.explode(30);
Common Patterns
Scale, Alpha, and Color Over Lifetime
this.add.particles(400, 300, 'spark', {
lifespan: 2000,
speed: 100,
scale: { start: 1, end: 0, ease: 'power2' },
alpha: { start: 1, end: 0, ease: 'cubic.in' }
});
Color Interpolation
The color property interpolates through an array of colors over particle lifetime (overrides tint):
this.add.particles(400, 300, 'spark', {
lifespan: 2000, speed: 100, scale: { start: 0.5, end: 0 },
color: [0xfacc22, 0xf89800, 0xf83600, 0x9f0404], colorEase: 'quad.out'
});
Tinting Particles
this.add.particles(400, 300, 'spark', { tint: 0xff0000 });
this.add.particles(400, 300, 'spark', { tint: { start: 0xffffff, end: 0xff0000 } });
Gravity Wells
A GravityWell applies inverse-square gravitational force, pulling (or repelling with negative power) particles toward a point.
const emitter = this.add.particles(400, 300, 'spark', {
speed: 100, lifespan: 4000, scale: { start: 0.4, end: 0 }, quantity: 2
});
const well = emitter.createGravityWell({
x: 400, y: 300, power: 2, epsilon: 100, gravity: 50
});
well.x = 300;
well.power = -1;
const well2 = new Phaser.GameObjects.Particles.GravityWell(500, 200, 3, 100, 50);
emitter.addParticleProcessor(well2);
emitter.removeParticleProcessor(well2);
Emission Zones (Random)
A RandomZone spawns particles at random positions within a shape. The source must have a getRandomPoint(point) method -- all Phaser geometry classes (Circle, Ellipse, Rectangle, Triangle, Polygon, Line) support this, or provide a custom source:
this.add.particles(400, 300, 'spark', {
speed: 50, lifespan: 2000,
emitZone: { type: 'random', source: new Phaser.Geom.Circle(0, 0, 100) }
});
emitter.addEmitZone({
type: 'random',
source: {
getRandomPoint: (point) => {
const a = Math.random() * Math.PI * 2;
point.x = Math.cos(a) * 100;
point.y = Math.sin(a) * 50;
return point;
}
}
});
Emission Zones (Edge)
An EdgeZone places particles sequentially along shape edges. The source must have a getPoints(quantity, stepRate) method. Curves, Paths, and all geometry shapes support this:
this.add.particles(400, 300, 'spark', {
lifespan: 1500, speed: 20,
emitZone: {
type: 'edge',
source: new Phaser.Geom.Circle(0, 0, 150),
quantity: 48,
yoyo: false,
seamless: true
}
});
emitter.addEmitZone({ type: 'edge', source: geom, quantity: 50, yoyo: false, seamless: true });
Multiple emission zones: Pass an array to emitZone or call addEmitZone() multiple times. Zones iterate in sequence. The total property controls how many particles emit before rotating to the next zone (-1 = never rotate).
this.add.particles(400, 300, 'spark', {
emitZone: [
{ type: 'random', source: new Phaser.Geom.Circle(0, 0, 50) },
{ type: 'random', source: new Phaser.Geom.Circle(200, 0, 50) }
]
});
Death Zones
A DeathZone kills particles when they enter (or leave) a region. The source must have a contains(x, y) method.
this.add.particles(400, 100, 'spark', {
speed: 200, lifespan: 5000, gravityY: 100,
deathZone: { type: 'onEnter', source: new Phaser.Geom.Rectangle(300, 400, 200, 50) }
});
this.add.particles(400, 300, 'spark', {
speed: 100, lifespan: 5000,
deathZone: { type: 'onLeave', source: new Phaser.Geom.Circle(400, 300, 150) }
});
emitter.addDeathZone({
type: 'onEnter',
source: { contains: (x, y) => x > 600 && y > 400 }
});
Following a Game Object
const player = this.add.sprite(100, 100, 'player');
const emitter = this.add.particles(0, 0, 'spark', {
speed: 50, lifespan: 800, scale: { start: 0.5, end: 0 }
});
emitter.startFollow(player);
emitter.startFollow(player, 10, -20);
emitter.startFollow(player, 0, 0, true);
emitter.stopFollow();
this.add.particles(0, 0, 'spark', { follow: player, followOffset: { x: 0, y: -20 } });
Particle Callbacks
const emitter = this.add.particles(400, 300, 'spark', {
speed: 100, lifespan: 2000,
emitCallback: (particle, emitter) => { },
deathCallback: (particle) => { }
});
emitter.onParticleEmit((particle, emitter) => { });
emitter.onParticleDeath((particle) => { });
emitter.forEachAlive((particle, emitter) => { });
Duration, StopAfter, and Advance
this.add.particles(400, 300, 'spark', { speed: 100, duration: 3000 });
this.add.particles(400, 300, 'spark', { speed: 100, stopAfter: 50 });
this.add.particles(400, 300, 'spark', { speed: 100, lifespan: 2000, advance: 2000 });
Particle Bounds (Bounce)
this.add.particles(400, 300, 'spark', {
speed: 200, lifespan: 5000, bounce: 0.8,
bounds: { x: 100, y: 100, width: 600, height: 400 },
collideLeft: true, collideRight: true, collideTop: true, collideBottom: true
});
Texture Frames and Animations
this.add.particles(400, 300, 'flares', { frame: ['red', 'green', 'blue'] });
this.add.particles(400, 300, 'flares', {
frame: { frames: ['red', 'green', 'blue'], cycle: true, quantity: 4 }
});
this.add.particles(400, 300, 'explosion', { anim: 'explode_anim', lifespan: 1000 });
this.add.particles(400, 300, 'sheet', {
anim: { anims: ['fire', 'smoke'], cycle: false, quantity: 1 }
});
Sorting Particles
this.add.particles(400, 300, 'spark', { sortProperty: 'y', sortOrderAsc: true });
Custom Particle Processor
Extend ParticleProcessor to apply custom per-particle logic each frame. Implement update(particle, delta, step, t):
class WindProcessor extends Phaser.GameObjects.Particles.ParticleProcessor {
constructor (windX, windY) {
super(0, 0);
this.windX = windX;
this.windY = windY;
}
update (particle, delta, step, t) {
particle.velocityX += this.windX * step;
particle.velocityY += this.windY * step;
}
}
emitter.addParticleProcessor(new WindProcessor(0.5, 0));
Custom Particle Class
Extend Particle and override update for per-particle behavior. Set via particleClass in config:
class TrailParticle extends Phaser.GameObjects.Particles.Particle {
update (delta, step, processors) {
const result = super.update(delta, step, processors);
this.alpha = this.lifeT;
return result;
}
}
this.add.particles(400, 300, 'spark', {
particleClass: TrailParticle,
speed: 100, lifespan: 2000
});
Configuration Reference
ParticleEmitterConfig -- Simple Properties
| Property | Type | Default | Description |
|---|
active | boolean | true | False = emitter does not update at all |
emitting | boolean | true | False = no new particles (alive ones still update) |
blendMode | string/number | 0 | Blend mode for rendering |
frequency | number | 0 | ms between flow cycles; 0 = every frame; -1 = explode |
gravityX, gravityY | number | 0 | Gravity in px/s^2 |
maxParticles | number | 0 | Hard limit on total particle objects (0 = unlimited) |
maxAliveParticles | number | 0 | Max alive particles at once (0 = unlimited) |
duration | number | 0 | Auto-stop after ms (0 = forever) |
stopAfter | number | 0 | Auto-stop after N particles emitted (0 = unlimited) |
advance | number | 0 | Fast-forward on creation (ms) |
radial | boolean | true | True = speed+angle; false = speedX/speedY |
particleBringToTop | boolean | true | New particles render on top |
timeScale | number | 1 | Time multiplier for updates |
follow | Vector2Like | null | Object to follow |
followOffset | Vector2Like | | Offset from follow target |
trackVisible | boolean | false | Match follow target's visibility |
reserve | number | | Pre-allocate particle objects |
particleClass | function | Particle | Custom particle class |
sortProperty | string | | Particle property to sort by |
sortOrderAsc | boolean | | Sort ascending if true |
ParticleEmitterConfig -- EmitterOp Properties
All accept the flexible value formats described above.
| Property | Default | E/U | Description |
|---|
x, y | 0 | E+U | Particle offset from emitter |
speed | 0 | E | Radial speed (sets speedX, deactivates speedY) |
speedX, speedY | 0 | E | Directional speed (sets radial=false) |
angle | {min:0,max:360} | E | Emission angle in degrees |
scale | 1 | E+U | Uniform scale (sets scaleX, deactivates scaleY) |
scaleX, scaleY | 1 | E+U | Non-uniform scale |
alpha | 1 | E+U | Alpha transparency |
rotate | 0 | E+U | Rotation in degrees |
tint | 0xffffff | E+U | Tint color (WebGL) |
color | | E+U | Color array to interpolate (overrides tint) |
colorEase | | | Ease for color interpolation |
lifespan | 1000 | E | Lifetime in ms |
delay | 0 | E | Delay before visible (ms) |
hold | 0 | E | Hold at end of life before dying (ms) |
quantity | 1 | E | Particles per flow cycle |
accelerationX/Y | 0 | E+U | Acceleration (px/s^2) |
maxVelocityX/Y | 10000 | E+U | Max velocity |
bounce | 0 | E+U | Bounce restitution (0-1) |
moveToX, moveToY | 0 | E+U | Target position (overrides angle/speed) |
E = emit-only, E+U = emit + update (supports start/end, onUpdate)
Zone Config Properties
| Config Key | Type | Properties |
|---|
emitZone | object or array | { type: 'random', source: <shape> } |
| | { type: 'edge', source: <shape>, quantity, stepRate, yoyo, seamless, total } |
deathZone | object or array | { type: 'onEnter'|'onLeave', source: <shape> } |
bounds | object | { x, y, width, height } or { x, y, w, h } |
Events
All events are emitted on the ParticleEmitter instance itself.
| Event | String | Callback Args | When |
|---|
START | 'start' | (emitter) | start() is called and emitter begins emitting |
STOP | 'stop' | (emitter) | stop() is called, or duration/stopAfter limit reached |
COMPLETE | 'complete' | (emitter) | Final alive particle dies after emitter has stopped |
EXPLODE | 'explode' | (emitter, particle) | explode() is called |
DEATH_ZONE | 'deathzone' | (emitter, particle, zone) | A death zone kills a particle |
emitter.on('stop', (emitter) => { });
emitter.on('complete', (emitter) => { });
emitter.on('deathzone', (emitter, particle, zone) => { });
API Quick Reference
ParticleEmitter Key Methods
Lifecycle: start(advance?, duration?), stop(kill?), pause(), resume(), flow(frequency, count?, stopAfter?), explode(count?, x?, y?), emitParticleAt(x?, y?, count?), emitParticle(count?, x?, y?), fastForward(time, delta?).
Config: setConfig(config), updateConfig(config).
Following: startFollow(target, offX?, offY?, trackVisible?), stopFollow().
Zones: addEmitZone(config), removeEmitZone(zone), clearEmitZones(), addDeathZone(config), removeDeathZone(zone), clearDeathZones().
Processors: createGravityWell(config), addParticleProcessor(processor), removeParticleProcessor(processor), getProcessors().
Bounds: addParticleBounds(x, y, w, h, collideL?, collideR?, collideT?, collideB?).
Callbacks/Iteration: onParticleEmit(cb, ctx?), onParticleDeath(cb, ctx?), killAll(), forEachAlive(cb, ctx?), forEachDead(cb, ctx?).
Counts: getAliveParticleCount(), getDeadParticleCount(), getParticleCount(), atLimit(), reserve(count).
Property setters: setParticleSpeed(x, y?), setParticleScale(x, y?), setParticleGravity(x, y), setParticleAlpha(value), setParticleTint(value), setParticleLifespan(value), setEmitterAngle(value), setQuantity(qty), setFrequency(freq, qty?), setRadial(value), setEmitterFrame(frames, random?, qty?), setAnim(anims, random?, qty?).
Sorting: setSortProperty(property, ascending?), setSortCallback(callback), depthSort().
Utility: getBounds(padding?, advance?, delta?, output?), overlap(target).
GravityWell
| Property/Method | Description |
|---|
x, y | World position of the well |
power | Force strength (negative to repel) |
epsilon | Min distance for force calc (default 100) |
gravity | Gravitational constant (default 50) |
active | Enable/disable processing (inherited from ParticleProcessor) |
Constructor: new GravityWell(x, y, power, epsilon, gravity) or new GravityWell(config) where config is { x, y, power, epsilon, gravity }.
Gotchas
- No ParticleEmitterManager: Removed in v3.60.
this.add.particles() returns a ParticleEmitter directly.
speed vs speedX/speedY: speed sets speedX and deactivates speedY (radial). speedX/speedY switches to point mode (radial: false).
scale vs scaleX/scaleY: scale applies to scaleX and deactivates scaleY. Use both for non-uniform scaling.
color overrides tint: They are mutually exclusive; color (array) takes priority.
moveToX/moveToY: Both must be set to activate. Overrides angle and speed.
emitting vs active: emitting: false = no new particles but alive ones update. active: false = entire emitter frozen.
stop vs complete: 'stop' fires when emission stops. 'complete' fires when the last alive particle dies.
frequency: 0: Means emit every frame (max rate), not "never." Use emitting: false to prevent emission.
frequency: -1: Puts the emitter in explode mode -- it will not flow automatically. Use explode() to emit bursts.
hold freezes particle: After lifespan expires, hold keeps the particle visible and frozen for the specified ms before it dies. Useful for trail/lingering effects.
advance fast-forwards: Pre-warms the emitter by simulating the given ms on creation, so particles are already visible on the first frame.
reserve(count) pre-allocates: Call reserve() or set reserve in config to pre-create particle objects upfront, avoiding GC spikes during gameplay from on-demand allocation.
- Zone source methods: RandomZone needs
getRandomPoint(point). EdgeZone needs getPoints(quantity, stepRate). DeathZone needs contains(x, y).
- Particle pool:
maxParticles limits total objects (not alive count). Use maxAliveParticles for visible limit.
- Texture required: The emitter needs a valid texture key. Use
frame config for multi-frame textures.
Source Files
See references/REFERENCE.md for the full source file map. Key entry points: src/gameobjects/particles/ParticleEmitter.js (main class), src/gameobjects/particles/Particle.js (individual particle), src/gameobjects/particles/zones/ (zone classes).