曲线路径
Curve2D 按走过的距离在曲线上取点,弯道上也是匀速。适合塔防怪物的行进路线、弯曲的弹道、巡逻路线。
ts
import { Curve2D, Scene, Sprite2D, tex, v } from 'sapling2d'
// 路线:平滑经过这几个点(也可以来自 Tiled 的折线对象:o.points 加上 o.x / o.y)
const ROUTE = Curve2D.catmullRom([v(100, -40), v(550, 250), v(200, 650), v(500, 1000), v(375, 1300)])
class Walker extends Sprite2D {
speed = 160 // 像素/秒:按弧长前进,弯道上也是匀速
dist = 0
private readonly _p = { x: 0, y: 0 } // 复用:每帧不分配
override process(dt: number) {
this.dist += this.speed * dt
ROUTE.sample(this.dist, this._p)
this.x = this._p.x
this.y = this._p.y
this.flipH = Math.cos(ROUTE.angleAt(this.dist)) < 0 // 往左走时翻转;要转向就写 rotation = angleAt(...)
if (this.dist >= ROUTE.length) this.queueFree() // 走到终点
}
}
export class Field extends Scene {
static override assets = { enemy: tex('enemy.png') }
walker!: Walker
override ready() {
this.walker = this.add(new Walker({ texture: Field.assets.enemy }))
}
}创建
Curve2D.catmullRom(points, { closed?, bakeInterval? }):平滑地经过每一个控制点。用的是 centripetal 参数化,控制点间距不均(比如随机生成的路线)时也不会打结或出尖角。Curve2D.polyline(points, { closed? }):直线段相连。- 控制点可以是
Vector2,也可以是任何带x/y的对象;相邻的重复点自动去掉。 closed: true时首尾相接,距离绕回起点,适合巡逻。
取点
| 成员 | 说明 |
|---|---|
length | 曲线长度(像素) |
sample(distance, out) | 把位置写进 out 并返回它;out 每帧复用,不分配内存 |
angleAt(distance) | 前进方向(弧度,0 朝右,和 rotation 一致) |
开放曲线的距离超出 [0, length] 时停在端点。
用法
- 曲线只是数据,不是节点。节点自己记
dist,每帧dist += speed * dt再sample。 - 减速、冰冻就是改
speed;"谁离终点最近"就是length - dist最小的那个;击退就是dist -= …。 - 创建时预先采样成折线(间距
bakeInterval,默认 5 像素),之后取点是二分查找。 - 曲线不可变。每只怪一条随机路线就各建一条(一条只有几十个点,很便宜),但不要每帧新建。
测试
ts
import { expect, it } from 'vitest'
import { createTestGame } from 'sapling2d/testing'
it('curve', async () => {
const g = await createTestGame({ main: Field })
const w = g.scene.walker
g.stepSeconds(2)
expect(w.dist).toBeCloseTo(320)
const p = ROUTE.sample(320, { x: 0, y: 0 })
expect([w.x, w.y]).toEqual([expect.closeTo(p.x), expect.closeTo(p.y)])
g.stepSeconds(ROUTE.length / 160)
expect(w.isFreed).toBe(true) // 走完全程
})