Skip to content

Physique pour les jeux web

La simulation physique donne vie aux jeux : objets qui tombent, balles qui rebondissent, ragdolls, véhicules et environnements destructibles. Ce guide présente les principales bibliothèques de physique disponibles pour les jeux web, avec de véritables exemples de code et une évaluation honnête de leurs avantages et inconvénients.

Vue d’ensemble

MoteurDimensionPerformancesTailleDifficultéCorps souplesVéhiculesCCDDéterminisme
Rapier2D/3D⭐⭐⭐⭐⭐1,4 MoMoyenne
Cannon-es3D⭐⭐⭐150 KoFacile
Ammo.js3D⭐⭐⭐⭐⭐1-2 MoDifficile⚠️
Jolt3D⭐⭐⭐⭐⭐Difficile
Oimo.js3D⭐⭐⭐100 KoFacile
Matter.js2D⭐⭐⭐80 KoFacile⚠️
Planck.js2D⭐⭐⭐⭐120 KoMoyenne⚠️
p2-es2D⭐⭐⭐100 KoMoyenne
Box2D WASM2D⭐⭐⭐⭐⭐300 KoDifficile

Légende :

  • CCD = détection continue des collisions (empêche les objets rapides de traverser les murs)
  • ⚠️ Déterminisme = possible avec un pas de temps fixe, mais non garanti entre les différentes plateformes

Recommandations rapides

Votre situationMeilleur choix
Jeu 3D en productionRapier — meilleures performances, API moderne et développement actif
Apprentissage / prototypeCannon-es (3D) ou Matter.js (2D) — API simples et débogage facile
Physique des véhiculesRapier ou Ammo.js — tous deux disposent de contrôleurs de véhicule par lancer de rayons
Corps souples, tissus, cordesAmmo.js ou Jolt (JoltPhysics.js) — tous deux prennent en charge les tissus et les corps souples ; le portage WASM de Jolt est maintenu plus activement
Jeu de plateformes 2D précisPlanck.js — algorithmes Box2D et comportement déterministe avec un pas de temps fixe
Performances 2D maximalesBox2D WASM — vitesse native dans le navigateur
Taille de bundle minimaleOimo.js (3D) ou Matter.js (2D)

Comparaison détaillée

Moteurs physiques 3D

MoteurLangageTaillePerformancesIdéal pour
RapierRust/WASM~1,4 MoExcellentesJeux en production, simulations complexes
Cannon-esJavaScript~150 KoBonnesPrototypes, jeux simples
Ammo.jsC++/WASM~1-2 MoExcellentesFonctionnalités AAA, corps souples, véhicules
JoltC++/WASMWASMExcellentesFonctionnalités AAA avec une maintenance active
Oimo.jsJavaScript~100 KoBonnesJeux simples, prototypes rapides

Moteurs physiques 2D

MoteurLangageTaillePerformancesIdéal pour
Matter.jsJavaScript~80 KoBonnesJeux visuels, prototypes
Planck.jsJavaScript~120 KoBonnesJeux de plateformes, physique précise
p2-esJavaScript~100 KoBonnesContraintes, mécanismes
Box2D WASMC++/WASM~300 KoExcellentesGrand nombre de corps

Moteurs physiques 3D

Rapier — Le choix moderne

Rapier est un moteur physique écrit en Rust, avec des liaisons JavaScript/WASM. C’est l’option la plus performante pour les jeux web en 2025-2026, avec des gains de vitesse de 2 à 5 fois par rapport à sa version de 2024.

Installation :

bash
npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simd

Configuration de base :

js
import RAPIER from '@dimforge/rapier3d'

// Initialize (async required for WASM)
await RAPIER.init()

// Create world with gravity
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)

// Create ground (static body)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)

// Create falling box (dynamic body)
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)

Intégration à la boucle de jeu :

js
const FIXED_TIMESTEP = 1 / 60

function physicsStep() {
  world.step()
}

