粒子
粒子用于火焰、烟雾、爆炸、火花、雨、魔法等特效。Estella 在 C++/WebAssembly 内核里用
CPU 模拟粒子,并按发射器GPU 实例化为单次 draw call 渲染,所以一个场景可以廉价地
跑上千个粒子。你用一个 ParticleEmitter 组件声明式地描述特效,再通过 Particle 资源
控制播放。
每个发射器拥有一个最多 maxParticles 的固定粒子池。它每帧按 rate(每秒个数)生成新
粒子(外加可选的爆发),每个粒子:
- 生成在发射器形状内的某处,初速度的大小和方向在配置的 min/max 之间随机取。
- 存活
lifetimeMin…lifetimeMax秒(随机),期间它的颜色和尺寸从起始值 动画到结束值。 - 运动受自身速度加上
gravity和damping影响,并可选地自转(angularVelocity)。 - 生命耗尽时消亡,回到池中。
一切都是逐粒子在 …Min 与 …Max 之间随机——两者设相等得到统一外观,拉开则得到
变化。随生命的动画默认用 startColor/endColor + startSize/endSize 配一条缓动曲线,
或用完整的渐变/曲线做精确控制(见下)。
粒子系统在主包里。编辑器和 Web 运行时会自动安装插件。在自定义 App 里手动加一次:
import { particlePlugin } from 'esengine';
app.addPlugin(particlePlugin);给实体加一个 ParticleEmitter 它就开始发射(playOnStart 默认开)。下面做一个循环的
火星喷泉,从黄色渐隐为透明红并在重力下抛出:
import { defineSystem, Commands, Transform, ParticleEmitter, EmitterShape,} from 'esengine';
const spawnEmbers = defineSystem([Commands()], (cmds) => { cmds.spawn() .insert(Transform, { position: { x: 0, y: -200, z: 0 } }) .insert(ParticleEmitter, { rate: 120, lifetimeMin: 1.2, lifetimeMax: 2.0, shape: EmitterShape.Cone, shapeAngle: 20, speedMin: 300, speedMax: 520, startSizeMin: 12, startSizeMax: 20, endSizeMin: 0, endSizeMax: 0, startColor: { r: 1, g: 0.9, b: 0.3, a: 1 }, endColor: { r: 1, g: 0.2, b: 0.1, a: 0 }, gravity: { x: 0, y: -400 }, });});ParticleEmitter 字段参考
Section titled “ParticleEmitter 字段参考”每个字段都是可选的——省略即取下方默认值。尺寸、距离、速度以世界单位计(默认 PPU 下即 像素);角度以度计。
发射 (Emission)
Section titled “发射 (Emission)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
rate |
number | 10 |
持续发射,每秒粒子数。0 = 只用爆发。 |
burstCount |
number | 0 |
每次爆发释放的粒子数。0 禁用爆发。 |
burstInterval |
number | 1 |
两次爆发之间的秒数。 |
duration |
number | 5 |
发射器停止前运行的秒数(见 looping)。 |
looping |
boolean | true |
到 duration 后重新开始,而不是停止。 |
playOnStart |
boolean | true |
实体一存在就开始发射。 |
maxParticles |
number | 1000 |
池大小——存活粒子的硬上限。 |
生命周期 (Lifetime)
Section titled “生命周期 (Lifetime)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
lifetimeMin |
number | 5 |
最短粒子寿命,秒。 |
lifetimeMax |
number | 5 |
最长粒子寿命,秒。 |
形状 (Shape)
Section titled “形状 (Shape)”粒子从哪生成及其初始方向。见 发射器形状。
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
shape |
EmitterShape |
Cone |
Point / Circle / Rectangle / Cone。 |
shapeRadius |
number | 100 |
Circle / Cone 的半径。 |
shapeSize |
Vec2 | {100, 100} |
Rectangle 的盒子范围。 |
shapeAngle |
number | 25 |
Cone 的锥半角(°)。 |
速度与力 (Velocity & forces)
Section titled “速度与力 (Velocity & forces)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
speedMin |
number | 500 |
最慢初速度。 |
speedMax |
number | 500 |
最快初速度。 |
angleSpreadMin |
number | 0 |
发射方向弧的起始(°)。 |
angleSpreadMax |
number | 360 |
发射方向弧的结束(°)。 |
gravity |
Vec2 | {0, 0} |
施加到每个粒子的恒定加速度。 |
damping |
number | 0 |
每秒速度阻尼(0 = 无)。 |
噪声与湍流 (Noise & turbulence)
Section titled “噪声与湍流 (Noise & turbulence)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
noiseStrength |
number | 0 |
湍流推动粒子的峰值流速(px/s)。0 = 关闭该模块。 |
noiseFrequency |
number | 0.01 |
场的空间尺度 —— 越大漩涡越紧、越繁密。 |
noiseScrollSpeed |
number | 0 |
场随时间漂移的速度(0 = 静止场)。 |
noiseOctaves |
number | 1 |
分形细节层数。1 = 平滑流动;越多越细碎的湍流。 |
见下文噪声与湍流。
随生命的尺寸 (Size over life)
Section titled “随生命的尺寸 (Size over life)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
startSizeMin |
number | 100 |
生成时的最小尺寸。 |
startSizeMax |
number | 100 |
生成时的最大尺寸。 |
endSizeMin |
number | 100 |
消亡时的最小尺寸。 |
endSizeMax |
number | 100 |
消亡时的最大尺寸。 |
sizeEasing |
ParticleEasing |
Linear |
从起始到结束尺寸的插值。 |
sizeCurve |
Curve |
无 | 完整的随生命尺寸曲线;覆盖 start/end(见下)。 |
随生命的颜色 (Color over life)
Section titled “随生命的颜色 (Color over life)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
startColor |
Color | {1,1,1,1} |
生成时的颜色(RGBA,0..1)。 |
endColor |
Color | {1,1,1,0} |
消亡时的颜色——默认把 alpha 渐隐到 0。 |
colorEasing |
ParticleEasing |
Linear |
从起始到结束颜色的插值。 |
colorGradient |
Gradient |
无 | 完整的随生命颜色渐变;覆盖 start/end。 |
旋转 (Rotation)
Section titled “旋转 (Rotation)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
rotationMin |
number | 0 |
生成旋转的最小值(°)。 |
rotationMax |
number | 0 |
生成旋转的最大值(°)。 |
angularVelocityMin |
number | 0 |
最慢自转(°/s)。 |
angularVelocityMax |
number | 0 |
最快自转(°/s)。 |
贴图与精灵表 (Texture & sprite sheet)
Section titled “贴图与精灵表 (Texture & sprite sheet)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
texture |
asset | 无 | 粒子贴图。无贴图的粒子是柔和的方块。 |
spriteColumns |
number | 1 |
精灵表列数(1 = 整张贴图)。 |
spriteRows |
number | 1 |
精灵表行数。 |
spriteFPS |
number | 10 |
播放精灵表时的每秒帧数。 |
spriteLoop |
boolean | true |
循环精灵表 vs 停在最后一帧。 |
渲染与状态 (Rendering & state)
Section titled “渲染与状态 (Rendering & state)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
blendMode |
BlendMode |
Additive |
粒子如何合成。见 混合模式。 |
layer |
number | 0 |
决定绘制顺序的排序层。 |
material |
asset | 无 | 可选的自定义材质/着色器。 |
simulationSpace |
SimulationSpace |
World |
World 或 Local(见下)。 |
enabled |
boolean | true |
关掉即冻结并隐藏,而不移除组件。 |
子发射器 (Sub-emitter)
Section titled “子发射器 (Sub-emitter)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
subEmitter |
entity | 无 | 粒子触发时发射的子发射器实体。0/未设 = 关闭。 |
subEmitterTrigger |
SubEmitterTrigger |
Death |
在每个粒子的 Death(死亡)或 Birth(出生)时触发。 |
subEmitterChance |
number | 1 |
触发粒子发射子爆发的概率(0..1)。 |
subEmitterInheritVelocity |
number | 0 |
传给子爆发的父粒子速度比例。 |
见下文子发射器。
拖尾 (Trail)
Section titled “拖尾 (Trail)”| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
trailEnabled |
boolean | false |
每个粒子拖一条渐细的 ribbon。关 = 不记录历史。 |
trailWidth |
number | 8 |
ribbon 头部宽度;向尾部收细到一点。 |
trailPoints |
number | 6 |
历史点数(2..12)—— 越长越平滑但越贵。 |
trailMinDistance |
number | 6 |
记录新拖尾点前粒子须移动的最小世界距离。 |
见下文逐粒子拖尾。

