跳到正文

物理世界 ​

Box2D 把所有刚体的运动与碰撞交给一个 world 统一计算。你不写重力、不写反弹,只负责创建物体并推进时间。

世界是容器 ​

world 不是场景,也不负责绘制。它持有 body、joint 和接触信息,world:update(dt) 时执行碰撞检测、求解约束、更新位置。

lua
world = love.physics.newWorld(0, 300)

前两个参数是 X、Y 方向的重力。第三个参数 sleep 默认 true,静止的物体会休眠,减少无效计算。

俯视游戏把两个重力都设成 0,物体只受冲量、关节和阻尼影响;水下关卡可以把 Y 调到 50 左右,下落变慢,浮力感更强。

调用作用
love.physics.newWorld(gx, gy, sleep)创建世界
world:update(dt, velIter, posIter)推进模拟
world:getGravity()读回重力
world:getBodyCount()当前 body 数量
world:setCallbacks(...)注册碰撞回调

LÖVE 11 里 velocityIterations 默认 8,positionIterations 默认 3。调高更稳定,代价是每帧更慢。

单位与坐标 ​

Box2D 内部以米计算,LÖVE 在 API 边界按比例换算,默认 30 像素等于 1 米。地球重力 9.8 m/s² 因此写成 9.8 × 30 ≈ 300 像素每二次方秒。

love.physics.setMeter(30) 可以改比例,但要在创建任何物理对象之前调用。已有对象的米制坐标不会重算,换算结果会突然改变。

量单位例子
位置像素newBody(world, 320, 180)
速度像素/秒setLinearVelocity(300, -200)
重力像素/秒²newWorld(0, 300)
角度弧度body:getAngle()
质量千克body:getMass()

Box2D 建议动态形状落在 0.1 到 10 米之间,按 30 像素/米算就是 3 到 300 像素。35 像素的方块约 1.2 米,正合适;静态地面通常用一条很长的 EdgeShape,不必拼成小段。

固定步长 ​

直接 world:update(dt),低帧率时会给出很大的 dt,物体一步跨过障碍,看起来像穿墙。课程用累加器把物理固定在 60 Hz:

lua
local FIXED_DT = 1 / 60
local accumulator = 0

function Level:update(dt)
    accumulator = math.min(accumulator + dt, 0.25)

    while accumulator >= FIXED_DT do
        self.world:update(FIXED_DT)
        accumulator = accumulator - FIXED_DT
    end
end

math.min 是保险:一帧卡了 2 秒时最多补 0.25 秒,避免一次算上百步、把帧率拖得更低。

是否累加 dt够一步world update减去一步结束本帧

高速物体还可以给 body 打开 setBullet(true),让 Box2D 做连续碰撞检测。静态 body 本来就总是用连续检测,所以墙不会被高速物体穿过。

物理频率不必等于渲染频率。60 Hz 是 Box2D 的经验值,30 Hz 更省 CPU,但快速物体更容易漏判;再低就会看到堆叠物抖动和穿透。

生命周期 ​

一个关卡一个 world,换关就重建。body 创建时绑定 world,之后不能搬到另一个世界。销毁用 body:destroy(),挂在上面的 fixture 与 joint 一起失效。

不要在 beginContact 里改世界。Box2D 在回调期间锁定世界,增加或删除对象会崩溃。把要删的 body 收集起来,等 world:update 返回后再处理。

常见坑 ​

  • 把重力写成 9.8,物体几乎不动
  • 忘记调用 world:update,所有物体停在原地
  • 在 love.draw 里推进物理,速度由帧率决定
  • 把 world 当成渲染层级,试图往里面塞精灵
  • 关卡切换后复用旧 body,旧世界残留
  • 累加器不设上限,掉帧后连续补步雪崩