Skip to content

物理与碰撞 ​

物理用 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 做命中判定,不要用刚体。

MIT 许可证