Skip to content

Web 游戏物理

物理模拟让游戏变得生动起来——下落的物体、弹跳的球、布娃娃、载具和可破坏环境。本指南介绍适用于 Web 游戏的主要物理库,并提供真实的代码示例以及客观的优缺点分析。

一览

引擎维度性能大小难度软体载具CCD确定性
Rapier2D/3D⭐⭐⭐⭐⭐1.4 MB中等
Cannon-es3D⭐⭐⭐150 KB简单
Ammo.js3D⭐⭐⭐⭐⭐1-2 MB困难⚠️
Jolt3D⭐⭐⭐⭐⭐困难
Oimo.js3D⭐⭐⭐100 KB简单
Matter.js2D⭐⭐⭐80 KB简单⚠️
Planck.js2D⭐⭐⭐⭐120 KB中等⚠️
p2-es2D⭐⭐⭐100 KB中等
Box2D WASM2D⭐⭐⭐⭐⭐300 KB困难

图例:

  • CCD = 连续碰撞检测(防止高速物体穿过墙壁)
  • ⚠️ 确定性 = 使用固定时间步长时可以实现,但不保证跨平台一致

快速推荐

你的情况最佳选择
正式制作 3D 游戏Rapier — 性能最佳、API 现代、开发活跃
学习/原型开发Cannon-es(3D)或 Matter.js(2D)— API 简单,易于调试
载具物理RapierAmmo.js — 两者都提供射线投射载具控制器
软体、布料、绳索Ammo.jsJolt(JoltPhysics.js) — 两者都支持布料和软体;Jolt 的 WASM 移植版维护更活跃
精确的 2D 平台游戏Planck.js — 采用 Box2D 算法,使用固定时间步长时具备确定性
最高 2D 性能Box2D WASM — 在浏览器中实现原生级速度
最小打包体积Oimo.js(3D)或 Matter.js(2D)

详细比较

3D 物理引擎

引擎语言大小性能最适合
RapierRust/WASM~1.4 MB出色正式游戏、复杂模拟
Cannon-esJavaScript~150 KB良好原型、简单游戏
Ammo.jsC++/WASM~1-2 MB出色AAA 级功能、软体、载具
JoltC++/WASMWASM出色需要活跃维护的 AAA 级功能
Oimo.jsJavaScript~100 KB良好简单游戏、快速原型

2D 物理引擎

引擎语言大小性能最适合
Matter.jsJavaScript~80 KB良好视觉类游戏、原型
Planck.jsJavaScript~120 KB良好平台游戏、精确物理
p2-esJavaScript~100 KB良好约束、机械结构
Box2D WASMC++/WASM~300 KB出色大量刚体

3D 物理引擎

Rapier — 现代之选

Rapier 是一款提供 JavaScript/WASM 绑定的 Rust 物理引擎。它是 2025–2026 年 Web 游戏中性能最强的选择,相比其 2024 年版本,速度提升了 2–5 倍。

安装:

bash
npm install @dimforge/rapier3d
# 或使用 SIMD(速度更快,需要现代浏览器):
npm install @dimforge/rapier3d-simd

基础设置:

js
import RAPIER from '@dimforge/rapier3d'

// 初始化(WASM 需要异步执行)
await RAPIER.init()

// 创建带重力的世界
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)

// 创建地面(静态刚体)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)

// 创建下落的箱子(动态刚体)
const boxDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 10, 0)
const boxBody = world.createRigidBody(boxDesc)
const boxCollider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  .setDensity(1.0)
  .setRestitution(0.5)
world.createCollider(boxCollider, boxBody)

游戏循环集成:

js
const FIXED_TIMESTEP = 1 / 60

function physicsStep() {
  world.step()
}

function gameLoop() {
  physicsStep()
  
  // 将渲染对象与物理状态同步
  const position = boxBody.translation()
  const rotation = boxBody.rotation()
  
  // 更新你的 Three.js/Babylon 网格
  mesh.position.set(position.x, position.y, position.z)
  mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)
  
  requestAnimationFrame(gameLoop)
}

碰撞检测:

