Skip to content

Fisika gim untuk gim web

Simulasi fisika membuat gim terasa hidup—objek jatuh, bola memantul, ragdoll, kendaraan, dan lingkungan yang dapat dihancurkan. Panduan ini membahas pustaka fisika utama yang tersedia untuk gim web, lengkap dengan contoh kode nyata serta kelebihan dan kekurangan yang jujur.

Sekilas

MesinDimensiPerformaUkuranTingkat kesulitanBenda LunakKendaraanCCDDeterminisme
Rapier2D/3D⭐⭐⭐⭐⭐1.4 MBSedang
Cannon-es3D⭐⭐⭐150 KBMudah
Ammo.js3D⭐⭐⭐⭐⭐1-2 MBSulit⚠️
Jolt3D⭐⭐⭐⭐⭐Sulit
Oimo.js3D⭐⭐⭐100 KBMudah
Matter.js2D⭐⭐⭐80 KBMudah⚠️
Planck.js2D⭐⭐⭐⭐120 KBSedang⚠️
p2-es2D⭐⭐⭐100 KBSedang
Box2D WASM2D⭐⭐⭐⭐⭐300 KBSulit

Keterangan:

  • CCD = Deteksi Tabrakan Berkelanjutan (mencegah objek cepat menembus dinding)
  • ⚠️ Determinisme = Dapat dicapai dengan timestep tetap, tetapi tidak dijamin lintas platform

Rekomendasi singkat

Situasi AndaPilihan terbaik
Gim 3D produksiRapier — performa terbaik, API modern, pengembangan aktif
Belajar / purwarupaCannon-es (3D) atau Matter.js (2D) — API sederhana, mudah di-debug
Fisika kendaraanRapier atau Ammo.js — keduanya memiliki pengontrol kendaraan berbasis ray cast
Benda lunak, kain, taliAmmo.js atau Jolt (JoltPhysics.js) — keduanya mendukung kain dan benda lunak; port WASM Jolt dipelihara dengan lebih aktif
Platformer 2D presisiPlanck.js — algoritma Box2D, deterministik dengan timestep tetap
Performa 2D maksimumBox2D WASM — kecepatan native di browser
Ukuran bundel terkecilOimo.js (3D) atau Matter.js (2D)

Perbandingan mendetail

Mesin Fisika 3D

MesinBahasaUkuranPerformaTerbaik untuk
RapierRust/WASM~1.4 MBSangat baikGim produksi, simulasi kompleks
Cannon-esJavaScript~150 KBBaikPurwarupa, gim sederhana
Ammo.jsC++/WASM~1-2 MBSangat baikFitur AAA, benda lunak, kendaraan
JoltC++/WASMWASMSangat baikFitur AAA dengan pemeliharaan aktif
Oimo.jsJavaScript~100 KBBaikGim sederhana, purwarupa cepat

Mesin Fisika 2D

MesinBahasaUkuranPerformaTerbaik untuk
Matter.jsJavaScript~80 KBBaikGim visual, purwarupa
Planck.jsJavaScript~120 KBBaikPlatformer, fisika presisi
p2-esJavaScript~100 KBBaikConstraint, mekanisme
Box2D WASMC++/WASM~300 KBSangat baikJumlah benda yang besar

Mesin Fisika 3D

Rapier — Pilihan modern

Rapier adalah mesin fisika Rust dengan binding JavaScript/WASM. Ini merupakan opsi dengan performa terbaik untuk gim web pada 2025-2026, dengan peningkatan kecepatan 2-5 kali lipat dibandingkan versi 2024.

Instalasi:

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

Penyiapan dasar:

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)

Integrasi game loop:

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

Deteksi tabrakan:

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

Joint:

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)

Pengontrol kendaraan:

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

Kelebihan:

  • Performa terbaik (WASM + SIMD)
  • Akurasi deteksi tabrakan yang sangat baik
  • Determinisme lintas platform (input sama = output sama)
  • Pengembangan aktif, API modern
  • Deteksi tabrakan berkelanjutan (tanpa tunneling)
  • Pengontrol karakter dan kendaraan bawaan

Kekurangan:

  • Ukuran bundel lebih besar (~1.4 MB)
  • Memerlukan inisialisasi asinkron
  • API lebih kompleks daripada alternatif JS murni
  • Debugging WASM dapat terasa rumit

