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
| Moteur | Dimension | Performances | Taille | Difficulté | Corps souples | Véhicules | CCD | Déterminisme |
|---|---|---|---|---|---|---|---|---|
| Rapier | 2D/3D | ⭐⭐⭐⭐⭐ | 1,4 Mo | Moyenne | — | ✅ | ✅ | ✅ |
| Cannon-es | 3D | ⭐⭐⭐ | 150 Ko | Facile | — | — | — | — |
| Ammo.js | 3D | ⭐⭐⭐⭐⭐ | 1-2 Mo | Difficile | ✅ | ✅ | ✅ | ⚠️ |
| Jolt | 3D | ⭐⭐⭐⭐⭐ | — | Difficile | ✅ | ✅ | ✅ | ✅ |
| Oimo.js | 3D | ⭐⭐⭐ | 100 Ko | Facile | — | — | — | — |
| Matter.js | 2D | ⭐⭐⭐ | 80 Ko | Facile | — | — | — | ⚠️ |
| Planck.js | 2D | ⭐⭐⭐⭐ | 120 Ko | Moyenne | — | — | ✅ | ⚠️ |
| p2-es | 2D | ⭐⭐⭐ | 100 Ko | Moyenne | — | — | — | — |
| Box2D WASM | 2D | ⭐⭐⭐⭐⭐ | 300 Ko | Difficile | — | — | ✅ | ✅ |
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 situation | Meilleur choix |
|---|---|
| Jeu 3D en production | Rapier — meilleures performances, API moderne et développement actif |
| Apprentissage / prototype | Cannon-es (3D) ou Matter.js (2D) — API simples et débogage facile |
| Physique des véhicules | Rapier ou Ammo.js — tous deux disposent de contrôleurs de véhicule par lancer de rayons |
| Corps souples, tissus, cordes | Ammo.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écis | Planck.js — algorithmes Box2D et comportement déterministe avec un pas de temps fixe |
| Performances 2D maximales | Box2D WASM — vitesse native dans le navigateur |
| Taille de bundle minimale | Oimo.js (3D) ou Matter.js (2D) |
Comparaison détaillée
Moteurs physiques 3D
| Moteur | Langage | Taille | Performances | Idéal pour |
|---|---|---|---|---|
| Rapier | Rust/WASM | ~1,4 Mo | Excellentes | Jeux en production, simulations complexes |
| Cannon-es | JavaScript | ~150 Ko | Bonnes | Prototypes, jeux simples |
| Ammo.js | C++/WASM | ~1-2 Mo | Excellentes | Fonctionnalités AAA, corps souples, véhicules |
| Jolt | C++/WASM | WASM | Excellentes | Fonctionnalités AAA avec une maintenance active |
| Oimo.js | JavaScript | ~100 Ko | Bonnes | Jeux simples, prototypes rapides |
Moteurs physiques 2D
| Moteur | Langage | Taille | Performances | Idéal pour |
|---|---|---|---|---|
| Matter.js | JavaScript | ~80 Ko | Bonnes | Jeux visuels, prototypes |
| Planck.js | JavaScript | ~120 Ko | Bonnes | Jeux de plateformes, physique précise |
| p2-es | JavaScript | ~100 Ko | Bonnes | Contraintes, mécanismes |
| Box2D WASM | C++/WASM | ~300 Ko | Excellentes | Grand 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 :
npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simdConfiguration de base :
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 :
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 :
// 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 :
// 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 :
// 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 :
npm install cannon-esConfiguration de base :
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 :
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 :
sphereBody.addEventListener('collide', (event) => {
const contact = event.contact
const impactVelocity = contact.getImpactVelocityAlongNormal()
if (Math.abs(impactVelocity) > 5) {
console.log('Hard impact!')
}
})Contraintes :
// 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 :
npm install ammo.js
# Or use from CDNConfiguration de base :
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) :
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 :
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.
npm install jolt-physicsComme 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 :
npm install oimoConfiguration de base :
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 :
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 :
npm install matter-jsConfiguration de base :
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 :
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
console.log('Collision between:', pair.bodyA.label, pair.bodyB.label)
})
})Contraintes :
// 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 :
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 :
npm install planckConfiguration de base :
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 :
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 :
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 :
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 :
npm install p2-esConfiguration de base :
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 :
// 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 :
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 = rubberAvantages :
- 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
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 :
// 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
// 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)
// Cannon-es
world.solver.iterations = 5 // Default 10
// Rapier - configured at world creation5. Exécutez la simulation physique dans 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
// 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énario | Recommandation | Pourquoi |
|---|---|---|
| Jeu 3D en production | Rapier | Meilleures performances, API moderne, véhicules |
| Prototype de jeu 3D | Cannon-es | Simple et facile à déboguer |
| Corps mous, tissus, cordes | Ammo.js ou Jolt | Les 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 navigateur | Oimo.js | Minuscule et suffisant |
| Game jam 2D | Matter.js | Mise en place rapide, rendu intégré |
| Jeu de plateformes précis | Planck.js | Déterminisme avec un pas de temps fixe, bonne documentation |
| Mécanismes 2D complexes | p2-es | Système de contraintes riche |
| Jeu 2D aux performances critiques | Box2D WASM | Option 2D la plus rapide, véritable CCD |
Intégration avec les moteurs de rendu
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() // 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
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.
// 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 renderingFuites de mémoire avec les moteurs WASM
Nettoyez toujours les corps lorsque vous les supprimez :
// 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 :
// 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 60Articles connexes
- Fondamentaux de WebGL pour les créateurs de jeux
- Web Workers pour la logique de jeu — déchargez les calculs physiques sur un thread Worker
- Publier un jeu web qui se charge rapidement
- Boucle de jeu avec Canvas 2D — le modèle à pas de temps fixe utilisé par tous les moteurs physiques
- Les technologies des jeux web en 2026 — comment les moteurs physiques s’intègrent à l’ensemble de la pile technologique
- Comparatif des moteurs de jeux web — moteurs intégrant la gestion de la physique
Ressources externes
- Documentation de Rapier — documentation officielle et exemples sur le moteur physique Rapier
- Documentation de Cannon-es — référence de l’API et guides
- Documentation de Matter.js — référence complète de l’API
- Documentation de Planck.js — Box2D pour JavaScript