js
// 基于事件的碰撞检测
world.contactPairsWith(boxCollider, (otherCollider) => {
  console.log('箱子正在接触:', otherCollider)
})

// 射线投射
const ray = new RAPIER.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 })
const hit = world.castRay(ray, 100, true)
if (hit) {
  const hitPoint = ray.pointAt(hit.timeOfImpact)
  console.log('命中位置:', hitPoint)
}

关节:

js
// 创建铰链关节(门)
const jointData = RAPIER.JointData.revolute(
  { x: 0, y: 0, z: 0 },  // 刚体 1 上的锚点
  { x: -1, y: 0, z: 0 }, // 刚体 2 上的锚点
  { x: 0, y: 1, z: 0 }   // 旋转轴
)
world.createImpulseJoint(jointData, body1, body2, true)

载具控制器:

js
// 创建底盘刚体
const chassisDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 2, 0)
const chassis = world.createRigidBody(chassisDesc)
const chassisCollider = RAPIER.ColliderDesc.cuboid(1, 0.5, 2)
  .setDensity(100)
world.createCollider(chassisCollider, chassis)

// 创建载具控制器
const vehicle = world.createVehicleController(chassis)

// 添加车轮(左前、右前、左后、右后)
const suspensionRestLength = 0.3
const wheelRadius = 0.4

// 前轮(转向)
vehicle.addWheel(
  { x: -0.8, y: 0, z: 1.2 },   // 连接点
  { x: 0, y: -1, z: 0 },       // 悬挂方向
  { x: -1, y: 0, z: 0 },       // 车轴方向
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: 1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// 后轮(驱动)
vehicle.addWheel(
  { x: -0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// 配置所有车轮的悬挂
for (let i = 0; i < 4; i++) {
  vehicle.setWheelSuspensionStiffness(i, 30)
  vehicle.setWheelSuspensionCompression(i, 4.4)
  vehicle.setWheelSuspensionRelaxation(i, 2.3)
  vehicle.setWheelMaxSuspensionTravel(i, 0.5)
  vehicle.setWheelFrictionSlip(i, 2)
}

// 在游戏循环中
function updateVehicle(steering, engineForce, brakeForce) {
  // 转向(仅前轮)
  vehicle.setWheelSteering(0, steering)
  vehicle.setWheelSteering(1, steering)
  
  // 引擎(后轮)
  vehicle.setWheelEngineForce(2, engineForce)
  vehicle.setWheelEngineForce(3, engineForce)
  
  // 制动(所有车轮)
  for (let i = 0; i < 4; i++) {
    vehicle.setWheelBrake(i, brakeForce)
  }
  
  // 更新载具物理
  vehicle.updateVehicle(world.timestep)
}

优点:

  • 性能最佳(WASM + SIMD)
  • 碰撞检测精度出色
  • 跨平台确定性(相同输入 = 相同输出)
  • 开发活跃,API 现代
  • 支持连续碰撞检测(不会穿透)
  • 内置角色控制器和载具控制器

缺点:

  • 打包体积较大(约 1.4 MB)
  • 需要异步初始化
  • API 比纯 JS 替代方案更复杂
  • WASM 调试可能比较棘手

Cannon-es — 简单而有效

Cannon-es 是 Cannon.js 的持续维护分支。它采用纯 JavaScript 编写,易于理解,非常适合学习和原型开发。

安装:

bash
npm install cannon-es

基础设置:

js
import * as CANNON from 'cannon-es'

// 创建世界
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.81, 0)
})