Cannon-es — Sederhana dan efektif

Cannon-es adalah fork Cannon.js yang masih dipelihara. Dibuat dengan JavaScript murni, mudah dipahami, dan sangat cocok untuk belajar serta membuat purwarupa.

Instalasi:

bash
npm install cannon-es

Penyiapan dasar:

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)

Game loop:

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

Event tabrakan:

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

Constraint:

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)

Kelebihan:

  • JavaScript murni, tanpa kompleksitas WASM
  • Ukuran bundel kecil (~150 KB)
  • Mudah dipelajari dan di-debug
  • Berfungsi di mana saja
  • Integrasi Three.js yang sangat baik
  • Dokumentasi yang baik

Kekurangan:

  • Lebih lambat daripada alternatif WASM
  • Kesulitan menangani banyak benda (>100)
  • Dukungan trimesh terbatas
  • Tidak ada CCD bawaan (tunneling dapat terjadi)
  • Pengembangan kurang aktif

Ammo.js — Kekuatan penuh Bullet Physics

Ammo.js adalah mesin Bullet Physics yang dikompilasi ke WebAssembly. Menawarkan fitur paling lengkap, termasuk benda lunak, kendaraan, dan constraint tingkat lanjut.

Instalasi:

bash
npm install ammo.js
# Or use from CDN

Penyiapan dasar:

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
}

Benda lunak (kain):

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
}

Fisika kendaraan:

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
}

Kelebihan:

  • Kumpulan fitur paling lengkap
  • Benda lunak, kain, tali
  • Fisika kendaraan tingkat lanjut
  • Teruji di berbagai gim (digunakan dalam banyak gim AAA)
  • Sangat dapat dikonfigurasi Kekurangan:
  • API kompleks dan bertele-tele
  • Ukuran bundel besar
  • Memerlukan pengelolaan memori (menghancurkan objek)
  • Kurva belajar terjal
  • Dokumentasi tersebar
  • Tidak deterministik lintas platform (hanya pada perangkat yang sama, dengan konfigurasi cermat)

Jolt — fisika 3D modern kelas AAA

Jolt Physics adalah engine di balik game seperti Horizon Forbidden West, dan JoltPhysics.js menghadirkannya ke browser sebagai port WASM. Jolt menjadi jalan tengah yang kuat antara Rapier dan Ammo.js: fiturnya lebih lengkap daripada Rapier (benda lunak, kain, pengontrol kendaraan beroda), dengan pemeliharaan aktif yang tidak dimiliki Ammo.js. Paket npm-nya adalah jolt-physics, dan tersedia integrasi siap pakai untuk React Three Fiber (@react-three/jolt) dan Babylon.js.

bash
npm install jolt-physics

Seperti Rapier, Jolt menggunakan WASM yang diinisialisasi secara asinkron dengan API panjang tetapi lengkap yang mencerminkan antarmuka C++. Gunakan Jolt ketika Anda menginginkan fitur sekelas Bullet (benda lunak, kendaraan), tetapi lebih memilih basis kode modern yang terpelihara daripada Ammo.js.


Oimo.js — Ringan dan cepat

Oimo.js adalah engine fisika 3D yang ringan. Cocok untuk game sederhana yang tidak memerlukan fitur canggih.

Instalasi:

bash
npm install oimo

Penyiapan dasar:

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

Loop game:

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

Kelebihan:

  • Sangat kecil (~100 KB)
  • API sederhana
  • Performa bagus untuk adegan dasar
  • Dukungan bawaan untuk Babylon.js

Kekurangan:

  • Bentuk terbatas (hanya bentuk primitif)
  • Tidak ada benda lunak
  • Dokumentasi minim
  • Pengembangan kurang aktif
  • Pilihan sambungan terbatas

Engine Fisika 2D

Matter.js — Indah dan intuitif

Matter.js adalah engine fisika 2D kaya fitur dengan alat rendering dan debug yang sangat baik.

Instalasi:

bash
npm install matter-js

Penyiapan dasar:

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)

Peristiwa tabrakan:

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

Constraint:

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])

Interaksi mouse:

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)

