A deterministic 2D rigid body physics engine in pure MoonBit: SAT collision detection, sequential impulse solver with warm starting, Coulomb friction and restitution, revolute/distance/prismatic/angle-limit/motor joints, sleeping, ray casts and region queries, snapshot JSON with a state digest, plus SVG rendering and a WebAssembly playground.
let world = @moonphys.World::new()
// 静态地面
ignore(
world.add_body(
@moonphys.Body::fixed(@moonphys.Shape::box(10.0, 0.1)).at(0.0, -0.1),
),
)
// 一个自由下落的方块
ignore(world.add_body(@moonphys.Body::box(0.5, 0.5).at(0.0, 4.0)))
// 固定步长推进 2 秒
for _ in 0..<120 {
world.step(1.0 / 60.0)
}
println(world.body(1).position().y()) // 0.5:稳稳落在有厚度的地面上| 模块 | 内容 |
|---|---|
| 数学基座 | Vec2、Transform、向量运算符重载、Show 实现 |
| 形状 | 圆、任意凸多边形(逆时针),自动计算质心与转动惯量(平行轴定理) |
| 宽相 | 增量 sort-and-sweep:复用上一帧的排序数组,只对近序数组做插入排序(每步近乎 O(n));pair 缓冲区与持久接触桶都跨帧复用,tie 按 body 下标打破 |
| 窄相 | SAT + reference/incident edge 裁剪(圆/圆、多边形/圆、多边形/多边形),返回多点接触 |
| 求解器 | 顺序冲量法 + 持久接触(Arbiter 按 feature id 迁移冲量做暖启动)+ Baumgarte 位置偏置 |
| 摩擦 | 库仑锥,切向冲量钳制在 ±μ·Pn |
| 恢复系数 | 只在法向相对速度超过阈值时生效,避免静止抖动 |
| 关节 | DistanceJoint(定长/软弹簧)、RevolveJoint(铰链)、PrismaticJoint(滑轨+行程限位+电动)、AngleLimitJoint(角限位)、MotorJoint(角马达),统一用有效质量矩阵求解 |
| 限位 | 硬限位按纯速度约束求解(不反弹),残余穿出由积分后的位置投影消除 |
| 睡眠 | 线/角速度低于阈值持续 0.5s 后休眠,接触对的动态体互相唤醒 |
| 确定性 | 无随机、无哈希迭代、固定步长 + 稳定排序 → 两次运行逐位一致(有测试) |
| 渲染 | render 包把世界快照输出为 SVG,带网格与配色,可逐帧拼成动画 |
| 交互演示 | playground/ 编译成 wasm-gc,导出 24 个纯数值 pg_* 函数,浏览器里拖拽刚体、点击施力 |
moon add weopqrst/moonphysimport {
"weopqrst/moonphys" @moonphys,
}// main.mbt
fn main {
let world = @moonphys.World::builder()
.ground(20.0) // 静态地面,上表面 y = 0
.ball(0.4, 0.0, 6.0) // 半径 0.4,位于 (0, 6)
.box(0.5, 0.5, 1.5, 8.0) // 半宽 0.5、半高 0.5
.body(@moonphys.Body::ball(0.3).at(-2.0, 9.0).with_restitution(0.8))
.build()
for _ in 0..<240 {
world.step(1.0 / 60.0)
}
println(world.snapshot_json()) // 直接喂给前端/回放
}moon run examples/bounce # 20 行:弹球落地,逐步打印位置与速度
moon run examples/pyramid # 金字塔随动能衰减整体入睡
moon run examples/ragdoll # 四段布娃娃,铰链连成的链条let world = @moonphys.World::new()
let a = world.add_body(@moonphys.Body::box(0.2, 0.05).at(0.0, 5.0))
let b = world.add_body(@moonphys.Body::box(0.2, 0.05).at(0.4, 5.0))
world.add_revolve_joint(a, b, @moonphys.Vec2::new(0.2, 5.0))moon run cmd/main --target js > docs/index.htmlpython -m http.server 8123 # 仓库根目录
# 浏览器打开 http://127.0.0.1:8123/docs/playground/| 输入 | 效果 |
|---|---|
| 按住刚体拖动 | 用临界阻尼弹簧把刚体拉向光标(pg_grab_*),拖动中照样参与碰撞 |
| 单击空白处 | 半径 3 m 的衰减冲量(pg_blast) |
| 滚轮 | 缩放视图 |
| 空格 / R / 数字键 | 暂停继续 / 重置当前场景 / 切换到第 n 个场景 |
| 底部滑块 | 重力大小、速度迭代次数 |
powershell -File tools/build-playground.ps1 # 或 bash tools/build-playground.sh
node tools/check-playground.mjs # 校验 24 个导出 + 6 场景各 180 步// 世界
let world = @moonphys.World::new() // 重力默认 (0, -9.81)
world.set_gravity(@moonphys.Vec2::new(0.0, -3.0)) // 低重力
world.set_velocity_iterations(10) // 迭代次数,越大越硬
world.set_sleep_enabled(true)
world.step(1.0 / 60.0) // 固定步长
world.run(120, 1.0 / 60.0) // 等价于循环 step
world.time() // 累计仿真时间
world.arbiter_count() // 当前接触对
world.snapshot() // 渲染/测试用的不可变快照
world.snapshot_json() // 同上,序列化成 JSON
world.state_hash() // 状态指纹,用于确定性回归
// 链式建场景(等价于逐个 add_body)
@moonphys.World::builder()
.gravity(0.0, -9.81).iterations(10).sleep_enabled(true)
.ground(20.0).ball(0.4, 0.0, 4.0).box(0.5, 0.5, 1.0, 6.0)
.body(some_preconfigured_body)
.build()
// 刚体
let body = @moonphys.Body::box(0.5, 0.5) // 动态方块
.at(0.0, 3.0) // 质心位置
.with_rotation(0.3) // 弧度
.with_velocity(1.0, 0.0)
.with_angular_velocity(-0.5)
.with_friction(0.6)
.with_restitution(0.2) // 弹性
.with_density(1.0) // 质量 = 密度 × 面积
.with_damping(0.05, 0.05) // 线/角阻尼
.with_allow_sleep(true)
.with_user_data(3) // 渲染配色用的自定义标记
ignore(world.add_body(body))
// 形状
@moonphys.Shape::box(0.5, 0.5) // 半宽半高
@moonphys.Shape::circle(0.4)
@moonphys.Shape::convex(vertices) // 逆时针凸多边形
// 力与冲量
body.apply_force(@moonphys.Vec2::new(0.0, 10.0)) // 本步有效
body.apply_impulse(@moonphys.Vec2::new(2.0, 0.0), body.position())
body.point_velocity(body.position())
// 查询
body.mass(); body.inertia(); body.aabb(); body.contains_point(p)
body.local_to_world(p); body.world_to_local(p)
// 关节
world.add_distance_joint(index_a, index_b) // 保持当前中心距
world.add_revolve_joint(index_a, index_b, @moonphys.Vec2::new(0.0, 5.0)) // 世界锚点
world.add_revolve_joint_local(a, b, local_a, local_b) // 各自的局部锚点
world.add_prismatic_joint(a, b, anchor, axis) // 滑轨,可 .with_limits/.with_motor
world.add_angle_limit_joint_at(a, b, anchor, -0.6, 0.6) // 绕锚点的角限位
world.add_motor_joint(a, b, 2.0, 200.0) // 目标角速度, 最大力矩
world.add_soft_distance_joint(a, b, 4.0, 1.0) // 频率 Hz, 阻尼比moon run bench --target js # 或 --target wasm-gc| 场景 | bodies | 接触数 | js ms/step | wasm-gc ms/step |
|---|---|---|---|---|
| stack:10×10 方块塔 | 101 | 143 | 0.40 | 0.49 |
| crowd:500 个相切圆密集堆 | 501 | 883 | 1.13 | 1.21 |
| chain:20 节刚性链 | 22 | 17 | 0.07 | 0.09 |
moon test # 64 项
moon test --target js # 同一套断言在 JS 后端上再跑一遍
node tools/check-playground.mjs # playground 的 wasm:24 个导出 + 6 场景各 180 步
powershell -File tools/acceptance.ps1 # 模拟验收:克隆仓库 + 从 mooncakes 装包,各跑一遍pub struct AngleLimitJoint {
body_a : Int
body_b : Int
lower : Double
upper : Double
impulse : Double
mass : Double
active : Int
has_pivot : Bool
local_pivot_b : Vec2
}fn AngleLimitJoint::at(world : World, body_a : Int, body_b : Int, anchor : Vec2, lower : Double, upper : Double) -> AngleLimitJointfn AngleLimitJoint::new(body_a : Int, body_b : Int, lower : Double, upper : Double) -> AngleLimitJointfn Arbiter::new(body_a : Int, body_b : Int, contacts : Array[ContactPoint], friction : Double, restitution : Double) -> Arbiterfn Arbiter::update(self : Arbiter, contacts : Array[ContactPoint], friction : Double, restitution : Double) -> Unitpub struct Body {
body_type : BodyType
shape : Shape
position : Vec2
rotation : Double
velocity : Vec2
angular_velocity : Double
force : Vec2
torque : Double
mass : Double
inv_mass : Double
inertia : Double
inv_inertia : Double
friction : Double
restitution : Double
linear_damping : Double
angular_damping : Double
allow_sleep : Bool
awake : Bool
sleep_time : Double
user_data : Int
world_aabb : Aabb
}pub enum BodyType {
Static
Dynamic
}fn DistanceJoint::new(body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2, length : Double) -> DistanceJointfn DistanceJoint::with_softness(self : DistanceJoint, frequency : Double, damping_ratio : Double) -> DistanceJointpub enum Joint {
Distance(DistanceJoint)
Revolve(RevolveJoint)
Prismatic(PrismaticJoint)
AngleLimit(AngleLimitJoint)
Motor(MotorJoint)
}pub struct MotorJoint {
body_a : Int
body_b : Int
target_speed : Double
max_torque : Double
impulse : Double
mass : Double
cap : Double
}fn MotorJoint::new(body_a : Int, body_b : Int, target_speed : Double, max_torque : Double) -> MotorJointpub struct PrismaticJoint {
body_a : Int
body_b : Int
local_anchor_a : Vec2
local_anchor_b : Vec2
local_axis_a : Vec2
has_limits : Bool
lower : Double
upper : Double
motor_speed : Double
max_motor_force : Double
impulse : Vec2
motor_impulse : Double
limit_impulse : Double
k11 : Double
k12 : Double
k22 : Double
axis_mass : Double
motor_cap : Double
bias : Vec2
active : Int
}fn PrismaticJoint::at(world : World, body_a : Int, body_b : Int, anchor : Vec2, axis : Vec2) -> PrismaticJointfn PrismaticJoint::new(body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2, local_axis_a : Vec2) -> PrismaticJointfn PrismaticJoint::with_limits(self : PrismaticJoint, lower : Double, upper : Double) -> PrismaticJointfn PrismaticJoint::with_motor(self : PrismaticJoint, speed : Double, max_force : Double) -> PrismaticJointfn RevolveJoint::new(body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2) -> RevolveJointpub struct Vec2 {
x : Double
y : Double
}fn World::add_angle_limit_joint(self : World, body_a : Int, body_b : Int, lower : Double, upper : Double) -> AngleLimitJointfn World::add_angle_limit_joint_at(self : World, body_a : Int, body_b : Int, anchor : Vec2, lower : Double, upper : Double) -> AngleLimitJointfn World::add_motor_joint(self : World, body_a : Int, body_b : Int, target_speed : Double, max_torque : Double) -> MotorJointfn World::add_prismatic_joint(self : World, body_a : Int, body_b : Int, anchor : Vec2, axis : Vec2) -> PrismaticJointlet slider = world.add_prismatic_joint(rail, cart, anchor, Vec2::new(1.0, 0.0))
slider.with_limits(-3.0, 3.0)
slider.with_motor(1.5, 40.0){
"time": 2.5,
"bodies": [
{"i": 0, "shape": "polygon", "v": [[-20, 0], [20, 0], [20, 1], [-20, 1]],
"pos": [0, 0.5], "angle": 0, "asleep": true, "tag": 0},
{"i": 1, "shape": "circle", "r": 0.4, "pos": [0, 3], "angle": 0,
"asleep": false, "tag": 7}
]
}let world = @moonphys.World::builder()
.ground(20.0)
.ball(0.4, 0.0, 4.0)
.box(0.5, 0.5, 1.0, 6.0)
.build()fn WorldBuilder::box(self : WorldBuilder, half_width : Double, half_height : Double, x : Double, y : Double) -> WorldBuilderlet LINEAR_SLOP : DoubleInstall
Download zipA deterministic 2D rigid body physics engine in pure MoonBit: SAT collision detection, sequential impulse solver with warm starting, Coulomb friction and restitution, revolute/distance/prismatic/angle-limit/motor joints, sleeping, ray casts and region queries, snapshot JSON with a state digest, plus SVG rendering and a WebAssembly playground.