// 地面
const groundBody = new CANNON.Body({
  type: CANNON.Body.STATIC,
  shape: new CANNON.Plane()
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)

// 下落的球体
const sphereBody = new CANNON.Body({
  mass: 1,
  shape: new CANNON.Sphere(0.5),
  position: new CANNON.Vec3(0, 10, 0)
})
sphereBody.linearDamping = 0.1
world.addBody(sphereBody)

游戏循环:

js
const TIMESTEP = 1 / 60

function animate() {
  world.step(TIMESTEP)
  
  // 与 Three.js 同步
  mesh.position.copy(sphereBody.position)
  mesh.quaternion.copy(sphereBody.quaternion)
  
  requestAnimationFrame(animate)
}

碰撞事件:

js
sphereBody.addEventListener('collide', (event) => {
  const contact = event.contact
  const impactVelocity = contact.getImpactVelocityAlongNormal()
  
  if (Math.abs(impactVelocity) > 5) {
    console.log('强烈撞击!')
  }
})

约束:

js
// 距离约束(类似绳索)
const constraint = new CANNON.DistanceConstraint(
  bodyA, bodyB, 
  2 // 距离
)
world.addConstraint(constraint)

// 铰链约束
const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
  pivotA: new CANNON.Vec3(1, 0, 0),
  axisA: new CANNON.Vec3(0, 1, 0),
  pivotB: new CANNON.Vec3(-1, 0, 0),
  axisB: new CANNON.Vec3(0, 1, 0)
})
world.addConstraint(hinge)

优点:

  • 纯 JavaScript,没有 WASM 的复杂性
  • 打包体积小(约 150 KB)
  • 易于学习和调试
  • 兼容所有环境
  • 与 Three.js 集成良好
  • 文档完善

缺点:

  • 比 WASM 替代方案慢
  • 难以处理大量刚体(>100)
  • 对三角网格的支持有限
  • 没有内置 CCD(可能发生穿透)
  • 开发活跃度较低

Ammo.js — 完整发挥 Bullet Physics 的强大功能

Ammo.js 是编译为 WebAssembly 的 Bullet Physics 引擎。它提供最全面的功能,包括软体、载具和高级约束。

安装:

bash
npm install ammo.js
# 或通过 CDN 使用

基础设置:

js
import Ammo from 'ammo.js'

let physicsWorld

async function initPhysics() {
  await Ammo()
  
  const collisionConfig = new Ammo.btDefaultCollisionConfiguration()
  const dispatcher = new Ammo.btCollisionDispatcher(collisionConfig)
  const broadphase = new Ammo.btDbvtBroadphase()
  const solver = new Ammo.btSequentialImpulseConstraintSolver()
  
  physicsWorld = new Ammo.btDiscreteDynamicsWorld(
    dispatcher, broadphase, solver, collisionConfig
  )
  physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0))
}

function createBox(mass, width, height, depth, x, y, z) {
  const transform = new Ammo.btTransform()
  transform.setIdentity()
  transform.setOrigin(new Ammo.btVector3(x, y, z))
  
  const motionState = new Ammo.btDefaultMotionState(transform)
  const shape = new Ammo.btBoxShape(
    new Ammo.btVector3(width / 2, height / 2, depth / 2)
  )
  
  const localInertia = new Ammo.btVector3(0, 0, 0)
  if (mass > 0) {
    shape.calculateLocalInertia(mass, localInertia)
  }
  
  const rbInfo = new Ammo.btRigidBodyConstructionInfo(
    mass, motionState, shape, localInertia
  )
  const body = new Ammo.btRigidBody(rbInfo)
  
  physicsWorld.addRigidBody(body)
  return body
}

软体(布料):

js
function createCloth(width, height, segments) {
  const softBodyHelpers = new Ammo.btSoftBodyHelpers()
  
  const corner00 = new Ammo.btVector3(-width/2, height, 0)
  const corner10 = new Ammo.btVector3(width/2, height, 0)
  const corner01 = new Ammo.btVector3(-width/2, 0, 0)
  const corner11 = new Ammo.btVector3(width/2, 0, 0)
  
  const softBody = softBodyHelpers.CreatePatch(
    physicsWorld.getWorldInfo(),
    corner00, corner10, corner01, corner11,
    segments, segments,
    0, true
  )
  
  const sbConfig = softBody.get_m_cfg()
  sbConfig.set_viterations(10)
  sbConfig.set_piterations(10)
  
  softBody.setTotalMass(0.9, false)
  Ammo.castObject(softBody, Ammo.btCollisionObject)
    .getCollisionShape().setMargin(0.05)
  
  physicsWorld.addSoftBody(softBody, 1, -1)
  return softBody
}

