웹 게임을 위한 게임 물리
물리 시뮬레이션은 게임에 생기를 불어넣습니다. 떨어지는 물체, 튀는 공, 래그돌, 차량, 부서지는 환경 같은 것들이죠. 이 가이드는 웹 게임에서 쓸 수 있는 주요 물리 라이브러리를 다루며, 실제 코드 예제와 솔직한 장단점을 함께 담았습니다.
한눈에 보기
| 엔진 | 차원 | 성능 | 크기 | 난이도 | 소프트 바디 | 차량 | CCD | 결정성 |
|---|---|---|---|---|---|---|---|---|
| Rapier | 2D/3D | ⭐⭐⭐⭐⭐ | 1.4 MB | 중 | — | ✅ | ✅ | ✅ |
| Cannon-es | 3D | ⭐⭐⭐ | 150 KB | 쉬움 | — | — | — | — |
| Ammo.js | 3D | ⭐⭐⭐⭐⭐ | 1-2 MB | 어려움 | ✅ | ✅ | ✅ | ⚠️ |
| Jolt | 3D | ⭐⭐⭐⭐⭐ | — | 어려움 | ✅ | ✅ | ✅ | ✅ |
| Oimo.js | 3D | ⭐⭐⭐ | 100 KB | 쉬움 | — | — | — | — |
| Matter.js | 2D | ⭐⭐⭐ | 80 KB | 쉬움 | — | — | — | ⚠️ |
| Planck.js | 2D | ⭐⭐⭐⭐ | 120 KB | 중 | — | — | ✅ | ⚠️ |
| p2-es | 2D | ⭐⭐⭐ | 100 KB | 중 | — | — | — | — |
| Box2D WASM | 2D | ⭐⭐⭐⭐⭐ | 300 KB | 어려움 | — | — | ✅ | ✅ |
범례:
- CCD = 연속 충돌 감지 (빠르게 움직이는 물체가 벽을 통과하는 현상을 막아줍니다)
- ⚠️ 결정성 = 고정 타임스텝을 쓰면 가능하지만, 플랫폼 간 일관성은 보장되지 않습니다
빠른 추천
| 당신의 상황 | 최선의 선택 |
|---|---|
| 프로덕션 3D 게임 | Rapier — 최고의 성능, 현대적인 API, 활발한 개발 |
| 학습 / 프로토타입 | Cannon-es (3D) 또는 Matter.js (2D) — 간단한 API, 쉬운 디버깅 |
| 차량 물리 | Rapier 또는 Ammo.js — 둘 다 레이캐스트 차량 컨트롤러를 갖췄습니다 |
| 소프트 바디, 천, 밧줄 | Ammo.js 또는 Jolt (JoltPhysics.js) — 둘 다 천과 소프트 바디를 지원하며, Jolt의 WASM 포팅이 더 활발하게 유지보수됩니다 |
| 정밀한 2D 플랫포머 | Planck.js — Box2D 알고리즘, 고정 타임스텝에서 결정적 |
| 최대 2D 성능 | Box2D WASM — 브라우저에서 네이티브 속도 |
| 가장 작은 번들 크기 | Oimo.js (3D) 또는 Matter.js (2D) |
자세한 비교
3D 물리 엔진
| 엔진 | 언어 | 크기 | 성능 | 적합한 용도 |
|---|---|---|---|---|
| Rapier | Rust/WASM | ~1.4 MB | 뛰어남 | 프로덕션 게임, 복잡한 시뮬레이션 |
| Cannon-es | JavaScript | ~150 KB | 좋음 | 프로토타입, 간단한 게임 |
| Ammo.js | C++/WASM | ~1-2 MB | 뛰어남 | AAA급 기능, 소프트 바디, 차량 |
| Jolt | C++/WASM | WASM | 뛰어남 | 활발히 유지보수되는 AAA급 기능 |
| Oimo.js | JavaScript | ~100 KB | 좋음 | 간단한 게임, 빠른 프로토타입 |
2D 물리 엔진
| 엔진 | 언어 | 크기 | 성능 | 적합한 용도 |
|---|---|---|---|---|
| Matter.js | JavaScript | ~80 KB | 좋음 | 비주얼 게임, 프로토타입 |
| Planck.js | JavaScript | ~120 KB | 좋음 | 플랫포머, 정밀한 물리 |
| p2-es | JavaScript | ~100 KB | 좋음 | 제약 조건, 기계 장치 |
| Box2D WASM | C++/WASM | ~300 KB | 뛰어남 | 많은 바디 수 |
3D 물리 엔진
Rapier — 현대적인 선택
Rapier는 JavaScript/WASM 바인딩을 갖춘 Rust 물리 엔진입니다. 2025-2026년 웹 게임에서 가장 성능이 좋은 선택지로, 2024년 버전 대비 2-5배 속도가 빨라졌습니다.
설치:
npm install @dimforge/rapier3d
# 또는 SIMD 사용 (더 빠르지만 최신 브라우저가 필요):
npm install @dimforge/rapier3d-simd기본 설정:
import RAPIER from '@dimforge/rapier3d'
// 초기화 (WASM은 비동기가 필요)
await RAPIER.init()
// 중력이 있는 월드 생성
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)
// 바닥 생성 (정적 바디)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)
// 떨어지는 박스 생성 (동적 바디)
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)게임 루프 연동:
const FIXED_TIMESTEP = 1 / 60
function physicsStep() {
world.step()
}
function gameLoop() {
physicsStep()
// 렌더 객체를 물리와 동기화
const position = boxBody.translation()
const rotation = boxBody.rotation()
// Three.js/Babylon 메시 업데이트
mesh.position.set(position.x, position.y, position.z)
mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)
requestAnimationFrame(gameLoop)
}충돌 감지:
// 이벤트 기반 충돌 감지
world.contactPairsWith(boxCollider, (otherCollider) => {
console.log('Box is touching:', otherCollider)
})
// 레이 캐스팅
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)
}조인트:
// 힌지 조인트 생성 (문)
const jointData = RAPIER.JointData.revolute(
{ x: 0, y: 0, z: 0 }, // body1의 앵커
{ x: -1, y: 0, z: 0 }, // body2의 앵커
{ x: 0, y: 1, z: 0 } // 회전축
)
world.createImpulseJoint(jointData, body1, body2, true)차량 컨트롤러:
// 섀시 강체 생성
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)
// 차량 컨트롤러 생성
const vehicle = world.createVehicleController(chassis)
// 바퀴 추가 (앞왼쪽, 앞오른쪽, 뒤왼쪽, 뒤오른쪽)
const suspensionRestLength = 0.3
const wheelRadius = 0.4
// 앞바퀴 (조향)
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
)
// 뒷바퀴 (구동)
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
)
// 모든 바퀴의 서스펜션 설정
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)
}
// 게임 루프 안에서
function updateVehicle(steering, engineForce, brakeForce) {
// 조향 (앞바퀴만)
vehicle.setWheelSteering(0, steering)
vehicle.setWheelSteering(1, steering)
// 엔진 (뒷바퀴)
vehicle.setWheelEngineForce(2, engineForce)
vehicle.setWheelEngineForce(3, engineForce)
// 브레이크 (모든 바퀴)
for (let i = 0; i < 4; i++) {
vehicle.setWheelBrake(i, brakeForce)
}
// 차량 물리 업데이트
vehicle.updateVehicle(world.timestep)
}장점:
- 최고의 성능 (WASM + SIMD)
- 뛰어난 충돌 감지 정확도
- 플랫폼 간 결정성 (같은 입력 = 같은 출력)
- 활발한 개발, 현대적인 API
- 연속 충돌 감지 (터널링 없음)
- 캐릭터 컨트롤러와 차량 컨트롤러 내장
단점:
- 큰 번들 크기 (~1.4 MB)
- 비동기 초기화 필요
- 순수 JS 대안보다 복잡한 API
- WASM 디버깅이 까다로울 수 있음
Cannon-es — 간단하고 효과적
Cannon-es는 Cannon.js의 유지보수 포크입니다. 순수 JavaScript로 되어 있어 이해하기 쉽고, 학습과 프로토타입에 잘 맞습니다.
설치:
npm install cannon-es기본 설정:
import * as CANNON from 'cannon-es'
// 월드 생성
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.81, 0)
})
// 바닥
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane()
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)
// 떨어지는 구체
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)게임 루프:
const TIMESTEP = 1 / 60
function animate() {
world.step(TIMESTEP)
// Three.js와 동기화
mesh.position.copy(sphereBody.position)
mesh.quaternion.copy(sphereBody.quaternion)
requestAnimationFrame(animate)
}충돌 이벤트:
sphereBody.addEventListener('collide', (event) => {
const contact = event.contact
const impactVelocity = contact.getImpactVelocityAlongNormal()
if (Math.abs(impactVelocity) > 5) {
console.log('Hard impact!')
}
})제약 조건:
// 거리 제약 (밧줄 같은)
const constraint = new CANNON.DistanceConstraint(
bodyA, bodyB,
2 // 거리
)
world.addConstraint(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)장점:
- 순수 JavaScript, WASM 복잡도 없음
- 작은 번들 크기 (~150 KB)
- 배우고 디버깅하기 쉬움
- 어디서나 동작
- 훌륭한 Three.js 연동
- 좋은 문서
단점:
- WASM 대안보다 느림
- 많은 바디(>100)에서 버거움
- 제한적인 트라이메시 지원
- CCD 내장 없음 (터널링 가능)
- 개발이 덜 활발함
Ammo.js — 완전한 Bullet Physics의 힘
Ammo.js는 Bullet Physics 엔진을 WebAssembly로 컴파일한 것입니다. 소프트 바디, 차량, 고급 제약 조건을 포함해 최대한의 기능을 제공합니다.
설치:
npm install ammo.js
# 또는 CDN에서 사용기본 설정:
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
}소프트 바디 (천):
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
}차량 물리:
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)
// 바퀴 추가
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)) // 앞왼쪽
addWheel(true, new Ammo.btVector3(-1, 0, 1.5)) // 앞오른쪽
addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // 뒤왼쪽
addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// 뒤오른쪽
return vehicle
}장점:
- 가장 완전한 기능 집합
- 소프트 바디, 천, 밧줄
- 고급 차량 물리
- 검증됨 (수많은 AAA 게임에서 사용)
- 높은 설정 자유도
단점:
- 복잡하고 장황한 API
- 큰 번들 크기
- 메모리 관리 필요 (객체 파괴)
- 가파른 학습 곡선
- 흩어진 문서
- 플랫폼 간 결정적이지 않음 (세심한 설정으로 같은 기기에서만 가능)
Jolt — 현대적인 AAA급 3D 물리
Jolt Physics는 Horizon Forbidden West 같은 게임의 바탕이 된 엔진이며, JoltPhysics.js가 이를 WASM 포팅으로 브라우저에 가져왔습니다. Rapier와 Ammo.js 사이의 든든한 중간 지점입니다. Rapier보다 기능이 많고(소프트 바디, 천, 바퀴 차량 컨트롤러), Ammo.js에는 없는 활발한 유지보수를 갖췄습니다. npm 패키지는 jolt-physics이고, React Three Fiber(@react-three/jolt)와 Babylon.js용 통합 패키지도 준비되어 있습니다.
npm install jolt-physicsRapier처럼 비동기로 초기화되는 WASM이며, C++ 인터페이스를 그대로 반영한 장황하지만 완전한 API를 갖췄습니다. Bullet급 기능(소프트 바디, 차량)이 필요하면서도 Ammo.js 대신 유지보수되는 현대적인 코드베이스를 원할 때 Jolt를 고르세요.
Oimo.js — 가볍고 빠름
Oimo.js는 가벼운 3D 물리 엔진입니다. 고급 기능이 필요 없는 간단한 게임에 잘 맞습니다.
설치:
npm install oimo기본 설정:
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]
})
// 바닥 생성
world.add({
type: 'box',
size: [100, 1, 100],
pos: [0, -0.5, 0],
move: false
})
// 떨어지는 구체 생성
const sphere = world.add({
type: 'sphere',
size: [1],
pos: [0, 10, 0],
move: true,
density: 1,
friction: 0.4,
restitution: 0.2
})게임 루프:
function animate() {
world.step()
// 위치/회전 가져오기
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)
}장점:
- 매우 작음 (~100 KB)
- 간단한 API
- 기본적인 장면에서 좋은 성능
- Babylon.js 지원 내장
단점:
- 제한적인 형태 (기본 도형만)
- 소프트 바디 없음
- 빈약한 문서
- 개발이 덜 활발함
- 제한적인 조인트 옵션
2D 물리 엔진
Matter.js — 아름답고 직관적
Matter.js는 뛰어난 렌더링과 디버그 도구를 갖춘 기능이 풍부한 2D 물리 엔진입니다.
설치:
npm install matter-js기본 설정:
import Matter from 'matter-js'
const { Engine, Render, World, Bodies, Runner } = Matter
// 엔진 생성
const engine = Engine.create()
// 렌더러 생성 (선택, 디버깅에 아주 좋음)
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
})
// 바디 생성
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
})
// 월드에 추가
World.add(engine.world, [ground, box, circle])
// 실행
Render.run(render)
Runner.run(Runner.create(), engine)충돌 이벤트:
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
console.log('Collision between:', pair.bodyA.label, pair.bodyB.label)
})
})제약 조건:
// 핀 제약
const pin = Matter.Constraint.create({
pointA: { x: 400, y: 100 },
bodyB: box,
stiffness: 0.9
})
// 두 바디 사이의 스프링
const spring = Matter.Constraint.create({
bodyA: box,
bodyB: circle,
stiffness: 0.01,
length: 100
})
World.add(engine.world, [pin, spring])마우스 상호작용:
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)장점:
- 아름다운 기본 렌더링
- 학습/프로토타이핑에 좋음
- 직관적인 API
- 좋은 문서
- 활발한 커뮤니티
- 기본적으로 결정적 — Matter.Runner는 이제 고정 결정적 타임스텝을 사용합니다 (비고정 타임스텝은 v0.20.0에서 제거됨)
단점:
- CCD 없음 (빠른 물체가 터널링할 수 있음 — 서브스테핑으로 완화)
- 많은 바디에서 성능 문제
- WASM 옵션 없음
- 복잡한 시뮬레이션에서 제한적인 정밀도
Planck.js — JavaScript용 Box2D
Planck.js는 Box2D를 JavaScript로 완전히 다시 작성한 것입니다. 검증된 물리이며, 고정 타임스텝을 사용하면 결정적입니다.
설치:
npm install planck기본 설정:
import { World, Vec2, Box, Circle, Edge } from 'planck'
// 월드 생성
const world = new World({
gravity: Vec2(0, -10)
})
// 바닥 생성
const ground = world.createBody()
ground.createFixture({
shape: Edge(Vec2(-40, 0), Vec2(40, 0))
})
// 동적 박스 생성
const box = world.createBody({
type: 'dynamic',
position: Vec2(0, 10)
})
box.createFixture({
shape: Box(1, 1),
density: 1,
friction: 0.3,
restitution: 0.5
})고정 타임스텝 게임 루프:
const TIMESTEP = 1 / 60
const VELOCITY_ITERATIONS = 8
const POSITION_ITERATIONS = 3
function gameLoop() {
world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
// 모든 바디 순회
for (let body = world.getBodyList(); body; body = body.getNext()) {
const pos = body.getPosition()
const angle = body.getAngle()
// 스프라이트 업데이트...
}
requestAnimationFrame(gameLoop)
}충돌 콜백:
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) => {
// 여기서 접촉을 비활성화할 수 있음
// contact.setEnabled(false)
})조인트:
import { RevoluteJoint, DistanceJoint, PrismaticJoint } from 'planck'
// 회전 조인트 (힌지)
const joint = world.createJoint(RevoluteJoint({
bodyA: ground,
bodyB: box,
localAnchorA: Vec2(0, 5),
localAnchorB: Vec2(-1, 0),
enableMotor: true,
maxMotorTorque: 1000,
motorSpeed: 2
}))
// 거리 조인트 (스프링)
world.createJoint(DistanceJoint({
bodyA: boxA,
bodyB: boxB,
localAnchorA: Vec2(0, 0),
localAnchorB: Vec2(0, 0),
length: 5,
stiffness: 10,
damping: 0.5
}))장점:
- 고정 타임스텝에서 결정적
- 잘 정리된 문서 (Box2D 문서가 그대로 적용됨)
- 좋은 성능
- TypeScript 지원
- 작은 번들 크기
단점:
- CCD가 조인트를 처리하지 못해, 조인트로 연결된 빠른 물체가 늘어날 수 있음
- Matter.js보다 가파른 학습 곡선
- 렌더러 내장 없음
- Box2D의 특이점 (단위 스케일이 중요)
p2-es — 유연한 2D 물리
p2-es는 p2.js의 유지보수 포크입니다. 복잡한 제약 조건과 기계 장치가 필요한 게임에 좋습니다.
설치:
npm install p2-es기본 설정:
import * as p2 from 'p2-es'
const world = new p2.World({
gravity: [0, -9.81]
})
// 바닥
const groundBody = new p2.Body({
mass: 0, // 정적
position: [0, -1]
})
groundBody.addShape(new p2.Plane())
world.addBody(groundBody)
// 동적 원
const circleBody = new p2.Body({
mass: 1,
position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)고급 제약 조건:
// 기어 제약
const gear = new p2.GearConstraint(bodyA, bodyB, {
ratio: 2 // bodyB가 두 배 빠르게 회전
})
world.addConstraint(gear)
// 프리즈매틱 제약 (슬라이더)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
localAnchorA: [0, 0],
localAnchorB: [0, 0],
localAxisA: [1, 0],
disableRotationalLock: false
})
world.addConstraint(prismatic)
// 잠금 제약 (용접)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)접촉 재질:
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)
// 형태에 적용
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber장점:
- 풍부한 제약 시스템
- 접촉 재질
- 기계/장치에 좋음
- 슬리핑 바디 (성능)
- ES 모듈, 트리 셰이킹 가능
단점:
- 렌더러 내장 없음
- 일부 형태 조합은 지원하지 않음
- 대안만큼 활발하지 않음
- 문서의 빈틈
성능 팁
1. 고정 타임스텝 사용
const TIMESTEP = 1 / 60
let accumulator = 0
function gameLoop(deltaTime) {
accumulator += deltaTime
while (accumulator >= TIMESTEP) {
world.step(TIMESTEP)
accumulator -= TIMESTEP
}
// 부드러운 렌더링을 위한 보간
const alpha = accumulator / TIMESTEP
// lerp(previousState, currentState, alpha)
}2. 비활성 바디 재우기
대부분의 엔진은 슬리핑을 지원합니다. 켜세요:
// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1
// Rapier
bodyDesc.setCanSleep(true)3. 간단한 충돌 형태 사용
// 좋음: 간단한 도형
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))
// 피하기: 동적 바디에 복잡한 트라이메시
const trimesh = new CANNON.Trimesh(vertices, indices) // 느림!4. 솔버 반복 횟수 줄이기 (신중하게)
// Cannon-es
world.solver.iterations = 5 // 기본 10
// Rapier - 월드 생성 시 설정5. 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
// 렌더 객체 업데이트
}
// 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())
})
}
}어떤 걸 언제 쓸까
| 시나리오 | 추천 | 이유 |
|---|---|---|
| 프로덕션 3D 게임 | Rapier | 최고의 성능, 현대적인 API, 차량 |
| 3D 게임 프로토타입 | Cannon-es | 간단하고 디버깅하기 쉬움 |
| 소프트 바디, 천, 밧줄 | Ammo.js 또는 Jolt | 둘 다 천/소프트 바디 지원, Jolt의 WASM 포팅(JoltPhysics.js)이 더 활발히 유지보수됨 |
| 간단한 3D 브라우저 게임 | Oimo.js | 작고 충분함 |
| 2D 게임잼 | Matter.js | 빠른 설정, 렌더링 내장 |
| 정밀한 플랫포머 | Planck.js | 고정 타임스텝 결정성, 좋은 문서 |
| 복잡한 2D 기계 장치 | p2-es | 풍부한 제약 시스템 |
| 성능이 중요한 2D | Box2D WASM | 가장 빠른 2D 옵션, 진짜 CCD |
렌더러와의 연동
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() // 물리 바디 -> 메시
function createPhysicsBox(x, y, z) {
// 물리
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)
// 렌더
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) {
// 물리
const body = Matter.Bodies.rectangle(x, y, width, height)
Matter.World.add(engine.world, body)
// 렌더
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
})
})흔한 함정
스케일이 중요하다
물리 엔진은 실제 세계 스케일(1 단위 = 1 미터)에서 가장 잘 동작합니다. 픽셀 좌표를 그대로 쓰지 마세요.
// 나쁨: 픽셀 위치 사용
const body = Bodies.circle(400, 300, 50) // 반지름 50 픽셀?
// 좋음: 스케일 팩터 사용
const SCALE = 50 // 미터당 50 픽셀
const body = Bodies.circle(8, 6, 1) // 반지름 1 미터
// 렌더링할 때 SCALE을 곱한다WASM 엔진의 메모리 누수
바디를 제거할 때는 항상 정리하세요:
// Rapier
world.removeRigidBody(body)
// Ammo.js
physicsWorld.removeRigidBody(body)
Ammo.destroy(body)
Ammo.destroy(shape)
Ammo.destroy(motionState)터널링 (물체가 서로 통과)
빠르게 움직이는 물체는 얇은 벽을 통과할 수 있습니다. 해결책:
// Rapier: CCD 켜기
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)
// Cannon-es: 더 작은 타임스텝이나 더 두꺼운 벽 사용
world.step(1/120) // 60 대신 120 Hz관련 글
- 게임 개발자를 위한 WebGL 기초
- 게임 로직을 위한 Web Workers — 물리를 Worker 스레드로 분리
- 빠르게 로드되는 웹 게임 배포하기
- Canvas 2D 게임 루프 — 모든 물리 엔진이 쓰는 고정 타임스텝 패턴
- 2026년 웹 게임 기술 스택 — 물리 엔진이 전체 스택에서 차지하는 위치
- 웹 게임 엔진 비교 — 물리 지원이 내장된 엔진들
외부 자료
- Rapier 문서 — 공식 Rapier 물리 문서와 예제
- Cannon-es 문서 — API 레퍼런스와 가이드
- Matter.js 문서 — 전체 API 레퍼런스
- Planck.js 문서 — JavaScript용 Box2D