透明度与颜色
Node2D 有三个颜色属性,都可以写在构造参数里,也都可以补间:
| 属性 | 默认 | 作用范围 | 说明 |
|---|---|---|---|
alpha | 1 | 自己和子节点 | 不透明度 0–1,父子相乘。为 0 时仍能被点中,要隐藏用 visible |
modulate | 0xffffff | 自己和子节点 | 颜色乘子 0xRRGGBB,每个像素乘上它:0xff6666 偏红、0x888888 变暗。只能变暗或偏色,不能变亮 |
selfModulate | 0xffffff | 只有自己的贴图 / 文字 | 同上,但不影响子节点(Boss 变红而血条不变);和 modulate 叠乘 |
ts
// 受伤变红再恢复:颜色属性按 RGB 通道补间
enemy.modulate = 0xff4040
enemy.createTween().to(enemy, { modulate: 0xffffff }, 0.2)
// 淡出后销毁
fx.createTween().to(fx, { alpha: 0 }, 0.3).call(() => fx.queueFree())受击闪白:Sprite2D.flash
modulate 是乘法,没法把角色变成白色。受击闪白用 flash:
ts
import { Ease, Scene, Sprite2D, tex, v, type Tween } from 'sapling2d'
class Enemy extends Sprite2D {
hp = 3
private _flashTween: Tween | null = null
hit() {
this.hp--
// 闪白:整个形状变成白色,再淡回原样(modulate 只能变暗,做不出这个效果)
this._flashTween?.kill()
this.flash = 1
this._flashTween = this.createTween().to(this as Enemy, { flash: 0 }, 0.12, Ease.QuadOut) // 子类的属性:to(this as 类名, …)
}
/** 中毒:闪绿色。 */
poison() {
this.flashColor = 0x60ff60
this.flash = 0.6
}
}
export class Arena extends Scene {
static override assets = { enemy: tex('enemy.png') }
enemy!: Enemy
override ready() {
this.enemy = this.add(new Enemy({ texture: Arena.assets.enemy, position: v(375, 400) }))
}
}ts
import { expect, it } from 'vitest'
import { createTestGame } from 'sapling2d/testing'
it('flash', async () => {
const g = await createTestGame({ main: Arena })
const enemy = g.scene.enemy
enemy.hit()
expect(enemy.flash).toBe(1)
g.step(3)
expect(enemy.flash).toBeGreaterThan(0)
expect(enemy.flash).toBeLessThan(1)
g.stepSeconds(0.2)
expect(enemy.flash).toBe(0) // 淡回原样
enemy.poison()
expect(g.dump()).toContain('flash=0.6 flashColor=#60ff60')
})flash(0–1,默认 0)在贴图的形状上叠一层flashColor(默认白色)。为 1 时整个形状变成纯色,透明的地方仍然透明。- 受击时设成 1,再补间回 0;反复受击时先
kill()上一个补间。flashColor也能补间。 modulate、alpha、blendMode照常作用在闪光上:被modulate染蓝的节点闪出来是淡蓝。- 帧动画、图集的帧、翻转、
offset都照常对齐。只作用于这个精灵自己,不影响子节点。 - 代价:一张图第一次闪白时,显存里会生成一张同样大小的白色剪影,之后这张图的所有帧、所有精灵共用。大图集闪一次就多占一份显存,所以给要闪白的角色单独打一张图集。
叠加发光:blendMode
火光、爆炸、刀光、魔法这类发光的特效,用 blendMode: 'add':颜色相加,叠在别的东西上会把它照亮。普通混合只会盖上一层颜色,看起来发灰。
ts
import { Ease, Node2D, Particles2D, Scene, Sprite2D, tex, v, type Vector2 } from 'sapling2d'
export class Arena extends Scene {
// glow.png:白色、中心亮边缘透明的圆,用 selfModulate 染色
static override assets = { glow: tex('glow.png'), spark: tex('spark.png') }
/** 所有发光特效的父节点:设一次 'add',下面的光圈和火花都叠加发光;挨在一起绘制也更容易合批。 */
fx!: Node2D
override ready() {
this.fx = this.add(new Node2D({ name: 'Fx', blendMode: 'add', zIndex: 100 }))
}
/** 爆炸:一个放大淡出的光圈 + 一把火花。叠在怪物上会把它照亮,而不是盖上一层不透明的橙色。 */
explode(at: Vector2) {
const glow = this.fx.add(new Sprite2D({ texture: Arena.assets.glow, position: at, scale: v(0.5, 0.5), selfModulate: 0xff9030 }))
glow.createTween().to(glow, { scale: v(2.5, 2.5), alpha: 0 }, 0.3, Ease.QuadOut).call(() => glow.queueFree())
const sparks = this.fx.add(
new Particles2D({ texture: Arena.assets.spark, position: at, emitting: false, amount: 12, lifetime: 0.3, speedMin: 150, speedMax: 350, alphaEnd: 0, selfModulate: 0xffd060 }),
)
sparks.emit()
sparks.finished.connect(() => sparks.queueFree())
}
}ts
import { expect, it } from 'vitest'
import { createTestGame } from 'sapling2d/testing'
it('blend mode', async () => {
const g = await createTestGame({ main: Arena })
g.scene.explode(v(375, 600))
g.step()
expect(g.dump()).toContain('Fx (Node2D) position=(0, 0) zIndex=100 blendMode=add')
const glow = g.scene.fx.children[0] as Sprite2D
expect(glow.blendMode).toBe('inherit') // 子节点跟随父节点:实际按 'add' 绘制
g.stepSeconds(0.5)
expect(g.scene.fx.children).toHaveLength(0) // 光圈淡出、火花散完后都删掉了
})- 取值:
'inherit'(默认,跟随父节点)、'normal'、'add'。作用于自己和整棵子树,子节点可以设回'normal'。 - 不传进
CanvasLayer:界面层自成一层,要发光就在层里面的节点上设。 - 贴图、
ColorRect、文字、粒子、图块地图都按它绘制。alpha照常起作用(淡出时光也变弱),selfModulate给光染色。 - 发光贴图做成白色、黑底或透明底、中心亮边缘暗,颜色交给
selfModulate。 - 背景越暗效果越好;同一处叠太多层会过曝成一片白。
- 混合模式不同的相邻节点不能合批:同屏很多发光特效时,把它们放在同一个父节点下,让它们在绘制顺序上挨在一起。