载具物理:

js
function createVehicle(chassisBody) {
  const tuning = new Ammo.btVehicleTuning()
  const rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld)
  const vehicle = new Ammo.btRaycastVehicle(tuning, chassisBody, rayCaster)
  
  vehicle.setCoordinateSystem(0, 1, 2)
  physicsWorld.addAction(vehicle)
  
  // 添加车轮
  const wheelRadius = 0.4
  const wheelWidth = 0.3
  const suspensionRestLength = 0.3
  
  const wheelDirectionCS = new Ammo.btVector3(0, -1, 0)
  const wheelAxleCS = new Ammo.btVector3(-1, 0, 0)
  
  function addWheel(isFront, pos) {
    const wheelInfo = vehicle.addWheel(
      pos, wheelDirectionCS, wheelAxleCS,
      suspensionRestLength, wheelRadius, tuning, isFront
    )
    wheelInfo.set_m_suspensionStiffness(20)
    wheelInfo.set_m_wheelsDampingRelaxation(2.3)
    wheelInfo.set_m_wheelsDampingCompression(4.4)
    wheelInfo.set_m_frictionSlip(1000)
    wheelInfo.set_m_rollInfluence(0.1)
  }
  
  addWheel(true, new Ammo.btVector3(1, 0, 1.5))   // 左前轮
  addWheel(true, new Ammo.btVector3(-1, 0, 1.5))  // 右前轮
  addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // 左后轮
  addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// 右后轮
  
  return vehicle
}

优点:

  • 功能集最完整
  • 支持软体、布料和绳索
  • 高级载具物理
  • 经过大量实战检验(许多 AAA 游戏都在使用)
  • 可高度配置 缺点:
  • API 复杂且冗长
  • 打包体积大
  • 需要手动管理内存(销毁对象)
  • 学习曲线陡峭
  • 文档分散
  • 无法实现跨平台确定性(仅能在同一设备上通过谨慎配置实现)

Jolt — 现代 AAA 级 3D 物理引擎

Jolt Physics 是《地平线:西之绝境》等游戏所使用的物理引擎,而 JoltPhysics.js 通过 WASM 移植将其带到浏览器中。它在 Rapier 和 Ammo.js 之间实现了良好平衡:功能比 Rapier 更多(软体、布料和轮式载具控制器),同时又拥有 Ammo.js 所欠缺的活跃维护。其 npm 包为 jolt-physics,并且已有适用于 React Three Fiber(@react-three/jolt)和 Babylon.js 的现成集成。

bash
npm install jolt-physics

与 Rapier 一样,它也是异步初始化的 WASM,引擎 API 虽然冗长但功能完整,并与 C++ 接口保持一致。如果你需要 Bullet 级别的功能(软体、载具),但希望使用维护活跃的现代代码库而不是 Ammo.js,那么可以选择 Jolt。


Oimo.js — 轻量且快速

Oimo.js 是一款轻量级 3D 物理引擎,非常适合不需要高级功能的简单游戏。

安装:

bash
npm install oimo

基本设置:

js
import * as OIMO from 'oimo'

const world = new OIMO.World({
  timestep: 1/60,
  iterations: 8,
  broadphase: 2, // 1:暴力检测,2:扫描与剪枝,3:包围体树
  worldscale: 1,
  random: true,
  gravity: [0, -9.8, 0]
})

// 创建地面
world.add({
  type: 'box',
  size: [100, 1, 100],
  pos: [0, -0.5, 0],
  move: false
})

// 创建下落的球体
const sphere = world.add({
  type: 'sphere',
  size: [1],
  pos: [0, 10, 0],
  move: true,
  density: 1,
  friction: 0.4,
  restitution: 0.2
})

游戏循环:

js
function animate() {
  world.step()
  
  // 获取位置和旋转
  const pos = sphere.getPosition()
  const rot = sphere.getQuaternion()
  
  mesh.position.set(pos.x, pos.y, pos.z)
  mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  
  requestAnimationFrame(animate)
}