视口里的发射器形状 gizmo——锥形、圆形、矩形,各自画在粒子生成的位置(开 Preview FX 可在编辑态看它们运行)。
EmitterShape 设定生成区域与初始方向:
| 形状 | 使用 | 行为 |
|---|---|---|
Point |
— | 所有粒子在实体原点生成。 |
Circle |
shapeRadius |
在圆盘内生成;方向向外辐射。 |
Rectangle |
shapeSize |
在矩形盒内生成。 |
Cone |
shapeRadius、shapeAngle |
在锥内生成——一束有方向的喷射(喷泉、推进器)。 |
angleSpreadMin…Max 弧进一步约束发射方向,所以任何形状都能定向(例如一个只向上发射的
Circle,用 0…180)。
随生命的颜色与尺寸
Section titled “随生命的颜色与尺寸”多数特效用 startColor → endColor 和 startSize → endSize 加一条缓动曲线就够了
——它们是普通字段,代码里和编辑器里都能用。ParticleEasing 取值为 Linear、EaseIn、
EaseOut、EaseInOut。
要精确控制,在 ParticleEmitter 检视面板里编辑一条渐变(颜色)或曲线(尺寸倍率)
——一串在归一化生命 t(0..1)上的停靠点/关键点。引擎会把它们烘焙成一张查找表(LUT),
模拟按粒子年龄采样。编辑出的数据形如:
// colorGradient: hot white -> orange -> smoke, fading out.{ stops: [ { t: 0.0, color: { r: 1, g: 1, b: 0.8, a: 1 } }, { t: 0.4, color: { r: 1, g: 0.5, b: 0.1, a: 1 } }, { t: 1.0, color: { r: 0.2, g: 0.2, b: 0.2, a: 0 } }, ],}// sizeCurve: swell in, then shrink away (multiplies the particle's start size).{ keys: [{ t: 0, v: 0.2 }, { t: 0.3, v: 1 }, { t: 1, v: 0 }] }要在运行时程序化改变这条曲线,通过资源上传一张预烘焙的查找表——颜色是 N×4 的 RGBA
Float32Array,尺寸是 N 个标量。传 null 清空并回退到 start/end + 缓动:
import { defineSystem, Res, Particle } from 'esengine';
const recolor = defineSystem([Res(Particle)], (particles) => { const lut = new Float32Array([ /* r,g,b,a, r,g,b,a, … */ ]); particles.setColorLut(entity, lut); // or particles.setColorLut(entity, null)});把 texture 指向一张精灵表,并设 spriteColumns / spriteRows,让每个粒子按 spriteFPS
在各帧间播放。一张 4×4 的爆炸表,在每个粒子的生命里只播一次:
.insert(ParticleEmitter, { texture: 'effects/explosion.png', spriteColumns: 4, spriteRows: 4, spriteFPS: 24, spriteLoop: false,});直线运动的粒子显得机械。噪声模块在速度和重力之上叠加一层 curl 噪声流场, 让粒子沿着平滑、旋绕的气流游走 —— 卷曲的烟、飘动的火星、漂移的魔法。
这个流场是无散度的(它是噪声势场的旋度),意味着粒子不会像原始随机抖动那样
挤成一团或散出空洞 —— 运动始终均匀而自然。它是纯 CPU 数学,在每个平台
(含微信)上逐字节一致,并且当 noiseStrength 为 0 时完全零开销。
.insert(ParticleEmitter, { // …发射、生命周期、颜色… noiseStrength: 120, // 气流推力有多强(px/s) noiseFrequency: 0.01, // 越小越是宽缓的漩涡;越大越是细密的涡流 noiseScrollSpeed: 0.5, // 让场漂移,使流动持续演化 noiseOctaves: 2, // 1 = 平滑;越多越细碎的湍流});调参建议:
noiseStrength是主旋钮 —— 从粒子speed的一小部分起调,逐步加大到游走感明显。noiseFrequency决定漩涡大小。上升的烟喜欢小值(~0.005);火花四溅的迸发喜欢大值(~0.03)。noiseScrollSpeed> 0 能让看似静止的烟柱保持缓慢的活气;设0则是粒子穿流而过的固定场。noiseOctaves以开销换细节 ——1–2已覆盖多数特效。
子发射器让一个发射器在粒子所在位置生成另一个发射器的爆发 —— 经典的升空后爆炸的 烟花弹、拖着烟雾的火箭、裂成火星的火花。无需逐帧代码:你连好一个引用,模拟就会在 选定事件上触发爆发。
子发射器是一个当作模板用的普通 ParticleEmitter 实体 —— 设 rate: 0 和
playOnStart: false,它就不会自己发射,只在父级触发时爆发。它的 burstCount
决定每次子爆发生成多少粒子。把父级的 subEmitter 指向那个子实体:
import { SubEmitterTrigger } from 'esengine';
// 爆发模板 —— 一个只在被触发时才发射的普通发射器。const shell = cmds.spawn() .insert(Transform, {}) .insert(ParticleEmitter, { rate: 0, playOnStart: false, burstCount: 44, // 每次爆发 44 颗火星 lifetimeMin: 0.6, lifetimeMax: 1.1, speedMin: 160, speedMax: 340, startColor: { r: 1, g: 0.85, b: 0.35, a: 1 }, endColor: { r: 1, g: 0.3, b: 0.1, a: 0 }, gravity: { x: 0, y: -150 }, }).id();
// 火箭 —— 每个粒子死亡时,在该处触发爆发。cmds.spawn() .insert(Transform, { position: { x: 0, y: -230, z: 0 } }) .insert(ParticleEmitter, { rate: 2.5, lifetimeMin: 1.4, lifetimeMax: 1.8, speedMin: 560, speedMax: 660, angleSpreadMin: 84, angleSpreadMax: 96, subEmitter: shell, subEmitterTrigger: SubEmitterTrigger.Death, // 或 Birth subEmitterInheritVelocity: 0.15, // 让火星带上一点火箭的运动 });subEmitterTrigger——Death在每个父粒子消亡时触发(炮弹爆炸、碎片);Birth在每个粒子出生时触发(沿着一道粒子流持续洒下子爆发)。subEmitterChance—— 设为小于1,只让部分粒子触发子级,稀释过密的效果。subEmitterInheritVelocity—— 把父粒子速度的一部分带进爆发,让火星随着火箭飞,而不只是四散。
在编辑器里,subEmitter 是一个实体选择器 —— 拖入场景中任意发射器实体即可;引用会在
场景加载时自动重映射。
打开 trailEnabled,每个粒子都会沿最近的运动轨迹拖出一条渐细的 ribbon —— 彗星火花、
曳光弹、旋绕的魔法尾迹。这和独立的 TrailRenderer 不同:后者
给单个实体拖尾,而这里(可达数千个)每个粒子都有自己的尾迹。
.insert(ParticleEmitter, { rate: 40, speedMin: 220, speedMax: 420, lifetimeMin: 0.7, lifetimeMax: 1.2, startColor: { r: 0.6, g: 0.9, b: 1, a: 1 }, endColor: { r: 0.2, g: 0.4, b: 1, a: 0 }, trailEnabled: true, trailWidth: 10, // 粒子处的 ribbon 宽度,向后收细到一点 trailPoints: 8, // 保留多少轨迹点(2..12) trailMinDistance: 5, // 每移动 5 个世界单位记录一个点});ribbon 头部取粒子当前颜色,沿长度渐隐到透明,所以拖尾自动继承你的随生命颜色。它在
CPU 上生成,走和 TrailRenderer 相同的三角带 batch 路径 —— 无特殊 GPU 路径 —— 因此
和其他 2D 可渲染物一样排序混合。
拖尾是可选项,因为它比普通粒子贵:每粒子一小段位置历史(受 trailPoints 限制)+ 每帧
ribbon 几何。trailPoints 保持适中(6–8 足够),重拖尾特效请调低 maxParticles。
噪声模块是烘进单个发射器的自带湍流,而力场是你放进场景的外部影响:
它是自己的实体、带 ParticleForceField 组件,作用于范围内每个发射器的世界空间粒子。
阵风、黑洞、漩涡、静止的空气团 —— 放一个进去,整个场景的粒子都会响应。
import { ParticleForceField, ForceFieldType } from 'esengine';
cmds.spawn() .insert(Transform, { position: { x: 0, y: 0, z: 0 } }) .insert(ParticleForceField, { type: ForceFieldType.Vortex, strength: 450, // 加速度(px/s²);Point 场取负值即从吸引变排斥 radius: 320, // 0 = 影响整个场景;> 0 = 只影响范围内的粒子 falloff: true, // 在半径边缘把力衰减到 0 });| 类型 | 效果 |
|---|---|
Directional |
沿 direction 的恒定推力 —— 风、上升气流、水流。 |
Point |
朝力场吸引(strength > 0)或排斥(strength < 0)—— 引力井、爆炸。 |
Vortex |
绕力场切向旋绕 —— 漩涡、龙卷、星系。 |
Drag |
在区域内阻尼速度 —— 减速粒子的静止空气/水团。 |
力场是纯 CPU:模拟每帧收集活跃力场,和重力一样折进同一套速度积分,无新子系统。Directional
用它的 direction;Point/Vortex/Drag 从力场→粒子向量推导方向,忽略它。
两个注意:力场只作用于世界空间粒子(Local 发射器的粒子随发射器移动、忽略力场);
strength: 0 或 enabled: false 直接跳过该力场。
打开 collisionEnabled,粒子就会在世界高度 collisionFloor 处的水平地面平面上弹跳
—— 雨点砸地、火花在表面上溅跳、雪花落定。纯 CPU 碰撞,不依赖物理。
.insert(ParticleEmitter, { rate: 60, speedMin: 200, speedMax: 400, shape: EmitterShape.Cone, shapeAngle: 50, gravity: { x: 0, y: -500 }, collisionEnabled: true, collisionFloor: -200, // 粒子落到的世界 Y collisionBounce: 0.5, // 0 = 立即停住,1 = 完全弹性 collisionFriction: 0.2, // 每次弹跳损失的水平速度 collisionLifetimeLoss: 0.1, // 每次弹跳烧掉的生命比例(落定时渐隐)});当世界空间粒子落到 collisionFloor 以下,它被拉回平面、竖直速度按 collisionBounce
反射缩放;collisionFriction 削掉水平速度,collisionLifetimeLoss 加速其衰老,让溅
射渐隐而不是永远蹦跳。
| 取值 | 粒子跟随发射器吗? | 用于 |
|---|---|---|
World(默认) |
否——它们留在出生的地方 | 拖尾、尾焰、移动物体身后留下的火花 |
Local |
是——它们随实体的 transform 移动 | 光环、护盾、刚性附着在移动体上的特效 |
BlendMode 控制粒子如何叠加到场景上:
| 模式 | 效果 |
|---|---|
Normal |
标准 alpha 混合。 |
Additive(默认) |
颜色相加——辉光、火焰、魔法、光。 |
Multiply |
变暗——烟雾、阴影。 |
Screen |
提亮,比 additive 更柔和。 |
PremultipliedAlpha |
用于预乘 alpha 的贴图。 |
控制播放 —— Res(Particle)
Section titled “控制播放 —— Res(Particle)”在系统里读 Particle 资源(或在 ECS 之外用 app.getResource(Particle))来命令式地
驱动发射器:
import { defineSystem, Res, Particle } from 'esengine';
const control = defineSystem([Res(Particle)], (particles) => { particles.play(entity); // start / resume emitting particles.stop(entity); // stop emitting (live particles finish) particles.reset(entity); // clear all live particles immediately const n = particles.getAliveCount(entity); // live particle count});| 方法 | 说明 |
|---|---|
play(entity) |
开始或恢复发射。 |
stop(entity) |
停止发射;已有粒子活完自己的寿命。 |
reset(entity) |
杀掉所有存活粒子并重置发射器。 |
getAliveCount(entity) |
当前存活的粒子数。 |
setColorLut(entity, lut | null) |
上传烘焙好的颜色 LUT(见上),或清空。 |
setSizeLut(entity, lut | null) |
上传烘焙好的尺寸 LUT,或清空。 |
性能与最佳实践
Section titled “性能与最佳实践”- 控制
maxParticles。 它是固定池;按最坏情况定大小,别更高。多个小发射器比一个巨大的 更利于剔除。 - 辉光优先用
Additive(火焰、火花、魔法)——它掩盖 overdraw 且无需排序。不透明的 烟尘留给Normal/Multiply。 - 把粒子贴图打成图集并共享一个
material,让发射器合批。 - 拉开 min/max 得到自然的变化;min/max 相等看起来很机械。
- 复用发射器:用
reset+play而不是生成/销毁。 - 游戏代码里粒子在播放模式推进;编辑器里 Preview FX 开关让它们在创作时保持鲜活。