function gameLoop() {
  physicsStep()
  
  // Sync render objects with physics
  const position = boxBody.translation()
  const rotation = boxBody.rotation()
  
  // Update your Three.js/Babylon mesh
  mesh.position.set(position.x, position.y, position.z)
  mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)
  
  requestAnimationFrame(gameLoop)
}

Détection des collisions :

js
// Event-based collision detection
world.contactPairsWith(boxCollider, (otherCollider) => {
  console.log('Box is touching:', 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('Hit at:', hitPoint)
}

Articulations :

js
// Create a hinge joint (door)
const jointData = RAPIER.JointData.revolute(
  { x: 0, y: 0, z: 0 },  // Anchor on body1
  { x: -1, y: 0, z: 0 }, // Anchor on body2
  { x: 0, y: 1, z: 0 }   // Rotation axis
)
world.createImpulseJoint(jointData, body1, body2, true)

Contrôleur de véhicule :

js
// Create chassis rigid body
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)

// Create vehicle controller
const vehicle = world.createVehicleController(chassis)

// Add wheels (front-left, front-right, rear-left, rear-right)
const suspensionRestLength = 0.3
const wheelRadius = 0.4

// Front wheels (steering)
vehicle.addWheel(
  { x: -0.8, y: 0, z: 1.2 },   // Connection point
  { x: 0, y: -1, z: 0 },       // Suspension direction
  { x: -1, y: 0, z: 0 },       // Axle direction
  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
)

// Rear wheels (drive)
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
)

// Configure suspension for all wheels
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)
}

// In game loop
function updateVehicle(steering, engineForce, brakeForce) {
  // Steering (front wheels only)
  vehicle.setWheelSteering(0, steering)
  vehicle.setWheelSteering(1, steering)
  
  // Engine (rear wheels)
  vehicle.setWheelEngineForce(2, engineForce)
  vehicle.setWheelEngineForce(3, engineForce)
  
  // Brakes (all wheels)
  for (let i = 0; i < 4; i++) {
    vehicle.setWheelBrake(i, brakeForce)
  }
  
  // Update vehicle physics
  vehicle.updateVehicle(world.timestep)
}

Avantages :

  • Meilleures performances (WASM + SIMD)
  • Excellente précision de la détection des collisions
  • Déterminisme multiplateforme (mêmes entrées = mêmes sorties)
  • Développement actif et API moderne
  • Détection continue des collisions (aucune traversée des obstacles)
  • Contrôleurs de personnage et de véhicule intégrés

Inconvénients :

  • Bundle plus volumineux (~1,4 Mo)
  • Initialisation asynchrone obligatoire
  • API plus complexe que celles des solutions en JavaScript pur
  • Le débogage de WASM peut être délicat

Cannon-es — Simple et efficace

Cannon-es est le fork maintenu de Cannon.js. Écrit entièrement en JavaScript, il est facile à comprendre et convient parfaitement à l’apprentissage et au prototypage.

Installation :

bash
npm install cannon-es

Configuration de base :

js
import * as CANNON from 'cannon-es'

// Create world
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.81, 0)
})

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

// Falling sphere
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)

Boucle de jeu :

js
const TIMESTEP = 1 / 60

function animate() {
  world.step(TIMESTEP)
  
  // Sync with Three.js
  mesh.position.copy(sphereBody.position)
  mesh.quaternion.copy(sphereBody.quaternion)
  
  requestAnimationFrame(animate)
}

Événements de collision :

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

Contraintes :

js
// Distance constraint (rope-like)
const constraint = new CANNON.DistanceConstraint(
  bodyA, bodyB, 
  2 // distance
)
world.addConstraint(constraint)

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

Avantages :

  • JavaScript pur, sans la complexité de WASM
  • Bundle peu volumineux (~150 Ko)
  • Facile à apprendre et à déboguer
  • Fonctionne partout
  • Excellente intégration avec Three.js
  • Bonne documentation