优点:

  • 体积非常小(约 100 KB)
  • API 简单
  • 在基础场景中性能良好
  • 内置 Babylon.js 支持

缺点:

  • 形状有限(仅支持基本几何体)
  • 不支持软体
  • 文档较少
  • 开发活跃度较低
  • 关节选项有限

2D 物理引擎

Matter.js — 美观且直观

Matter.js 是一款功能丰富的 2D 物理引擎,拥有出色的渲染和调试工具。

安装:

bash
npm install matter-js

基本设置:

js
import Matter from 'matter-js'

const { Engine, Render, World, Bodies, Runner } = Matter

// 创建引擎
const engine = Engine.create()

// 创建渲染器(可选,非常适合调试)
const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 800,
    height: 600,
    wireframes: false
  }
})

// 创建刚体
const ground = Bodies.rectangle(400, 580, 810, 60, { 
  isStatic: true 
})

const box = Bodies.rectangle(400, 200, 80, 80, {
  restitution: 0.8,
  friction: 0.5
})

const circle = Bodies.circle(300, 100, 40, {
  restitution: 0.9
})

// 添加到世界
World.add(engine.world, [ground, box, circle])

// 运行
Render.run(render)
Runner.run(Runner.create(), engine)

碰撞事件:

js
Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach(pair => {
    console.log('发生碰撞:', pair.bodyA.label, pair.bodyB.label)
  })
})

约束:

js
// 固定点约束
const pin = Matter.Constraint.create({
  pointA: { x: 400, y: 100 },
  bodyB: box,
  stiffness: 0.9
})

// 刚体之间的弹簧
const spring = Matter.Constraint.create({
  bodyA: box,
  bodyB: circle,
  stiffness: 0.01,
  length: 100
})

World.add(engine.world, [pin, spring])

鼠标交互:

js
const mouse = Matter.Mouse.create(render.canvas)
const mouseConstraint = Matter.MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,
    render: { visible: false }
  }
})
World.add(engine.world, mouseConstraint)

优点:

  • 默认渲染效果美观
  • 非常适合学习和原型开发
  • API 直观
  • 文档完善
  • 社区活跃
  • 默认具有确定性——Matter.Runner 现在使用固定的确定性时间步长(非固定时间步长已在 v0.20.0 中移除)

缺点:

  • 不支持 CCD(高速物体可能穿透——可使用子步进缓解)
  • 刚体数量较多时存在性能问题
  • 没有 WASM 版本
  • 复杂模拟的精度有限

Planck.js — JavaScript 版 Box2D

Planck.js 是使用 JavaScript 对 Box2D 的完整重写。其物理系统经过大量实战检验,并且在使用固定时间步长时具有确定性。

安装:

bash
npm install planck

基本设置:

js
import { World, Vec2, Box, Circle, Edge } from 'planck'

// 创建世界
const world = new World({
  gravity: Vec2(0, -10)
})

// 创建地面
const ground = world.createBody()
ground.createFixture({
  shape: Edge(Vec2(-40, 0), Vec2(40, 0))
})

// 创建动态盒体
const box = world.createBody({
  type: 'dynamic',
  position: Vec2(0, 10)
})
box.createFixture({
  shape: Box(1, 1),
  density: 1,
  friction: 0.3,
  restitution: 0.5
})

固定时间步长游戏循环:

js
const TIMESTEP = 1 / 60
const VELOCITY_ITERATIONS = 8
const POSITION_ITERATIONS = 3

function gameLoop() {
  world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
  
  // 遍历所有刚体
  for (let body = world.getBodyList(); body; body = body.getNext()) {
    const pos = body.getPosition()
    const angle = body.getAngle()
    // 更新你的精灵……
  }
  
  requestAnimationFrame(gameLoop)
}

碰撞回调:

js
world.on('begin-contact', (contact) => {
  const fixtureA = contact.getFixtureA()
  const fixtureB = contact.getFixtureB()
  console.log('接触开始')
})

world.on('end-contact', (contact) => {
  console.log('接触结束')
})

