Skip to content

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

MotorDimensiónRendimientoTamañoDificultadCuerpos blandosVehículosCCDDeterminismo
Rapier2D/3D⭐⭐⭐⭐⭐1,4 MBMedia
Cannon-es3D⭐⭐⭐150 KBFácil
Ammo.js3D⭐⭐⭐⭐⭐1-2 MBDifícil⚠️
Jolt3D⭐⭐⭐⭐⭐Difícil
Oimo.js3D⭐⭐⭐100 KBFácil
Matter.js2D⭐⭐⭐80 KBFácil⚠️
Planck.js2D⭐⭐⭐⭐120 KBMedia⚠️
p2-es2D⭐⭐⭐100 KBMedia
Box2D WASM2D⭐⭐⭐⭐⭐300 KBDifí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ónMejor opción
Juego 3D para producciónRapier — el mejor rendimiento, API moderna y desarrollo activo
Aprendizaje / prototipoCannon-es (3D) o Matter.js (2D) — API sencillas y depuración fácil
Física de vehículosRapier o Ammo.js — ambos tienen controladores de vehículos mediante ray casting
Cuerpos blandos, telas y cuerdasAmmo.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 precisoPlanck.js — algoritmos de Box2D y determinismo con un intervalo de tiempo fijo
Máximo rendimiento en 2DBox2D WASM — velocidad nativa en el navegador
Paquete más pequeñoOimo.js (3D) o Matter.js (2D)

Comparación detallada

Motores de física 3D

MotorLenguajeTamañoRendimientoIdeal para
RapierRust/WASM~1,4 MBExcelenteJuegos para producción y simulaciones complejas
Cannon-esJavaScript~150 KBBuenoPrototipos y juegos sencillos
Ammo.jsC++/WASM~1-2 MBExcelenteFunciones de nivel AAA, cuerpos blandos y vehículos
JoltC++/WASMWASMExcelenteFunciones de nivel AAA con mantenimiento activo
Oimo.jsJavaScript~100 KBBuenoJuegos sencillos y prototipos rápidos

Motores de física 2D

MotorLenguajeTamañoRendimientoIdeal para
Matter.jsJavaScript~80 KBBuenoJuegos visuales y prototipos
Planck.jsJavaScript~120 KBBuenoJuegos de plataformas y física precisa
p2-esJavaScript~100 KBBuenoRestricciones y mecanismos
Box2D WASMC++/WASM~300 KBExcelenteGrandes 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:

bash
npm install @dimforge/rapier3d
# O con SIMD (más rápido, requiere navegadores modernos):
npm install @dimforge/rapier3d-simd

Configuración básica:

js
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:

js
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:

js
// 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:

js
// 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:

js
// 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:

bash
npm install cannon-es

Configuración básica:

js
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:

js
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:

js
sphereBody.addEventListener('collide', (event) => {
  const contact = event.contact
  const impactVelocity = contact.getImpactVelocityAlongNormal()
  
  if (Math.abs(impactVelocity) > 5) {
    console.log('¡Impacto fuerte!')
  }
})

Restricciones:

js
// 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:

bash
npm install ammo.js
# O usar desde una CDN

Configuración básica:

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
}

Cuerpos blandos (tela):

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
}

Física de vehículos:

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)
  
  // 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.

bash
npm install jolt-physics

Al 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:

bash
npm install oimo

Configuración básica:

js
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:

js
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:

bash
npm install matter-js

Configuración básica:

js
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:

js
Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach(pair => {
    console.log('Colisión entre:', pair.bodyA.label, pair.bodyB.label)
  })
})

Restricciones:

js
// 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:

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)

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:

bash
npm install planck

Configuración básica:

js
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:

js
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:

js
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:

js
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:

bash
npm install p2-es

Configuración básica:

js
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:

js
// 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:

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)

// Aplicar a las formas
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber

Ventajas:

  • 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

js
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:

js
// 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

js
// 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)

js
// Cannon-es
world.solver.iterations = 5 // Valor predeterminado: 10

// Rapier: se configura al crear el mundo

5. Ejecuta la física en un 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
  // 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ónRecomendadoMotivo
Juego 3D para producciónRapierEl mejor rendimiento, API moderna y vehículos
Prototipo de juego 3DCannon-esSencillo y fácil de depurar
Cuerpos blandos, telas y cuerdasAmmo.js o JoltAmbos admiten telas y cuerpos blandos; la adaptación WASM de Jolt (JoltPhysics.js) cuenta con un mantenimiento más activo
Juego 3D sencillo para navegadorOimo.jsPequeño y suficiente
Game jam de juegos 2DMatter.jsConfiguración rápida y renderizado integrado
Juego de plataformas precisoPlanck.jsDeterminismo con paso temporal fijo y buena documentación
Mecanismos 2D complejosp2-esSistema de restricciones completo
Juego 2D donde el rendimiento es críticoBox2D WASMLa opción 2D más rápida, con CCD real

Integración con renderizadores

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() // 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

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.

js
// 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 renderizar

Fugas de memoria con motores WASM

Libera siempre los cuerpos cuando los elimines:

js
// 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:

js
// 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 60

Contenido relacionado

Recursos externos