Inconvénients :

  • Plus lent que les solutions WASM
  • Montre ses limites avec un grand nombre de corps (>100)
  • Prise en charge limitée des maillages triangulés
  • Aucune CCD intégrée (les objets peuvent traverser les obstacles)
  • Développement moins actif

Ammo.js — Toute la puissance de Bullet Physics

Ammo.js est le moteur Bullet Physics compilé en WebAssembly. Il offre un maximum de fonctionnalités, notamment les corps souples, les véhicules et les contraintes avancées.

Installation :

bash
npm install ammo.js
# Or use from CDN

Configuration de base :

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
}

Corps souples (tissu) :

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
}

Physique des véhicules :

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)
  
  // Add wheels
  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))   // Front left
  addWheel(true, new Ammo.btVector3(-1, 0, 1.5))  // Front right
  addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // Rear left
  addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// Rear right
  
  return vehicle
}

Avantages :

  • Ensemble de fonctionnalités le plus complet
  • Corps souples, tissus et cordes
  • Physique avancée des véhicules
  • Éprouvé en production (utilisé dans de nombreux jeux AAA)
  • Hautement configurable Inconvénients :
  • API complexe et verbeuse
  • Taille de bundle importante
  • Gestion de la mémoire nécessaire (destruction des objets)
  • Courbe d’apprentissage abrupte
  • Documentation dispersée
  • Pas de déterminisme multiplateforme (uniquement sur un même appareil, avec une configuration rigoureuse)

Jolt — physique 3D moderne de niveau AAA

Jolt Physics est le moteur utilisé par des jeux comme Horizon Forbidden West, et JoltPhysics.js le rend accessible dans le navigateur grâce à un portage WASM. Il constitue un excellent compromis entre Rapier et Ammo.js : davantage de fonctionnalités que Rapier (corps mous, tissus, contrôleur de véhicule à roues), avec une maintenance active qui fait défaut à Ammo.js. Le paquet npm est jolt-physics, et des intégrations prêtes à l’emploi existent pour React Three Fiber (@react-three/jolt) et Babylon.js.

bash
npm install jolt-physics

Comme Rapier, il s’agit d’un module WASM initialisé de manière asynchrone, doté d’une API verbeuse mais complète qui reproduit l’interface C++. Choisissez Jolt si vous recherchez des fonctionnalités comparables à celles de Bullet (corps mous, véhicules), tout en préférant une base de code moderne et maintenue à Ammo.js.


Oimo.js — léger et rapide

Oimo.js est un moteur physique 3D léger. Il convient parfaitement aux jeux simples qui ne nécessitent pas de fonctionnalités avancées.

Installation :

bash
npm install oimo

Configuration de base :

js
import * as OIMO from 'oimo'

const world = new OIMO.World({
  timestep: 1/60,
  iterations: 8,
  broadphase: 2, // 1: brute, 2: sweep & prune, 3: volume tree
  worldscale: 1,
  random: true,
  gravity: [0, -9.8, 0]
})

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

// Create falling sphere
const sphere = world.add({
  type: 'sphere',
  size: [1],
  pos: [0, 10, 0],
  move: true,
  density: 1,
  friction: 0.4,
  restitution: 0.2
})

Boucle de jeu :

js
function animate() {
  world.step()
  
  // Get position/rotation
  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)
}

Avantages :

  • Très léger (environ 100 Ko)
  • API simple
  • Bonnes performances pour les scènes basiques
  • Prise en charge native de Babylon.js

Inconvénients :

  • Formes limitées (primitives uniquement)
  • Aucun corps mou
  • Documentation peu fournie
  • Développement moins actif
  • Choix limité d’articulations

Moteurs physiques 2D

Matter.js — élégant et intuitif

Matter.js est un moteur physique 2D riche en fonctionnalités, doté d’excellents outils de rendu et de débogage.

Installation :

bash
npm install matter-js

Configuration de base :

js
import Matter from 'matter-js'

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

// Create engine
const engine = Engine.create()