world.on('pre-solve', (contact, oldManifold) => {
  // 可以在此处禁用接触
  // contact.setEnabled(false)
})

关节:

js
import { RevoluteJoint, DistanceJoint, PrismaticJoint } from 'planck'

// 旋转关节(铰链)
const joint = world.createJoint(RevoluteJoint({
  bodyA: ground,
  bodyB: box,
  localAnchorA: Vec2(0, 5),
  localAnchorB: Vec2(-1, 0),
  enableMotor: true,
  maxMotorTorque: 1000,
  motorSpeed: 2
}))

// 距离关节(弹簧)
world.createJoint(DistanceJoint({
  bodyA: boxA,
  bodyB: boxB,
  localAnchorA: Vec2(0, 0),
  localAnchorB: Vec2(0, 0),
  length: 5,
  stiffness: 10,
  damping: 0.5
}))

优点:

  • 使用固定时间步长时具有确定性
  • 文档完善(可参考 Box2D 文档)
  • 性能良好
  • 支持 TypeScript
  • 打包体积小

缺点:

  • CCD 无法处理关节,因此由关节连接的高速物体可能被拉伸
  • 学习曲线比 Matter.js 更陡峭
  • 没有内置渲染器
  • 存在 Box2D 特有的注意事项(单位比例很重要)

p2-es — 灵活的 2D 物理引擎

p2-es 是 p2.js 持续维护的分支,非常适合需要复杂约束和机械结构的游戏。

安装:

bash
npm install p2-es

基本设置:

js
import * as p2 from 'p2-es'

const world = new p2.World({
  gravity: [0, -9.81]
})

// 地面
const groundBody = new p2.Body({
  mass: 0, // 静态
  position: [0, -1]
})
groundBody.addShape(new p2.Plane())
world.addBody(groundBody)

// 动态圆形刚体
const circleBody = new p2.Body({
  mass: 1,
  position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)

高级约束:

js
// 齿轮约束
const gear = new p2.GearConstraint(bodyA, bodyB, {
  ratio: 2 // bodyB 的旋转速度是 bodyA 的两倍
})
world.addConstraint(gear)

// 棱柱约束(滑块)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
  localAnchorA: [0, 0],
  localAnchorB: [0, 0],
  localAxisA: [1, 0],
  disableRotationalLock: false
})
world.addConstraint(prismatic)

// 锁定约束(焊接)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)

接触材质:

js
const ice = new p2.Material()
const rubber = new p2.Material()

const iceRubber = new p2.ContactMaterial(ice, rubber, {
  friction: 0.1,
  restitution: 0.9
})
world.addContactMaterial(iceRubber)

// 应用到形状
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber

优点:

  • 丰富的约束系统
  • 支持接触材质
  • 适合机械结构和机器
  • 支持刚体休眠(提升性能)
  • 使用 ES 模块,支持 tree-shaking

缺点:

  • 没有内置渲染器
  • 不支持某些形状组合
  • 活跃度低于其他替代方案
  • 文档存在缺失

性能优化技巧

1. 使用固定时间步长

js
const TIMESTEP = 1 / 60
let accumulator = 0

function gameLoop(deltaTime) {
  accumulator += deltaTime
  
  while (accumulator >= TIMESTEP) {
    world.step(TIMESTEP)
    accumulator -= TIMESTEP
  }
  
  // 通过插值实现平滑渲染
  const alpha = accumulator / TIMESTEP
  // lerp(previousState, currentState, alpha)
}

2. 让不活跃的刚体休眠

大多数引擎都支持休眠。请将其启用:

js
// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1

// Rapier
bodyDesc.setCanSleep(true)

3. 使用简单的碰撞形状

js
// 推荐:简单的基本几何体
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))

// 避免:为动态刚体使用复杂三角网格
const trimesh = new CANNON.Trimesh(vertices, indices) // 很慢!

4. 减少求解器迭代次数(请谨慎)

js
// Cannon-es
world.solver.iterations = 5 // 默认为 10

// Rapier——在创建世界时配置

5. 在 Web Worker 中运行物理模拟

