物理与碰撞
物理用 planck.js(Box2D 的 TypeScript 移植)。单位是像素和 px/s,重力默认 (0, 980)。
| 节点 | 用途 |
|---|---|
RigidBody2D | 动态刚体:受重力、会碰撞 |
StaticBody2D | 静态刚体:地面、墙 |
Area2D | 检测区域:只报告进出,不挡住刚体 |
CollisionShape2D | 碰撞形状:circle(r)、rectangle(w, h)、polygon(points),必须是刚体或区域的直接子节点 |
例子:合成玩法
两个同级的球相撞,合成一个更高级的球:
ts
import { circle, CollisionShape2D, rectangle, RigidBody2D, Scene, StaticBody2D, v, type Vector2 } from 'sapling2d'
class Ball extends RigidBody2D {
constructor(
readonly level: number,
position: Vector2,
) {
super({ position, bounce: 0.2 }) // 单位:像素、px/s;重力默认 (0, 980)
this.add(new CollisionShape2D({ shape: circle(20 + level * 10) }))
}
override ready() {
// 接触信号在物理步结束后触发:回调里可以安全地 queueFree / add
this.bodyEntered.connect((other) => {
if (!(other instanceof Ball) || other.level !== this.level) return
if (this.isQueuedForDeletion || other.isQueuedForDeletion) return // 两边都会收到信号:只处理一次
this.queueFree()
other.queueFree()
this.parent!.add(new Ball(this.level + 1, this.position.lerp(other.position, 0.5)))
}, this)
}
}
export class Box extends Scene {
override ready() {
const floor = this.add(new StaticBody2D({ position: v(375, 1250) }))
floor.add(new CollisionShape2D({ shape: rectangle(750, 100) })) // 上表面 y = 1200
this.add(new Ball(0, v(375, 1100)))
this.add(new Ball(0, v(380, 900)))
}
}- 接触信号
bodyEntered/bodyExited在物理步结束后触发,回调里queueFree和add都是安全的。 - 两边都会收到信号:用
isQueuedForDeletion保证只处理一次。
移动刚体
刚体的位置由物理决定。给 position 赋值等于瞬移,速度清零;要推它就施加冲量或力:
ts
// 给 position 赋值等于瞬移(速度清零);施加冲量:ball.applyCentralImpulse(v(200, 0))
balls[0]!.position = v(100, 500)
g.step()
expect(balls[0]!.x).toBeCloseTo(100)常用属性:mass、friction、bounce、gravityScale、linearVelocity、angularVelocity、lockRotation、ccd(快速移动的物体防穿透)。
检测区域
ts
import { Area2D, circle, CollisionShape2D, rectangle, RigidBody2D, Scene, v } from 'sapling2d'
export class Goal extends Scene {
scored = 0
override ready() {
// 区域:只检测 RigidBody2D 进出,不挡住它们
const zone = this.add(new Area2D({ position: v(375, 800) }))
zone.add(new CollisionShape2D({ shape: rectangle(300, 40) }))
zone.bodyEntered.connect(() => this.scored++, this)
const ball = this.add(new RigidBody2D({ position: v(375, 500) }))
ball.add(new CollisionShape2D({ shape: circle(20) }))
}
}碰撞层
collisionLayer / collisionMask 共 32 层,规则同 Godot:任一方的 mask 包含对方的 layer,两者就碰撞。用 setCollisionLayerValue(n, bool) 单独开关某一层。
注意
- 改碰撞形状(移动、修改、禁用、增删
CollisionShape2D)会重建形状,正在接触的对象会先收到一次bodyExited、再收到一次bodyEntered。 scale不影响碰撞形状。- 微信小游戏的 iOS 端没有 JIT,同时活跃的刚体建议控制在 60–80 个以内。子弹这类大量对象用
HitTester做命中判定,不要用刚体。