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
| Mesin | Dimensi | Performa | Ukuran | Tingkat kesulitan | Benda Lunak | Kendaraan | CCD | Determinisme |
|---|---|---|---|---|---|---|---|---|
| Rapier | 2D/3D | ⭐⭐⭐⭐⭐ | 1.4 MB | Sedang | — | ✅ | ✅ | ✅ |
| Cannon-es | 3D | ⭐⭐⭐ | 150 KB | Mudah | — | — | — | — |
| Ammo.js | 3D | ⭐⭐⭐⭐⭐ | 1-2 MB | Sulit | ✅ | ✅ | ✅ | ⚠️ |
| Jolt | 3D | ⭐⭐⭐⭐⭐ | — | Sulit | ✅ | ✅ | ✅ | ✅ |
| Oimo.js | 3D | ⭐⭐⭐ | 100 KB | Mudah | — | — | — | — |
| Matter.js | 2D | ⭐⭐⭐ | 80 KB | Mudah | — | — | — | ⚠️ |
| Planck.js | 2D | ⭐⭐⭐⭐ | 120 KB | Sedang | — | — | ✅ | ⚠️ |
| p2-es | 2D | ⭐⭐⭐ | 100 KB | Sedang | — | — | — | — |
| Box2D WASM | 2D | ⭐⭐⭐⭐⭐ | 300 KB | Sulit | — | — | ✅ | ✅ |
Keterangan:
- CCD = Deteksi Tabrakan Berkelanjutan (mencegah objek cepat menembus dinding)
- ⚠️ Determinisme = Dapat dicapai dengan timestep tetap, tetapi tidak dijamin lintas platform
Rekomendasi singkat
| Situasi Anda | Pilihan terbaik |
|---|---|
| Gim 3D produksi | Rapier — performa terbaik, API modern, pengembangan aktif |
| Belajar / purwarupa | Cannon-es (3D) atau Matter.js (2D) — API sederhana, mudah di-debug |
| Fisika kendaraan | Rapier atau Ammo.js — keduanya memiliki pengontrol kendaraan berbasis ray cast |
| Benda lunak, kain, tali | Ammo.js atau Jolt (JoltPhysics.js) — keduanya mendukung kain dan benda lunak; port WASM Jolt dipelihara dengan lebih aktif |
| Platformer 2D presisi | Planck.js — algoritma Box2D, deterministik dengan timestep tetap |
| Performa 2D maksimum | Box2D WASM — kecepatan native di browser |
| Ukuran bundel terkecil | Oimo.js (3D) atau Matter.js (2D) |
Perbandingan mendetail
Mesin Fisika 3D
| Mesin | Bahasa | Ukuran | Performa | Terbaik untuk |
|---|---|---|---|---|
| Rapier | Rust/WASM | ~1.4 MB | Sangat baik | Gim produksi, simulasi kompleks |
| Cannon-es | JavaScript | ~150 KB | Baik | Purwarupa, gim sederhana |
| Ammo.js | C++/WASM | ~1-2 MB | Sangat baik | Fitur AAA, benda lunak, kendaraan |
| Jolt | C++/WASM | WASM | Sangat baik | Fitur AAA dengan pemeliharaan aktif |
| Oimo.js | JavaScript | ~100 KB | Baik | Gim sederhana, purwarupa cepat |
Mesin Fisika 2D
| Mesin | Bahasa | Ukuran | Performa | Terbaik untuk |
|---|---|---|---|---|
| Matter.js | JavaScript | ~80 KB | Baik | Gim visual, purwarupa |
| Planck.js | JavaScript | ~120 KB | Baik | Platformer, fisika presisi |
| p2-es | JavaScript | ~100 KB | Baik | Constraint, mekanisme |
| Box2D WASM | C++/WASM | ~300 KB | Sangat baik | Jumlah 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:
npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simdPenyiapan dasar:
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:
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:
// 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:
// 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:
// 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:
npm install cannon-esPenyiapan dasar:
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:
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:
sphereBody.addEventListener('collide', (event) => {
const contact = event.contact
const impactVelocity = contact.getImpactVelocityAlongNormal()
if (Math.abs(impactVelocity) > 5) {
console.log('Hard impact!')
}
})Constraint:
// 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:
npm install ammo.js
# Or use from CDNPenyiapan dasar:
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):
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:
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.
npm install jolt-physicsSeperti 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:
npm install oimoPenyiapan dasar:
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:
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:
npm install matter-jsPenyiapan dasar:
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:
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
console.log('Collision between:', pair.bodyA.label, pair.bodyB.label)
})
})Constraint:
// 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:
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:
npm install planckPenyiapan dasar:
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:
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:
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:
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:
npm install p2-esPenyiapan dasar:
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:
// 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:
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 = rubberKelebihan:
- 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
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:
// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1
// Rapier
bodyDesc.setCanSleep(true)3. Gunakan bentuk tabrakan sederhana
// 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)
// Cannon-es
world.solver.iterations = 5 // Default 10
// Rapier - configured at world creation5. Jalankan fisika di 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())
})
}
}Kapan harus menggunakan apa
| Skenario | Rekomendasi | Alasan |
|---|---|---|
| Game 3D produksi | Rapier | Performa terbaik, API modern, kendaraan |
| Prototipe game 3D | Cannon-es | Sederhana, mudah di-debug |
| Benda lunak, kain, tali | Ammo.js atau Jolt | Keduanya mendukung kain/benda lunak; port WASM Jolt (JoltPhysics.js) dipelihara lebih aktif |
| Game browser 3D sederhana | Oimo.js | Sangat kecil dan memadai |
| Game jam 2D | Matter.js | Penyiapan cepat, rendering bawaan |
| Platformer presisi | Planck.js | Determinisme timestep tetap, terdokumentasi dengan baik |
| Mekanisme 2D kompleks | p2-es | Sistem constraint yang kaya |
| Game 2D yang mengutamakan performa | Box2D WASM | Opsi 2D tercepat, CCD sesungguhnya |
Integrasi dengan renderer
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
})
})Kesalahan umum
Skala sangat berpengaruh
Engine fisika bekerja paling baik dengan skala dunia nyata (1 satuan = 1 meter). Jangan gunakan koordinat piksel secara langsung.
// 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 renderingKebocoran memori pada engine WASM
Selalu bersihkan benda saat menghapusnya:
// 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:
// 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 60Terkait
- Dasar-dasar WebGL untuk pengembang game
- Web Worker untuk logika game — alihkan pemrosesan fisika ke thread Worker
- Rilis game web yang dimuat dengan cepat
- Game loop Canvas 2D — pola timestep tetap yang digunakan oleh setiap mesin fisika
- Stack Teknologi Game Web pada 2026 — bagaimana mesin fisika menjadi bagian dari stack yang lebih luas
- Perbandingan Mesin Game Web — mesin game dengan dukungan fisika bawaan
Sumber Daya Eksternal
- Dokumentasi Rapier — dokumentasi dan contoh resmi fisika Rapier
- Dokumentasi Cannon-es — referensi API dan panduan
- Dokumentasi Matter.js — referensi API lengkap
- Dokumentasi Planck.js — Box2D untuk JavaScript