js
// main.js
const worker = new Worker('physics-worker.js')

worker.postMessage({ type: 'step', deltaTime: 1/60 })
worker.onmessage = (e) => {
  const { positions, rotations } = e.data
  // 更新渲染对象
}

// physics-worker.js
importScripts('cannon-es.js')
const world = new CANNON.World()

onmessage = (e) => {
  if (e.data.type === 'step') {
    world.step(e.data.deltaTime)
    postMessage({
      positions: bodies.map(b => b.position.toArray()),
      rotations: bodies.map(b => b.quaternion.toArray())
    })
  }
}

不同场景该如何选择

场景推荐原因
正式发布的 3D 游戏Rapier性能最佳、API 现代、支持载具
3D 游戏原型Cannon-es简单、易于调试
软体、布料、绳索Ammo.jsJolt两者都支持布料和软体;Jolt 的 WASM 移植版(JoltPhysics.js)维护更活跃
简单的 3D 浏览器游戏Oimo.js体积小且功能足够
2D Game JamMatter.js设置快速、内置渲染
精确的平台跳跃游戏Planck.js固定时间步长具有确定性,文档完善
复杂的 2D 机械结构p2-es约束系统丰富
性能要求极高的 2D 游戏Box2D WASM最快的 2D 方案,支持真正的 CCD

与渲染器集成

Three.js + Rapier

js
import * as THREE from 'three'
import RAPIER from '@dimforge/rapier3d'

await RAPIER.init()

const scene = new THREE.Scene()
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 })

const bodies = new Map() // 物理刚体 -> 网格

function createPhysicsBox(x, y, z) {
  // 物理
  const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z)
  const body = world.createRigidBody(bodyDesc)
  const collider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  world.createCollider(collider, body)
  
  // 渲染
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x00ff00 })
  )
  scene.add(mesh)
  
  bodies.set(body, mesh)
  return body
}

function syncPhysics() {
  bodies.forEach((mesh, body) => {
    const pos = body.translation()
    const rot = body.rotation()
    mesh.position.set(pos.x, pos.y, pos.z)
    mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  })
}

PixiJS + Matter.js

js
import * as PIXI from 'pixi.js'
import Matter from 'matter-js'

const app = new PIXI.Application({ width: 800, height: 600 })
document.body.appendChild(app.view)

const engine = Matter.Engine.create()
const bodies = new Map()

function createPhysicsSprite(texture, x, y, width, height) {
  // 物理
  const body = Matter.Bodies.rectangle(x, y, width, height)
  Matter.World.add(engine.world, body)
  
  // 渲染
  const sprite = new PIXI.Sprite(texture)
  sprite.anchor.set(0.5)
  app.stage.addChild(sprite)
  
  bodies.set(body, sprite)
  return body
}

app.ticker.add(() => {
  Matter.Engine.update(engine, 1000 / 60)
  
  bodies.forEach((sprite, body) => {
    sprite.position.set(body.position.x, body.position.y)
    sprite.rotation = body.angle
  })
})

常见陷阱

比例很重要

物理引擎在现实世界的比例下运行效果最佳(1 个单位 = 1 米)。不要直接使用像素坐标。

js
// 错误:使用像素位置
const body = Bodies.circle(400, 300, 50) // 半径为 50 像素?

// 正确:使用缩放系数
const SCALE = 50 // 每米 50 像素
const body = Bodies.circle(8, 6, 1) // 半径为 1 米
// 渲染时再乘以 SCALE

WASM 引擎中的内存泄漏

移除刚体时,务必清理相关对象:

js
// Rapier
world.removeRigidBody(body)

// Ammo.js
physicsWorld.removeRigidBody(body)
Ammo.destroy(body)
Ammo.destroy(shape)
Ammo.destroy(motionState)

穿透(物体相互穿过)

高速移动的物体可能穿过较薄的墙体。解决方案:

js
// Rapier:启用 CCD
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)

// Cannon-es:使用更小的时间步长或更厚的墙体
world.step(1/120) // 120 Hz,而不是 60 Hz

相关内容

外部资源