Skip to content

Particles

Estella provides a GPU-instanced 2D particle system with a C++ simulation backend. The ParticlePlugin is included by the engine by default.

ParticleEmitter Component

Add ParticleEmitter to an entity with Transform to emit particles from that position.

General

PropertyTypeDefaultDescription
enabledbooleantrueComponent active
playOnStartbooleantrueAuto-play when entity loads
loopingbooleantrueRestart after duration ends
durationnumber5Emission duration in seconds

Emission

PropertyTypeDefaultDescription
ratenumber10Particles emitted per second
burstCountnumber0Particles emitted per burst (0 = disabled)
burstIntervalnumber1Seconds between bursts
maxParticlesnumber1000Maximum alive particles

Lifetime

PropertyTypeDefaultDescription
lifetimeMinnumber5Minimum particle lifetime (seconds)
lifetimeMaxnumber5Maximum particle lifetime (seconds)

Shape

PropertyTypeDefaultDescription
shapenumber3Emission shape (see below)
shapeRadiusnumber100Radius for Circle and Cone shapes
shapeSizeVec2{100, 100}Width/height for Rectangle shape
shapeAnglenumber25Half-angle in degrees for Cone shape (0–360)

Emission shapes:

ValueShapeDescription
0PointEmit from a single point
1CircleEmit from random positions within a circle
2RectangleEmit from random positions within a rectangle
3ConeEmit in a directed cone from the emitter origin

Velocity

PropertyTypeDefaultDescription
speedMinnumber500Minimum initial speed
speedMaxnumber500Maximum initial speed
angleSpreadMinnumber0Minimum emission angle (degrees, 0–360)
angleSpreadMaxnumber360Maximum emission angle (degrees, 0–360)

Size Over Lifetime

PropertyTypeDefaultDescription
startSizeMinnumber100Minimum size at birth
startSizeMaxnumber100Maximum size at birth
endSizeMinnumber100Minimum size at death
endSizeMaxnumber100Maximum size at death
sizeEasingnumber0Interpolation curve (0=Linear, 1=EaseIn, 2=EaseOut, 3=EaseInOut)

Color Over Lifetime

PropertyTypeDefaultDescription
startColorColor{1,1,1,1}Color at birth (RGBA)
endColorColor{1,1,1,0}Color at death (RGBA, default fades out)
colorEasingnumber0Interpolation curve (0=Linear, 1=EaseIn, 2=EaseOut, 3=EaseInOut)

Rotation

PropertyTypeDefaultDescription
rotationMinnumber0Minimum initial rotation (degrees)
rotationMaxnumber0Maximum initial rotation (degrees)
angularVelocityMinnumber0Minimum rotation speed (degrees/sec)
angularVelocityMaxnumber0Maximum rotation speed (degrees/sec)

Forces

PropertyTypeDefaultDescription
gravityVec2{0, 0}Gravity force applied each frame
dampingnumber0Velocity damping factor (friction)

Texture & Sprite Sheet

PropertyTypeDefaultDescription
textureTextureHandleParticle texture
spriteColumnsnumber1Sprite sheet column count
spriteRowsnumber1Sprite sheet row count
spriteFPSnumber10Sprite sheet animation FPS
spriteLoopbooleantrueLoop sprite sheet animation

Rendering

PropertyTypeDefaultDescription
blendModenumber10 = Normal, 1 = Additive (default)
layernumber0Render order (-1000 to 1000)
materialnumber0Custom material ID (0 = default)
simulationSpacenumber00 = World, 1 = Local

Runtime API

Use the static Particle object to control emitters from code:

import { Particle } from 'esengine';
// Start emitting
Particle.play(entity);
// Stop emitting (existing particles continue their lifetime)
Particle.stop(entity);
// Reset emitter state and clear all alive particles
Particle.reset(entity);
// Query alive particle count
const count = Particle.getAliveCount(entity);

Controlling Emitter Properties

Query and modify ParticleEmitter in a system:

import { defineSystem, addSystem, Query, Mut, ParticleEmitter } from 'esengine';
addSystem(defineSystem(
[Query(Mut(ParticleEmitter))],
(query) => {
for (const [entity, emitter] of query) {
emitter.rate = 50;
emitter.startColor = { r: 1, g: 0.5, b: 0, a: 1 };
emitter.endColor = { r: 1, g: 0, b: 0, a: 0 };
emitter.gravity = { x: 0, y: -500 };
}
}
));

Custom Materials

Assign a custom material to particles for special rendering effects:

emitter.material = myMaterialHandle;

The particle renderer calls getMaterialDataWithUniforms on the assigned material, so any shader uniforms you define will be applied per-emitter.

UIMask Compatibility

Particles respect UIMask scissor clipping. To use particles inside a scrollable UI panel:

  1. Add a UIMask component (Scissor mode) to the panel entity
  2. Place the ParticleEmitter entity as a child of the masked container
  3. Particles will be clipped to the mask region

Editor Integration

  • Inspector — properties are organized into collapsible groups (Emission, Lifetime, Shape, Velocity, Size, Color, Rotation, Forces, Texture, Rendering) with constrained numeric inputs
  • Scene View overlay — when a ParticleEmitter entity is selected, the Scene View draws the emission shape as a gizmo:
    • Point: crosshair marker
    • Circle: dashed circle outline
    • Rectangle: dashed rectangle outline
    • Cone: pie-slice showing the emission angle spread
  • Hierarchy — right-click → Create → Particle to add a new particle entity

Enums

Use the named constants for readability:

import { EmitterShape, SimulationSpace, ParticleEasing } from 'esengine';
emitter.shape = EmitterShape.Circle;
emitter.simulationSpace = SimulationSpace.Local;
emitter.sizeEasing = ParticleEasing.EaseOut;
emitter.colorEasing = ParticleEasing.EaseInOut;
EnumValues
EmitterShapePoint (0), Circle (1), Rectangle (2), Cone (3)
SimulationSpaceWorld (0), Local (1)
ParticleEasingLinear (0), EaseIn (1), EaseOut (2), EaseInOut (3)

Example: Fire Effect

import {
defineSystem, addStartupSystem, Commands, Transform,
ParticleEmitter, EmitterShape, ParticleEasing
} from 'esengine';
addStartupSystem(defineSystem(
[Commands()],
(commands) => {
commands.spawn()
.insert(Transform, { position: { x: 400, y: 300, z: 0 } })
.insert(ParticleEmitter, {
rate: 30,
lifetimeMin: 0.5,
lifetimeMax: 1.5,
shape: EmitterShape.Circle,
shapeRadius: 20,
speedMin: 100,
speedMax: 200,
angleSpreadMin: 250,
angleSpreadMax: 290,
startSizeMin: 40,
startSizeMax: 60,
endSizeMin: 5,
endSizeMax: 10,
sizeEasing: ParticleEasing.EaseIn,
startColor: { r: 1, g: 0.8, b: 0.2, a: 1 },
endColor: { r: 1, g: 0.2, b: 0, a: 0 },
colorEasing: ParticleEasing.EaseOut,
gravity: { x: 0, y: -50 },
blendMode: 1,
});
}
));