跳到主要内容

物理入门

PlayCanvas 物理基于 ammo.js,即开源 Bullet 物理引擎的 WebAssembly 移植。加载后,为实体添加决定运动方式的 rigidbody 和提供形状的 collision 即可构建模拟。本页介绍加载库、全局设置及本节其他页面依赖的基础规则。

启用物理​

物理需要主动启用。ammo.js 有数百 KB,因此不会自动加载。它包含三个文件:WebAssembly 模块 ammo.wasm.wasm、JavaScript 胶水代码 ammo.wasm.js,以及供不支持 WebAssembly 的浏览器使用的 asm.js 回退 ammo.js。库加载前,物理组件只是占位,添加它们不会生效,也不会报错。

创建应用前先加载模块。三个文件位于引擎仓库 examples/assets/wasm/ammo/,也包含在 sync-ammo npm 包中。

pc.WasmModule.setConfig('Ammo', {
glueUrl: 'ammo/ammo.wasm.js',
wasmUrl: 'ammo/ammo.wasm.wasm',
fallbackUrl: 'ammo/ammo.js'
});

// Wait for the module, then create the application as usual
await new Promise((resolve) => {
pc.WasmModule.getInstance('Ammo', () => resolve());
});

const app = new pc.Application(canvas);

应用启动时,刚体系统查找全局 Ammo 并安装 ammo.js 后端。pc.Application 自动注册全部组件系统;使用 pc.AppBase 和 pc.AppOptions 时,需要向 componentSystems 添加 pc.CollisionComponentSystem、pc.RigidBodyComponentSystem,使用关节时还需 pc.JointComponentSystem。Falling Shapes 示例展示完整流程。

重力​

重力是施加到所有动态刚体的恒定加速度,默认沿世界 Y 轴向下,值为 -9.81,近似地球。太空游戏可设为零,月球场景可降低。单独改变刚体受重力程度,可使用重力缩放。

// Lunar gravity. The older setGravity() method is deprecated in favor of this property.
app.systems.rigidbody.gravity = new pc.Vec3(0, -1.62, 0);

测量单位​

物理引擎将一个单位解释为一米,质量以千克计。场景应使用相应比例,例如 1.8 米高的角色应高 1.8 个单位。相差很大的比例虽然仍能模拟,但重力看起来会过强或过弱,极小形状也容易互相穿透。

模拟如何运行​

引擎驱动物理模拟的方式决定以下基础规则:

  • **固定频率步进。**每帧以 1/60 秒固定步长推进,按帧时间执行所需步数,受最大步数限制。因此 30 FPS 和 144 FPS 的物理行为一致。高于 60 Hz 时,部分帧没有物理步,通过变换插值保持平滑。
  • **物理控制动态刚体。**每步后将运动刚体的位置和旋转写回实体。设置实体变换不会移动刚体,因此应使用力、速度或 teleport,参见刚体。
  • **你控制运动学刚体。**每步开始从实体读取变换,因此像普通实体一样移动即可。
  • **静态刚体保持创建时位置。**创建时读取一次位置和旋转,之后不再跟踪实体。运行中需要移动的对象应使用运动学类型。
  • **刚体没有缩放。**实体只传递位置和旋转,基本几何碰撞形状始终保持自身尺寸,参见缩放。
  • **每步发出事件。**碰撞和触发事件按物理步而非渲染帧触发。静置在地板上的刚体每步都会产生 contact。

暂停与慢动作​

刚体系统的 timeScale 缩放每帧推进的模拟时间,默认为 1,小于 1 为慢动作,大于 1 为加速,0 为暂停。暂停时刚体保留速度并冻结,运动学刚体停止跟随实体,也不发出碰撞或触发事件。脚本和动画等其他部分继续运行,适合保持暂停菜单交互。

// Freeze physics while the pause menu is open
app.systems.rigidbody.timeScale = 0;

// Run physics at quarter speed for a slow motion effect
app.systems.rigidbody.timeScale = 0.25;

模拟仍采用 1/60 秒固定步长。慢动作可能每隔数帧才执行一步,中间通过插值保持平滑。加速受单帧最大步数限制:60 FPS 时最多加速十倍,更高值无法达到要求。

物理时间缩放还会叠加在应用的 timeScale 上,后者缩放脚本、动画和物理接收的 delta time。应用时间缩放设为 0,会停止所有随时间推进的内容;刚体系统设为 0,只暂停物理。

两者都不会停止脚本方法调用。update 和 postUpdate 每帧仍运行,应用时间缩放为 0 时只是 dt 为 0,因此输入、生成对象及其他未乘 dt 的逻辑仍会执行。玩法代码需要独立暂停判断,包括物理调用:暂停期间设置冲量和速度会立即生效,恢复后按新速度运动;物理时间缩放为 0 时,applyForce 的力会一直累积到下一物理步,因此每帧持续施力可能在恢复时一次释放巨大推力。

手动步进​

app.systems.rigidbody.step(dt) 立即推进 dt 秒,完成与自动更新相同的流程:运动学刚体和触发器跟随实体,执行所需固定步,动态刚体写回实体,并触发事件。dt 不应用时间缩放。

时间缩放大于 0 时,自动步进继续执行,若每帧再调用 step 就会重复推进。完全接管前先将时间缩放设为 0。这样也可用于调试,例如每按一次键推进一步,观察碰撞过程:

const physics = app.systems.rigidbody;
physics.timeScale = 0;

window.addEventListener('keydown', (event) => {
// Advance the paused simulation by one fixed step each time the period key is pressed
if (event.key === '.') {
physics.step(1 / 60);
}
});

传入 1/60 正好执行一步。更短 dt 可能只更新插值位置而不执行物理步;更长值会执行多步,每次最多十步,超出时间丢弃。连续调用可快速推进,例如在玩家看到场景前让物体堆稳定。

各开发方式调用相同,区别只在应用引用:编辑器脚本用 this.app,React 用 useApp(),Web Components 的 whenReady('pc-app') 返回 <pc-app> 元素,通过其 app 属性访问。

另请参阅​