Física para juegos web
La simulación física da vida a los juegos: objetos que caen, pelotas que rebotan, muñecos de trapo, vehículos y entornos destructibles. Esta guía abarca las principales bibliotecas de física disponibles para juegos web, con ejemplos de código reales y ventajas y desventajas honestas.
Resumen
| Motor | Dimensión | Rendimiento | Tamaño | Dificultad | Cuerpos blandos | Vehículos | CCD | Determinismo |
|---|---|---|---|---|---|---|---|---|
| Rapier | 2D/3D | ⭐⭐⭐⭐⭐ | 1,4 MB | Media | — | ✅ | ✅ | ✅ |
| Cannon-es | 3D | ⭐⭐⭐ | 150 KB | Fácil | — | — | — | — |
| Ammo.js | 3D | ⭐⭐⭐⭐⭐ | 1-2 MB | Difícil | ✅ | ✅ | ✅ | ⚠️ |
| Jolt | 3D | ⭐⭐⭐⭐⭐ | — | Difícil | ✅ | ✅ | ✅ | ✅ |
| Oimo.js | 3D | ⭐⭐⭐ | 100 KB | Fácil | — | — | — | — |
| Matter.js | 2D | ⭐⭐⭐ | 80 KB | Fácil | — | — | — | ⚠️ |
| Planck.js | 2D | ⭐⭐⭐⭐ | 120 KB | Media | — | — | ✅ | ⚠️ |
| p2-es | 2D | ⭐⭐⭐ | 100 KB | Media | — | — | — | — |
| Box2D WASM | 2D | ⭐⭐⭐⭐⭐ | 300 KB | Difícil | — | — | ✅ | ✅ |
Leyenda:
- CCD = Detección continua de colisiones (evita que los objetos rápidos atraviesen las paredes)
- ⚠️ Determinismo = Es posible con un intervalo de tiempo fijo, pero no está garantizado entre distintas plataformas
Recomendaciones rápidas
| Tu situación | Mejor opción |
|---|---|
| Juego 3D para producción | Rapier — el mejor rendimiento, API moderna y desarrollo activo |
| Aprendizaje / prototipo | Cannon-es (3D) o Matter.js (2D) — API sencillas y depuración fácil |
| Física de vehículos | Rapier o Ammo.js — ambos tienen controladores de vehículos mediante ray casting |
| Cuerpos blandos, telas y cuerdas | Ammo.js o Jolt (JoltPhysics.js) — ambos admiten telas y cuerpos blandos; el port WASM de Jolt tiene un mantenimiento más activo |
| Juego de plataformas 2D preciso | Planck.js — algoritmos de Box2D y determinismo con un intervalo de tiempo fijo |
| Máximo rendimiento en 2D | Box2D WASM — velocidad nativa en el navegador |
| Paquete más pequeño | Oimo.js (3D) o Matter.js (2D) |
Comparación detallada
Motores de física 3D
| Motor | Lenguaje | Tamaño | Rendimiento | Ideal para |
|---|---|---|---|---|
| Rapier | Rust/WASM | ~1,4 MB | Excelente | Juegos para producción y simulaciones complejas |
| Cannon-es | JavaScript | ~150 KB | Bueno | Prototipos y juegos sencillos |
| Ammo.js | C++/WASM | ~1-2 MB | Excelente | Funciones de nivel AAA, cuerpos blandos y vehículos |
| Jolt | C++/WASM | WASM | Excelente | Funciones de nivel AAA con mantenimiento activo |
| Oimo.js | JavaScript | ~100 KB | Bueno | Juegos sencillos y prototipos rápidos |
Motores de física 2D
| Motor | Lenguaje | Tamaño | Rendimiento | Ideal para |
|---|---|---|---|---|
| Matter.js | JavaScript | ~80 KB | Bueno | Juegos visuales y prototipos |
| Planck.js | JavaScript | ~120 KB | Bueno | Juegos de plataformas y física precisa |
| p2-es | JavaScript | ~100 KB | Bueno | Restricciones y mecanismos |
| Box2D WASM | C++/WASM | ~300 KB | Excelente | Grandes cantidades de cuerpos |
Motores de física 3D
Rapier — La opción moderna
Rapier es un motor de física escrito en Rust con enlaces para JavaScript/WASM. Es la opción de mayor rendimiento para juegos web en 2025-2026, con mejoras de velocidad de entre 2 y 5 veces respecto a su versión de 2024.
Instalación:
npm install @dimforge/rapier3d
# O con SIMD (más rápido, requiere navegadores modernos):
npm install @dimforge/rapier3d-simdConfiguración básica:
import RAPIER from '@dimforge/rapier3d'
// Inicialización (WASM requiere que sea asíncrona)
await RAPIER.init()
// Crear un mundo con gravedad
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)
// Crear el suelo (cuerpo estático)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)
// Crear una caja que cae (cuerpo dinámico)
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)Integración con el bucle del juego:
const FIXED_TIMESTEP = 1 / 60
function physicsStep() {
world.step()
}
function gameLoop() {
physicsStep()
// Sincronizar los objetos renderizados con la física
const position = boxBody.translation()
const rotation = boxBody.rotation()
// Actualizar tu malla de 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)
}Detección de colisiones:
// Detección de colisiones basada en eventos
world.contactPairsWith(boxCollider, (otherCollider) => {
console.log('La caja está tocando:', otherCollider)
})
// Ray casting
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('Impacto en:', hitPoint)
}Articulaciones:
// Crear una articulación de bisagra (puerta)
const jointData = RAPIER.JointData.revolute(
{ x: 0, y: 0, z: 0 }, // Anclaje en body1
{ x: -1, y: 0, z: 0 }, // Anclaje en body2
{ x: 0, y: 1, z: 0 } // Eje de rotación
)
world.createImpulseJoint(jointData, body1, body2, true)Controlador de vehículos:
// Crear el cuerpo rígido del chasis
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)
// Crear el controlador del vehículo
const vehicle = world.createVehicleController(chassis)
// Añadir ruedas (delantera izquierda, delantera derecha, trasera izquierda, trasera derecha)
const suspensionRestLength = 0.3
const wheelRadius = 0.4
// Ruedas delanteras (dirección)
vehicle.addWheel(
{ x: -0.8, y: 0, z: 1.2 }, // Punto de conexión
{ x: 0, y: -1, z: 0 }, // Dirección de la suspensión
{ x: -1, y: 0, z: 0 }, // Dirección del eje
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
)
// Ruedas traseras (tracción)
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
)
// Configurar la suspensión de todas las ruedas
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)
}
// En el bucle del juego
function updateVehicle(steering, engineForce, brakeForce) {
// Dirección (solo ruedas delanteras)
vehicle.setWheelSteering(0, steering)
vehicle.setWheelSteering(1, steering)
// Motor (ruedas traseras)
vehicle.setWheelEngineForce(2, engineForce)
vehicle.setWheelEngineForce(3, engineForce)
// Frenos (todas las ruedas)
for (let i = 0; i < 4; i++) {
vehicle.setWheelBrake(i, brakeForce)
}
// Actualizar la física del vehículo
vehicle.updateVehicle(world.timestep)
}Ventajas:
- El mejor rendimiento (WASM + SIMD)
- Excelente precisión en la detección de colisiones
- Determinismo multiplataforma (mismas entradas = mismas salidas)
- Desarrollo activo y API moderna
- Detección continua de colisiones (sin atravesar objetos)
- Controladores de personajes y vehículos integrados
Desventajas:
- Paquete más grande (~1,4 MB)
- Requiere inicialización asíncrona
- API más compleja que las alternativas de JS puro
- La depuración de WASM puede ser complicada
Cannon-es — Sencillo y eficaz
Cannon-es es el fork con mantenimiento activo de Cannon.js. Está escrito en JavaScript puro, es fácil de entender y resulta excelente para aprender y crear prototipos.
Instalación:
npm install cannon-esConfiguración básica:
import * as CANNON from 'cannon-es'
// Crear el mundo
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.81, 0)
})
// Suelo
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane()
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)
// Esfera que cae
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)Bucle del juego:
const TIMESTEP = 1 / 60
function animate() {
world.step(TIMESTEP)
// Sincronizar con Three.js
mesh.position.copy(sphereBody.position)
mesh.quaternion.copy(sphereBody.quaternion)
requestAnimationFrame(animate)
}Eventos de colisión:
sphereBody.addEventListener('collide', (event) => {
const contact = event.contact
const impactVelocity = contact.getImpactVelocityAlongNormal()
if (Math.abs(impactVelocity) > 5) {
console.log('¡Impacto fuerte!')
}
})Restricciones:
// Restricción de distancia (similar a una cuerda)
const constraint = new CANNON.DistanceConstraint(
bodyA, bodyB,
2 // distancia
)
world.addConstraint(constraint)
// Restricción de bisagra
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)Ventajas:
- JavaScript puro, sin la complejidad de WASM
- Paquete pequeño (~150 KB)
- Fácil de aprender y depurar
- Funciona en todas partes
- Excelente integración con Three.js
- Buena documentación
Desventajas:
- Más lento que las alternativas basadas en WASM
- Tiene dificultades con muchos cuerpos (>100)
- Compatibilidad limitada con mallas trianguladas
- Sin CCD integrado (los objetos pueden atravesarse)
- Desarrollo menos activo
Ammo.js — Toda la potencia de Bullet Physics
Ammo.js es el motor Bullet Physics compilado a WebAssembly. Ofrece el máximo número de funciones, incluidos cuerpos blandos, vehículos y restricciones avanzadas.
Instalación:
npm install ammo.js
# O usar desde una CDNConfiguración básica:
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
}Cuerpos blandos (tela):
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
}Física de vehículos:
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)
// Añadir ruedas
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)) // Delantera izquierda
addWheel(true, new Ammo.btVector3(-1, 0, 1.5)) // Delantera derecha
addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // Trasera izquierda
addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// Trasera derecha
return vehicle
}Ventajas:
- El conjunto de funciones más completo
- Cuerpos blandos, telas y cuerdas
- Física avanzada de vehículos
- Probado exhaustivamente (se utiliza en muchos juegos AAA)
- Altamente configurable Desventajas:
- API compleja y verbosa
- Tamaño de paquete grande
- Requiere gestión de memoria (destruir objetos)
- Curva de aprendizaje pronunciada
- Documentación dispersa
- No ofrece determinismo multiplataforma (solo en el mismo dispositivo y con una configuración cuidadosa)
Jolt — física 3D moderna con calidad AAA
Jolt Physics es el motor que impulsa juegos como Horizon Forbidden West, y JoltPhysics.js lo lleva al navegador mediante una adaptación a WASM. Es una excelente opción intermedia entre Rapier y Ammo.js: ofrece más funciones que Rapier (cuerpos blandos, telas y un controlador para vehículos con ruedas) y cuenta con el mantenimiento activo del que carece Ammo.js. El paquete npm es jolt-physics, y existen integraciones listas para usar con React Three Fiber (@react-three/jolt) y Babylon.js.
npm install jolt-physicsAl igual que Rapier, usa WASM con inicialización asíncrona y una API detallada pero completa que reproduce la interfaz de C++. Elige Jolt cuando necesites funciones comparables a las de Bullet (cuerpos blandos y vehículos), pero prefieras una base de código moderna y mantenida en lugar de Ammo.js.
Oimo.js — ligero y rápido
Oimo.js es un motor de física 3D ligero. Es ideal para juegos sencillos que no necesitan funciones avanzadas.
Instalación:
npm install oimoConfiguración básica:
import * as OIMO from 'oimo'
const world = new OIMO.World({
timestep: 1/60,
iterations: 8,
broadphase: 2, // 1: fuerza bruta, 2: barrido y poda, 3: árbol de volúmenes
worldscale: 1,
random: true,
gravity: [0, -9.8, 0]
})
// Crear el suelo
world.add({
type: 'box',
size: [100, 1, 100],
pos: [0, -0.5, 0],
move: false
})
// Crear una esfera que cae
const sphere = world.add({
type: 'sphere',
size: [1],
pos: [0, 10, 0],
move: true,
density: 1,
friction: 0.4,
restitution: 0.2
})Bucle del juego:
function animate() {
world.step()
// Obtener la posición/rotación
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)
}Ventajas:
- Muy pequeño (~100 KB)
- API sencilla
- Buen rendimiento en escenas básicas
- Compatibilidad integrada con Babylon.js
Desventajas:
- Formas limitadas (solo primitivas)
- Sin cuerpos blandos
- Documentación escasa
- Desarrollo menos activo
- Opciones de articulaciones limitadas
Motores de física 2D
Matter.js — atractivo e intuitivo
Matter.js es un motor de física 2D rico en funciones, con excelentes herramientas de renderizado y depuración.
Instalación:
npm install matter-jsConfiguración básica:
import Matter from 'matter-js'
const { Engine, Render, World, Bodies, Runner } = Matter
// Crear el motor
const engine = Engine.create()
// Crear el renderizador (opcional, excelente para depurar)
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
})
// Crear cuerpos
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
})
// Añadir al mundo
World.add(engine.world, [ground, box, circle])
// Ejecutar
Render.run(render)
Runner.run(Runner.create(), engine)Eventos de colisión:
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
console.log('Colisión entre:', pair.bodyA.label, pair.bodyB.label)
})
})Restricciones:
// Restricción de anclaje
const pin = Matter.Constraint.create({
pointA: { x: 400, y: 100 },
bodyB: box,
stiffness: 0.9
})
// Resorte entre cuerpos
const spring = Matter.Constraint.create({
bodyA: box,
bodyB: circle,
stiffness: 0.01,
length: 100
})
World.add(engine.world, [pin, spring])Interacción con el ratón:
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)Ventajas:
- Renderizado predeterminado atractivo
- Excelente para aprender y crear prototipos
- API intuitiva
- Buena documentación
- Comunidad activa
- Determinista de forma predeterminada: Matter.Runner ahora usa un paso temporal fijo y determinista (el paso temporal no fijo se eliminó en la versión 0.20.0)
Desventajas:
- Sin CCD (los objetos rápidos pueden atravesar otros; usa subpasos para mitigarlo)
- Problemas de rendimiento con muchos cuerpos
- Sin opción WASM
- Precisión limitada en simulaciones complejas
Planck.js — Box2D para JavaScript
Planck.js es una reimplementación completa de Box2D en JavaScript. Ofrece física probada en numerosos juegos y es determinista cuando se usa un paso temporal fijo.
Instalación:
npm install planckConfiguración básica:
import { World, Vec2, Box, Circle, Edge } from 'planck'
// Crear el mundo
const world = new World({
gravity: Vec2(0, -10)
})
// Crear el suelo
const ground = world.createBody()
ground.createFixture({
shape: Edge(Vec2(-40, 0), Vec2(40, 0))
})
// Crear una caja dinámica
const box = world.createBody({
type: 'dynamic',
position: Vec2(0, 10)
})
box.createFixture({
shape: Box(1, 1),
density: 1,
friction: 0.3,
restitution: 0.5
})Bucle del juego con paso temporal fijo:
const TIMESTEP = 1 / 60
const VELOCITY_ITERATIONS = 8
const POSITION_ITERATIONS = 3
function gameLoop() {
world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
// Recorrer todos los cuerpos
for (let body = world.getBodyList(); body; body = body.getNext()) {
const pos = body.getPosition()
const angle = body.getAngle()
// Actualizar tus sprites...
}
requestAnimationFrame(gameLoop)
}Callbacks de colisión:
world.on('begin-contact', (contact) => {
const fixtureA = contact.getFixtureA()
const fixtureB = contact.getFixtureB()
console.log('Contacto iniciado')
})
world.on('end-contact', (contact) => {
console.log('Contacto finalizado')
})
world.on('pre-solve', (contact, oldManifold) => {
// Aquí puedes desactivar el contacto
// contact.setEnabled(false)
})Articulaciones:
import { RevoluteJoint, DistanceJoint, PrismaticJoint } from 'planck'
// Articulación revoluta (bisagra)
const joint = world.createJoint(RevoluteJoint({
bodyA: ground,
bodyB: box,
localAnchorA: Vec2(0, 5),
localAnchorB: Vec2(-1, 0),
enableMotor: true,
maxMotorTorque: 1000,
motorSpeed: 2
}))
// Articulación de distancia (resorte)
world.createJoint(DistanceJoint({
bodyA: boxA,
bodyB: boxB,
localAnchorA: Vec2(0, 0),
localAnchorB: Vec2(0, 0),
length: 5,
stiffness: 10,
damping: 0.5
}))Ventajas:
- Determinista con un paso temporal fijo
- Bien documentado (se aplica la documentación de Box2D)
- Buen rendimiento
- Compatibilidad con TypeScript
- Tamaño de paquete pequeño
Desventajas:
- El CCD no funciona con articulaciones, por lo que los objetos rápidos conectados mediante articulaciones pueden estirarse
- Curva de aprendizaje más pronunciada que Matter.js
- Sin renderizador integrado
- Particularidades de Box2D (la escala de unidades es importante)
p2-es — física 2D flexible
p2-es es el fork mantenido de p2.js. Es ideal para juegos que necesitan restricciones y mecanismos complejos.
Instalación:
npm install p2-esConfiguración básica:
import * as p2 from 'p2-es'
const world = new p2.World({
gravity: [0, -9.81]
})
// Suelo
const groundBody = new p2.Body({
mass: 0, // estático
position: [0, -1]
})
groundBody.addShape(new p2.Plane())
world.addBody(groundBody)
// Círculo dinámico
const circleBody = new p2.Body({
mass: 1,
position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)Restricciones avanzadas:
// Restricción de engranaje
const gear = new p2.GearConstraint(bodyA, bodyB, {
ratio: 2 // bodyB gira al doble de velocidad
})
world.addConstraint(gear)
// Restricción prismática (deslizador)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
localAnchorA: [0, 0],
localAnchorB: [0, 0],
localAxisA: [1, 0],
disableRotationalLock: false
})
world.addConstraint(prismatic)
// Restricción de bloqueo (soldadura)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)Materiales de contacto:
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)
// Aplicar a las formas
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubberVentajas:
- Sistema de restricciones completo
- Materiales de contacto
- Adecuado para mecanismos y máquinas
- Cuerpos en reposo (mejor rendimiento)
- Módulos ES compatibles con tree shaking
Desventajas:
- Sin renderizador integrado
- Algunas combinaciones de formas no son compatibles
- Menos activo que otras alternativas
- Lagunas en la documentación
Consejos de rendimiento
1. Usa un paso temporal fijo
const TIMESTEP = 1 / 60
let accumulator = 0
function gameLoop(deltaTime) {
accumulator += deltaTime
while (accumulator >= TIMESTEP) {
world.step(TIMESTEP)
accumulator -= TIMESTEP
}
// Interpolar para obtener un renderizado fluido
const alpha = accumulator / TIMESTEP
// lerp(previousState, currentState, alpha)
}2. Pon en reposo los cuerpos inactivos
La mayoría de los motores admiten cuerpos en reposo. Activa esta función:
// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1
// Rapier
bodyDesc.setCanSleep(true)3. Usa formas de colisión sencillas
// BIEN: primitivas sencillas
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))
// EVITA: mallas triangulares complejas para cuerpos dinámicos
const trimesh = new CANNON.Trimesh(vertices, indices) // ¡Lento!4. Reduce las iteraciones del solucionador (con cuidado)
// Cannon-es
world.solver.iterations = 5 // Valor predeterminado: 10
// Rapier: se configura al crear el mundo5. Ejecuta la física en un Web Worker
// main.js
const worker = new Worker('physics-worker.js')
worker.postMessage({ type: 'step', deltaTime: 1/60 })
worker.onmessage = (e) => {
const { positions, rotations } = e.data
// Actualizar los objetos de renderizado
}
// 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())
})
}
}Cuándo usar cada motor
| Situación | Recomendado | Motivo |
|---|---|---|
| Juego 3D para producción | Rapier | El mejor rendimiento, API moderna y vehículos |
| Prototipo de juego 3D | Cannon-es | Sencillo y fácil de depurar |
| Cuerpos blandos, telas y cuerdas | Ammo.js o Jolt | Ambos admiten telas y cuerpos blandos; la adaptación WASM de Jolt (JoltPhysics.js) cuenta con un mantenimiento más activo |
| Juego 3D sencillo para navegador | Oimo.js | Pequeño y suficiente |
| Game jam de juegos 2D | Matter.js | Configuración rápida y renderizado integrado |
| Juego de plataformas preciso | Planck.js | Determinismo con paso temporal fijo y buena documentación |
| Mecanismos 2D complejos | p2-es | Sistema de restricciones completo |
| Juego 2D donde el rendimiento es crítico | Box2D WASM | La opción 2D más rápida, con CCD real |
Integración con renderizadores
Three.js + Rapier
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() // cuerpo físico -> malla
function createPhysicsBox(x, y, z) {
// Física
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)
// Renderizado
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
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) {
// Física
const body = Matter.Bodies.rectangle(x, y, width, height)
Matter.World.add(engine.world, body)
// Renderizado
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
})
})Errores frecuentes
La escala es importante
Los motores de física funcionan mejor con una escala del mundo real (1 unidad = 1 metro). No uses coordenadas de píxeles directamente.
// MAL: usar posiciones en píxeles
const body = Bodies.circle(400, 300, 50) // ¿Radio de 50 píxeles?
// BIEN: usar un factor de escala
const SCALE = 50 // 50 píxeles por metro
const body = Bodies.circle(8, 6, 1) // Radio de 1 metro
// Después, multiplicar por SCALE al renderizarFugas de memoria con motores WASM
Libera siempre los cuerpos cuando los elimines:
// Rapier
world.removeRigidBody(body)
// Ammo.js
physicsWorld.removeRigidBody(body)
Ammo.destroy(body)
Ammo.destroy(shape)
Ammo.destroy(motionState)Efecto túnel (objetos que se atraviesan)
Los objetos que se mueven rápidamente pueden atravesar paredes delgadas. Soluciones:
// Rapier: activar CCD
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)
// Cannon-es: usar pasos temporales más pequeños o paredes más gruesas
world.step(1/120) // 120 Hz en lugar de 60Contenido relacionado
- Fundamentos de WebGL para desarrolladores de juegos
- Web Workers para la lógica del juego — descarga los cálculos físicos en un hilo Worker
- Publica un juego web que cargue rápido
- Bucle de juego con Canvas 2D — el patrón de paso de tiempo fijo utilizado por todos los motores de física
- Stack tecnológico para juegos web en 2026 — cómo encajan los motores de física en el stack general
- Comparativa de motores para juegos web — motores con soporte de física integrado
Recursos externos
- Documentación de Rapier — documentación oficial y ejemplos de física con Rapier
- Documentación de Cannon-es — referencia de la API y guías
- Documentación de Matter.js — referencia completa de la API
- Documentación de Planck.js — Box2D para JavaScript