// Create renderer (optional, great for debugging)
const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 800,
    height: 600,
    wireframes: false
  }
})

// Create bodies
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
})

// Add to world
World.add(engine.world, [ground, box, circle])

// Run
Render.run(render)
Runner.run(Runner.create(), engine)

Événements de collision :

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

Contraintes :

js
// Pin constraint
const pin = Matter.Constraint.create({
  pointA: { x: 400, y: 100 },
  bodyB: box,
  stiffness: 0.9
})

// Spring between bodies
const spring = Matter.Constraint.create({
  bodyA: box,
  bodyB: circle,
  stiffness: 0.01,
  length: 100
})

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

Interaction à la souris :

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)

Avantages :

  • Beau rendu par défaut
  • Excellent pour l’apprentissage et le prototypage
  • API intuitive
  • Bonne documentation
  • Communauté active
  • Déterministe par défaut — Matter.Runner utilise désormais un pas de temps fixe et déterministe (le pas de temps non fixe a été supprimé dans la version 0.20.0)

Inconvénients :

  • Aucun CCD (les objets rapides peuvent traverser les obstacles — utilisez des sous-pas pour atténuer ce problème)
  • Problèmes de performances avec de nombreux corps
  • Aucune option WASM
  • Précision limitée pour les simulations complexes

Planck.js — Box2D pour JavaScript

Planck.js est une réécriture complète de Box2D en JavaScript. Son moteur physique est éprouvé et déterministe lorsqu’un pas de temps fixe est utilisé.

Installation :

bash
npm install planck

Configuration de base :

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

// Create world
const world = new World({
  gravity: Vec2(0, -10)
})

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

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

Boucle de jeu à pas de temps fixe :

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

function gameLoop() {
  world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
  
  // Iterate all bodies
  for (let body = world.getBodyList(); body; body = body.getNext()) {
    const pos = body.getPosition()
    const angle = body.getAngle()
    // Update your sprites...
  }
  
  requestAnimationFrame(gameLoop)
}

Fonctions de rappel de collision :

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) => {
  // Can disable contact here
  // contact.setEnabled(false)
})

Articulations :

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

// Revolute joint (hinge)
const joint = world.createJoint(RevoluteJoint({
  bodyA: ground,
  bodyB: box,
  localAnchorA: Vec2(0, 5),
  localAnchorB: Vec2(-1, 0),
  enableMotor: true,
  maxMotorTorque: 1000,
  motorSpeed: 2
}))

// Distance joint (spring)
world.createJoint(DistanceJoint({
  bodyA: boxA,
  bodyB: boxB,
  localAnchorA: Vec2(0, 0),
  localAnchorB: Vec2(0, 0),
  length: 5,
  stiffness: 10,
  damping: 0.5
}))

Avantages :

  • Déterministe avec un pas de temps fixe
  • Bien documenté (la documentation de Box2D s’applique)
  • Bonnes performances
  • Prise en charge de TypeScript
  • Bundle de petite taille

Inconvénients :

  • Le CCD ne prend pas en charge les articulations ; les objets rapides reliés par des articulations peuvent donc s’étirer
  • Courbe d’apprentissage plus abrupte que celle de Matter.js
  • Aucun moteur de rendu intégré
  • Particularités de Box2D (l’échelle des unités est importante)

p2-es — physique 2D flexible

p2-es est le fork maintenu de p2.js. Il convient parfaitement aux jeux qui nécessitent des contraintes et des mécanismes complexes.

Installation :

bash
npm install p2-es

Configuration de base :

js
import * as p2 from 'p2-es'

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

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

// Dynamic circle
const circleBody = new p2.Body({
  mass: 1,
  position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)

Contraintes avancées :

js
// Gear constraint
const gear = new p2.GearConstraint(bodyA, bodyB, {
  ratio: 2 // bodyB rotates twice as fast
})
world.addConstraint(gear)

// Prismatic constraint (slider)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
  localAnchorA: [0, 0],
  localAnchorB: [0, 0],
  localAxisA: [1, 0],
  disableRotationalLock: false
})
world.addConstraint(prismatic)

