粒子
Particles2D 是粒子发射器,用来做火花、爆炸、拖尾。粒子本身不是节点,数据存在发射器里,一个发射器只有一次绘制调用。
ts
import { Particles2D, Scene, tex, v, type Vector2 } from 'sapling2d'
export class Arena extends Scene {
static override assets = { spark: tex('spark.png'), smoke: tex('smoke.png') }
override ready() {
// 持续发射:跟着角色的烟(localCoords 默认 false:烟留在原地,角色跑开后形成拖尾)
this.add(
new Particles2D({ name: 'Smoke', texture: Arena.assets.smoke, position: v(375, 900), amount: 40, lifetime: 0.8,
direction: -Math.PI / 2, spread: 0.3, speedMin: 40, speedMax: 80, scaleStart: 0.5, scaleEnd: 1.5, alphaEnd: 0 }),
)
}
/** 刀碰刀的火花:一次性爆发,全部消失后删掉发射器。 */
sparks(at: Vector2) {
const fx = this.add(
new Particles2D({ name: 'Sparks', texture: Arena.assets.spark, position: at, emitting: false, amount: 14, lifetime: 0.35,
lifetimeRandomness: 0.4, speedMin: 250, speedMax: 500, damping: 5, scaleStart: 1, scaleEnd: 0.2, alphaEnd: 0, selfModulate: 0xffd060 }),
)
fx.emit()
fx.finished.connect(() => fx.queueFree())
}
}ts
g.scene.sparks(v(375, 600))
g.step()
expect(g.dump()).toMatch(/Sparks \(Particles2D\) position=\(375, 600\).* particles=14/)
g.stepSeconds(0.5)
expect(g.dump()).not.toContain('Sparks') // 火花散完,发射器删掉了
expect(g.dump()).toMatch(/Smoke \(Particles2D\).*emitting=true particles=\d+/)发射
emitting(默认 true):持续发射,每秒rate个(默认amount / lifetime)。emit(n):马上爆发 n 个(默认amount)。oneShot: true:emitting一变成 true 就爆发amount个,然后自动关掉。amount是同时存活的上限,到了上限不再发射。- 全部消失、且不再发射时发出
finished信号,一般在这里queueFree()。
运动和外观
| 属性 | 作用 |
|---|---|
direction ± spread | 发射方向(弧度,跟着节点的全局旋转)和散开角度(默认 π,即所有方向) |
speedMin / speedMax | 初速度范围 |
gravity | 重力 |
damping | 每秒损失速度的比例 |
lifetime / lifetimeRandomness | 寿命和寿命的随机程度 |
scaleStart → scaleEnd | 缩放随寿命线性变化 |
alphaStart → alphaEnd | 不透明度随寿命线性变化 |
粒子不旋转。颜色用 selfModulate(只染粒子)或 modulate;发光的特效配合 blendMode: 'add'。
坐标
默认粒子发射后留在原地,不随节点移动,适合火花、拖尾。localCoords: true 时粒子跟着节点走。
其他
- 每帧不分配内存。
- 随机数用
tree.fxRng:测试可复现,但和游戏逻辑的tree.rng分开,加减粒子特效不会改变同一个 seed 的对局。 - 时间受
timeScale和暂停影响。 aliveCount是存活的粒子数,dump()里显示为particles=。