网页游戏的游戏物理
物理模拟让游戏鲜活起来——下落的物体、弹跳的球、布娃娃、车辆、可破坏的环境。本文覆盖网页游戏可用的主流物理库,附真实代码示例和坦率的优缺点。
一览表
| 引擎 | 维度 | 性能 | 体积 | 难度 | 软体 | 车辆 | CCD | 确定性 |
|---|---|---|---|---|---|---|---|---|
| Rapier | 2D/3D | ⭐⭐⭐⭐⭐ | 1.4 MB | 中 | — | ✅ | ✅ | ✅ |
| Cannon-es | 3D | ⭐⭐⭐ | 150 KB | 易 | — | — | — | — |
| Ammo.js | 3D | ⭐⭐⭐⭐⭐ | 1-2 MB | 难 | ✅ | ✅ | ✅ | ⚠️ |
| Oimo.js | 3D | ⭐⭐⭐ | 100 KB | 易 | — | — | — | — |
| Matter.js | 2D | ⭐⭐⭐ | 80 KB | 易 | — | — | — | ⚠️ |
| Planck.js | 2D | ⭐⭐⭐⭐ | 120 KB | 中 | — | — | — | ⚠️ |
| p2-es | 2D | ⭐⭐⭐ | 100 KB | 中 | — | — | — | — |
| Box2D WASM | 2D | ⭐⭐⭐⭐⭐ | 300 KB | 难 | — | — | ✅ | ✅ |
图例:
- CCD = 连续碰撞检测(避免快速移动的物体穿墙)
- ⚠️ 确定性 = 使用固定时间步长时可行,但跨平台不保证一致
快速推荐
| 你的情况 | 最佳选择 |
|---|---|
| 生产级 3D 游戏 | Rapier —— 性能最佳、API 现代、活跃维护 |
| 学习 / 原型 | Cannon-es(3D)或 Matter.js(2D)—— API 简单、易调试 |
| 车辆物理 | Rapier 或 Ammo.js —— 都带射线投射的车辆控制器 |
| 软体、布料、绳索 | Ammo.js —— 唯一支持这些功能的选项 |
| 精确的 2D 平台跳跃 | Planck.js —— Box2D 算法,固定时间步长下确定性 |
| 2D 最高性能 | Box2D WASM —— 浏览器中的原生级速度 |
| 最小包体 | Oimo.js(3D)或 Matter.js(2D) |
详细对比
3D 物理引擎
| 引擎 | 语言 | 体积 | 性能 | 适用场景 |
|---|---|---|---|---|
| Rapier | Rust/WASM | ~1.4 MB | 极佳 | 生产级游戏、复杂模拟 |
| Cannon-es | JavaScript | ~150 KB | 良好 | 原型、简单游戏 |
| Ammo.js | C++/WASM | ~1-2 MB | 极佳 | AAA 级特性、软体、车辆 |
| Oimo.js | JavaScript | ~100 KB | 良好 | 简单游戏、快速原型 |
2D 物理引擎
| 引擎 | 语言 | 体积 | 性能 | 适用场景 |
|---|---|---|---|---|
| Matter.js | JavaScript | ~80 KB | 良好 | 视觉化游戏、原型 |
| Planck.js | JavaScript | ~120 KB | 良好 | 平台跳跃、精确物理 |
| p2-es | JavaScript | ~100 KB | 良好 | 约束、机械结构 |
| Box2D WASM | C++/WASM | ~300 KB | 极佳 | 大量刚体场景 |
3D 物理引擎
Rapier —— 现代的选择
Rapier 是一个用 Rust 编写、有 JavaScript/WASM 绑定的物理引擎。它是 2025-2026 年网页游戏中性能最强的选项,相较 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('Box is touching:', 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('Hit at:', hitPoint)
}关节:
js
// 创建铰链关节(门)
const jointData = RAPIER.JointData.revolute(
{ x: 0, y: 0, z: 0 }, // body1 上的锚点
{ x: -1, y: 0, z: 0 }, // body2 上的锚点
{ 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('Hard impact!')
}
})约束:
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 物理力量
Ammo.js 是 Bullet 物理引擎编译到 WebAssembly 的版本。功能最完整,包含软体、车辆和高级约束。
安装:
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 复杂、冗长
- 包体大
- 需要手动管理内存(销毁对象)
- 学习曲线陡
- 文档分散
- 非跨平台确定性(在仔细配置下,仅同设备一致)
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('Collision between:', 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 直观
- 文档完备
- 社区活跃
- 固定时间步长下确定性(
isFixed: true)
缺点:
- 无 CCD(高速物体可能穿透——可以通过子步缓解)
- 大量刚体时性能受限
- 无 WASM 选项
- 复杂模拟下精度有限
Planck.js —— JavaScript 版的 Box2D
Planck.js 是 Box2D 完整的 JavaScript 重写版。物理算法久经考验,在固定时间步长下具有确定性。
安装:
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('Contact started')
})
world.on('end-contact', (contact) => {
console.log('Contact ended')
})
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 模块,可摇树优化
缺点:
- 无内置渲染器
- 某些形状组合不支持
- 不如同类活跃
- 文档有缺口
性能小贴士
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.js | 唯一具备这些功能的选项 |
| 简单 3D 浏览器游戏 | Oimo.js | 体积小,够用 |
| 2D 游戏 Jam | Matter.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 米
// 渲染时再乘以 SCALEWASM 引擎的内存泄漏
移除刚体时一定要清理:
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相关阅读
- WebGL 游戏开发基础
- 用 Web Workers 跑游戏逻辑 —— 把物理卸载到 Worker 线程
- 让网页游戏加载更快
- Canvas 2D 游戏循环 —— 所有物理引擎都用的固定时间步长模式
- 2026 网页游戏技术栈 —— 物理引擎在整体技术栈里的定位
- 网页游戏引擎对比 —— 自带物理支持的引擎
外部资源
- Rapier 文档 —— 官方 Rapier 物理文档与示例
- Cannon-es 文档 —— API 参考与指南
- Matter.js 文档 —— 完整 API 参考
- Planck.js 文档 —— JavaScript 版的 Box2D