// Lock constraint (weld)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)

Matériaux de contact :

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)

// Apply to shapes
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber

Avantages :

  • Système de contraintes riche
  • Matériaux de contact
  • Bien adapté aux mécanismes et aux machines
  • Mise en veille des corps pour améliorer les performances
  • Modules ES compatibles avec le tree shaking

Inconvénients :

  • Aucun moteur de rendu intégré
  • Certaines paires de formes ne sont pas prises en charge
  • Moins actif que les autres solutions
  • Lacunes dans la documentation

Conseils de performance

1. Utilisez un pas de temps fixe

js
const TIMESTEP = 1 / 60
let accumulator = 0

function gameLoop(deltaTime) {
  accumulator += deltaTime
  
  while (accumulator >= TIMESTEP) {
    world.step(TIMESTEP)
    accumulator -= TIMESTEP
  }
  
  // Interpolate for smooth rendering
  const alpha = accumulator / TIMESTEP
  // lerp(previousState, currentState, alpha)
}

2. Mettez en veille les corps inactifs

La plupart des moteurs prennent en charge la mise en veille. Activez-la :

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

// Rapier
bodyDesc.setCanSleep(true)

3. Utilisez des formes de collision simples

js
// GOOD: Simple primitives
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))

// AVOID: Complex trimesh for dynamic bodies
const trimesh = new CANNON.Trimesh(vertices, indices) // Slow!

4. Réduisez le nombre d’itérations du solveur (avec prudence)

js
// Cannon-es
world.solver.iterations = 5 // Default 10

// Rapier - configured at world creation

5. Exécutez la simulation physique dans 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
  // Update render objects
}

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

Quel moteur choisir selon le besoin

ScénarioRecommandationPourquoi
Jeu 3D en productionRapierMeilleures performances, API moderne, véhicules
Prototype de jeu 3DCannon-esSimple et facile à déboguer
Corps mous, tissus, cordesAmmo.js ou JoltLes deux prennent en charge les tissus et les corps mous ; le portage WASM de Jolt (JoltPhysics.js) est maintenu plus activement
Jeu 3D simple pour navigateurOimo.jsMinuscule et suffisant
Game jam 2DMatter.jsMise en place rapide, rendu intégré
Jeu de plateformes précisPlanck.jsDéterminisme avec un pas de temps fixe, bonne documentation
Mécanismes 2D complexesp2-esSystème de contraintes riche
Jeu 2D aux performances critiquesBox2D WASMOption 2D la plus rapide, véritable CCD

Intégration avec les moteurs de rendu

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() // physics body -> mesh

function createPhysicsBox(x, y, z) {
  // Physics
  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)
  
  // Render
  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) {
  // Physics
  const body = Matter.Bodies.rectangle(x, y, width, height)
  Matter.World.add(engine.world, body)
  
  // Render
  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
  })
})

Pièges courants

L’échelle est importante

Les moteurs physiques fonctionnent mieux avec une échelle réaliste (1 unité = 1 mètre). N’utilisez pas directement des coordonnées en pixels.

js
// BAD: Using pixel positions
const body = Bodies.circle(400, 300, 50) // 50 pixel radius?

// GOOD: Use a scale factor
const SCALE = 50 // 50 pixels per meter
const body = Bodies.circle(8, 6, 1) // 1 meter radius
// Then multiply by SCALE when rendering

Fuites de mémoire avec les moteurs WASM

Nettoyez toujours les corps lorsque vous les supprimez :

js
// Rapier
world.removeRigidBody(body)

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

Effet tunnel (les objets se traversent)

Les objets se déplaçant rapidement peuvent traverser les parois fines. Solutions :

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

// Cannon-es: Use smaller timesteps or thicker walls
world.step(1/120) // 120 Hz instead of 60

Articles connexes

Ressources externes