레슨 7 / 8·25분
실전: 물리 퍼즐 게임
물리 기반 퍼즐 게임 만들기
지금까지 배운 Matter.js의 모든 기능을 활용하여 물리 퍼즐 게임을 만들어봅니다. 플레이어가 공을 발사하여 목표물을 맞추는 게임으로, 장애물 배치, 충돌 감지, 점수 시스템, 레벨 구성 등을 포함합니다. 마우스로 발사 각도와 힘을 조절하고, 충돌 이벤트로 점수를 관리합니다.
javascript
const { Engine, World, Bodies, Body, Events,
Constraint, Mouse, MouseConstraint, Composite } = Matter;
// ── 게임 설정 ──
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const engine = Engine.create({ gravity: { x: 0, y: 1 } });
let score = 0;
let shotsLeft = 5;
// ── 레벨 구성 ──
function createLevel() {
// 바닥, 벽
const ground = Bodies.rectangle(400, 590, 800, 20, {
isStatic: true, label: 'ground',
render: { fillStyle: '#2c3e50' },
});
const wallL = Bodies.rectangle(0, 300, 20, 600, { isStatic: true });
const wallR = Bodies.rectangle(800, 300, 20, 600, { isStatic: true });
// 장애물 플랫폼
const platform1 = Bodies.rectangle(500, 400, 150, 15, {
isStatic: true, angle: -0.2, label: 'platform',
render: { fillStyle: '#7f8c8d' },
});
const platform2 = Bodies.rectangle(350, 300, 120, 15, {
isStatic: true, angle: 0.15, label: 'platform',
render: { fillStyle: '#7f8c8d' },
});
// 목표물 (별)
const targets = [];
const targetPositions = [
{ x: 600, y: 370 },
{ x: 400, y: 260 },
{ x: 550, y: 200 },
];
targetPositions.forEach((pos, i) => {
const target = Bodies.circle(pos.x, pos.y, 15, {
isStatic: true, label: 'target',
isSensor: true, // 센서: 충돌 감지만 하고 물리 반응 없음
render: { fillStyle: '#f1c40f' },
});
targets.push(target);
});
World.add(engine.world, [
ground, wallL, wallR, platform1, platform2, ...targets
]);
return targets;
}
const targets = createLevel();javascript
// ── 발사 시스템 ──
let projectile = null;
let isAiming = false;
let aimStart = { x: 0, y: 0 };
canvas.addEventListener('mousedown', (e) => {
if (shotsLeft <= 0) return;
isAiming = true;
aimStart = { x: e.offsetX, y: e.offsetY };
});
canvas.addEventListener('mouseup', (e) => {
if (!isAiming) return;
isAiming = false;
// 드래그 방향과 거리로 발사 힘 계산
const dx = aimStart.x - e.offsetX;
const dy = aimStart.y - e.offsetY;
const power = Math.min(Math.sqrt(dx * dx + dy * dy) * 0.0005, 0.05);
// 발사체 생성
projectile = Bodies.circle(100, 500, 12, {
label: 'projectile',
restitution: 0.6,
density: 0.004,
render: { fillStyle: '#e74c3c' },
});
World.add(engine.world, projectile);
Body.applyForce(projectile, projectile.position, {
x: dx * power * 0.1,
y: dy * power * 0.1,
});
shotsLeft--;
});
// ── 충돌 이벤트: 점수 처리 ──
Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
const labels = [pair.bodyA.label, pair.bodyB.label];
if (labels.includes('projectile') && labels.includes('target')) {
const target = pair.bodyA.label === 'target'
? pair.bodyA : pair.bodyB;
World.remove(engine.world, target);
score += 100;
console.log('명중! 점수: ' + score);
// 모든 목표물 제거 시 클리어
const remaining = targets.filter(
(t) => Composite.get(engine.world, t.id, 'body')
);
if (remaining.length === 0) {
console.log('레벨 클리어! 총점: ' + score);
}
}
});
});javascript
// ── 게임 렌더 루프 ──
function gameLoop() {
Engine.update(engine, 1000 / 60);
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 모든 바디 그리기
const bodies = Composite.allBodies(engine.world);
bodies.forEach((body) => {
const vertices = body.vertices;
ctx.beginPath();
ctx.moveTo(vertices[0].x, vertices[0].y);
for (let j = 1; j < vertices.length; j++) {
ctx.lineTo(vertices[j].x, vertices[j].y);
}
ctx.closePath();
// 라벨에 따라 색상 결정
if (body.label === 'target') {
ctx.fillStyle = '#f1c40f';
} else if (body.label === 'projectile') {
ctx.fillStyle = '#e74c3c';
} else if (body.label === 'platform') {
ctx.fillStyle = '#7f8c8d';
} else {
ctx.fillStyle = '#2c3e50';
}
ctx.fill();
ctx.strokeStyle = '#34495e';
ctx.stroke();
});
// 조준선 그리기
if (isAiming) {
ctx.beginPath();
ctx.moveTo(100, 500);
ctx.setLineDash([5, 5]);
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 2;
ctx.stroke();
ctx.setLineDash([]);
}
// UI 표시
ctx.fillStyle = '#ffffff';
ctx.font = '18px sans-serif';
ctx.fillText('점수: ' + score, 20, 30);
ctx.fillText('남은 발사: ' + shotsLeft, 20, 55);
requestAnimationFrame(gameLoop);
}
gameLoop();- •isSensor: true — 물리 반응 없이 충돌 감지만 수행 (트리거 용도)
- •Body.applyForce — 마우스 드래그 방향/거리로 발사 힘 계산
- •Composite.get(world, id, type) — 월드에서 특정 바디 검색
- •Composite.allBodies(world) — 월드의 모든 바디 배열 반환
- •body.vertices — 바디의 꼭짓점으로 다각형 렌더링
- •label 속성으로 바디 종류를 구분하여 게임 로직 처리
💡
isSensor를 활용하면 트리거 영역을 만들 수 있습니다. 센서 바디는 충돌 이벤트는 발생시키지만 물리적으로 다른 바디를 밀어내지 않습니다. 아이템 수집, 영역 진입 감지 등에 활용하세요.