Kelebihan:

  • Rendering bawaan yang indah
  • Sangat cocok untuk belajar dan membuat prototipe
  • API intuitif
  • Dokumentasi bagus
  • Komunitas aktif
  • Deterministik secara bawaan — Matter.Runner kini menggunakan timestep tetap yang deterministik (timestep tidak tetap dihapus pada v0.20.0)

Kekurangan:

  • Tidak ada CCD (objek cepat dapat menembus objek lain — gunakan substepping untuk menguranginya)
  • Masalah performa saat terdapat banyak benda
  • Tidak ada opsi WASM
  • Presisi terbatas untuk simulasi kompleks

Planck.js — Box2D untuk JavaScript

Planck.js adalah penulisan ulang lengkap Box2D dalam JavaScript. Sistem fisikanya telah teruji dan deterministik saat menggunakan timestep tetap.

Instalasi:

bash
npm install planck

Penyiapan dasar:

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

Loop game dengan timestep tetap:

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

Callback tabrakan:

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

Sambungan:

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

Kelebihan:

  • Deterministik dengan timestep tetap
  • Terdokumentasi dengan baik (dokumentasi Box2D juga berlaku)
  • Performa bagus
  • Dukungan TypeScript
  • Ukuran bundel kecil

Kekurangan:

  • CCD tidak menangani sambungan, sehingga objek cepat yang terhubung dengan sambungan dapat meregang
  • Kurva belajar lebih terjal daripada Matter.js
  • Tidak ada renderer bawaan
  • Keunikan Box2D (skala satuan sangat berpengaruh)

p2-es — Fisika 2D yang fleksibel

p2-es adalah fork p2.js yang masih dipelihara. Cocok untuk game yang memerlukan constraint dan mekanisme kompleks.

Instalasi:

bash
npm install p2-es

Penyiapan dasar:

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)

Constraint tingkat lanjut:

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)

Material kontak:

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

Kelebihan:

  • Sistem constraint yang kaya
  • Material kontak
  • Cocok untuk mekanisme dan mesin
  • Benda dapat dinonaktifkan saat diam (meningkatkan performa)
  • Modul ES dan mendukung tree-shaking

Kekurangan:

  • Tidak ada renderer bawaan
  • Beberapa pasangan bentuk tidak didukung
  • Kurang aktif dibandingkan alternatifnya
  • Dokumentasi belum lengkap

Kiat performa

1. Gunakan timestep tetap

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. Nonaktifkan benda yang tidak bergerak

Sebagian besar engine mendukung mode tidur. Aktifkan fitur ini:

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

// Rapier
bodyDesc.setCanSleep(true)

3. Gunakan bentuk tabrakan sederhana

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. Kurangi iterasi solver (dengan hati-hati)

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

// Rapier - configured at world creation

5. Jalankan fisika di 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())
    })
  }
}

Kapan harus menggunakan apa

SkenarioRekomendasiAlasan
Game 3D produksiRapierPerforma terbaik, API modern, kendaraan
Prototipe game 3DCannon-esSederhana, mudah di-debug
Benda lunak, kain, taliAmmo.js atau JoltKeduanya mendukung kain/benda lunak; port WASM Jolt (JoltPhysics.js) dipelihara lebih aktif
Game browser 3D sederhanaOimo.jsSangat kecil dan memadai
Game jam 2DMatter.jsPenyiapan cepat, rendering bawaan
Platformer presisiPlanck.jsDeterminisme timestep tetap, terdokumentasi dengan baik
Mekanisme 2D kompleksp2-esSistem constraint yang kaya
Game 2D yang mengutamakan performaBox2D WASMOpsi 2D tercepat, CCD sesungguhnya

Integrasi dengan renderer

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

Kesalahan umum

Skala sangat berpengaruh

Engine fisika bekerja paling baik dengan skala dunia nyata (1 satuan = 1 meter). Jangan gunakan koordinat piksel secara langsung.

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

Kebocoran memori pada engine WASM

Selalu bersihkan benda saat menghapusnya:

js
// Rapier
world.removeRigidBody(body)

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

Tunneling (objek saling menembus)

Objek yang bergerak cepat dapat menembus dinding tipis. Solusinya:

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

Terkait

Sumber Daya Eksternal