투석기 돌을 관성으로 놓기까지: 트레뷰셋을 하루 동안 고친 기록
트레뷰셋은 투석기로 돌을 날려 언덕 위 성을 무너뜨리는 게임이다. 조작은 두 번 누르기다. 첫 번째 누름으로 오르내리는 힘 게이지를 멈추면 평형추가 떨어지며 팔이 돈다. 두 번째 누름으로 돌을 놓는다. 일찍 놓으면 높이 날아가고, 늦게 놓으면 낮게 날아간다. 성 꼭대기의 깃발을 떨어뜨리면 함락이고, 다음 성으로 넘어간다.
물리와 관련된 시행착오를 순서대로 적는다.
처음에는 놓는 순간 새로 쏘았다
첫 버전의 놓기는 간단했다. 팔은 0.8초 동안 일정하게 돌고, 두 번째로 누른 시점이 그 0.8초의 어디쯤인지로 각도를 정했다. 처음이면 75°, 끝이면 5° 다. 속도는 힘 게이지 값으로 정했다. 그리고 그 각도와 속도로 돌을 새로 쏘았다.
// first version: a fresh launch at release, unrelated to how the sling was moving
function release(s) { // s: 0..1 through the swing
const angle = 75 - 70 * s;
const v = 180 + power * 300, a = (angle * Math.PI) / 180;
stone = { x, y, vx: Math.cos(a) * v, vy: -Math.sin(a) * v };
}
해 보니 두 가지가 어색했다. 하나는 돌이 줄 끝에서 갑자기 추진력을 받아 튀어 나가는 것처럼 보였다는 것이다. 팔이 천천히 돌고 있어도 돌은 정해진 속도로 날아가니, 팔과 돌의 움직임이 이어지지 않았다. 다른 하나는 두 번째 누르기를 놓쳐도 돌이 앞으로 잘 날아갔다는 것이다. 끝까지 누르지 않으면 5° 로 놓는 것으로 처리했기 때문이다. 타이밍을 놓쳤는데 벌이 없었다.
줄 끝이 지나는 길을 표로 만들었다
진짜 투석기에서 돌은 따로 쏘아지지 않는다. 줄 끝에서 원을 그리며 돌다가, 줄이 풀리는 순간 그때 움직이던 방향과 빠르기 그대로 날아간다. 그렇게 바꾸려면 놓는 순간 돌이 어느 방향으로 얼마나 빠르게 움직이고 있는지 알아야 한다.
돌의 자리는 팔 각도로 정해진다. 길이 64px 인 팔이 160° 에서 310° 까지 돌고, 팔 끝에 30px 줄이 매달려 있다. 줄은 처음에 팔 끝에서 수직으로 늘어져 있다가 팔이 돌수록 바깥으로 펴진다. 줄은 처음에 16px 였다가 30px 로 늘렸다.
팔이 도는 동안 돌이 지나는 길을 600조각으로 나눠 표(PATH)로 만들었다. 조각마다 그 점까지 지나온 길이와 그 점에서 움직이는 방향(접선)을 적어 둔다.
// the path of the stone at the sling's tip: travelled length and tangent at each step
const PATH = [];
for (let i = 0, len = 0; i <= 600; i++) {
const p = slingPos(TH0 + (TH1 - TH0) * (i / 600), i / 600);
if (i) len += Math.hypot(p.x - PATH[i - 1].x, p.y - PATH[i - 1].y);
PATH.push({ x: p.x, y: p.y, len });
}
// then for each point: ux, uy = unit vector toward the next point, dir = its angle
그다음 시간에 따라 돌이 이 길을 얼마나 갔는지 정한다(swingAt). 돌이 움직이는 방향이 75° 가 되는 지점까지는 고르게 빨라져 빠르기 v 에 닿는다. v 는 힘 게이지로 정한다(180 + 힘 × 300 px/s). 75° 에서 5° 사이, 놓는 구간에서는 v 를 그대로 유지한다. 그 뒤로는 팔이 느려져 끝에서 0.3v 가 된다.
놓는 순간 돌의 속도는 그 지점의 접선 × 빠르기다. 따로 계산하지 않는다.
function release() {
const m = swingAt(swingT, swingV); // travelled length and speed now
const q = pathAt(m.len); // tangent at that point of the path
stone = { x, y, vx: q.ux * m.sp, vy: q.uy * m.sp };
}
이렇게 바꾸니 두 문제가 함께 풀렸다. 돌은 줄 끝에서 움직이던 그대로 이어져 날아간다. 너무 일찍 놓으면 아직 위·뒤로 움직이던 돌이 위·뒤로 날아간다. 누르기를 놓치면 팔이 끝까지 돌며 줄이 앞쪽 아래로 휘감기고, 돌은 투석기 바로 앞 땅(x 약 175~215)에 처박힌다. 이제 놓치면 한 발을 잃는다.
대가도 있었다. 놓는 구간이 짧아졌다. 힘을 끝까지 채우면 첫 누름 뒤 0.34초부터 0.56초 사이, 0.22초 안에 눌러야 한다. 힘이 절반이면 0.49~0.81초, 힘이 0 이면 0.9~1.5초다. 예전에는 0.8초 동안 75° 에서 5° 를 고르게 골랐다. 팔과 줄을 합쳐도 94px 남짓이라 돌이 75° 에서 5° 까지 도는 길은 짧다. 멀리 던지려면 빨라야 하니, 그 짧은 길을 금방 지나간다. 투석기의 크기가 정한 한계다. 실제로 여러 판 해 보고 빡빡하면 중력을 낮춰 필요한 빠르기를 줄이거나, 줄을 늘려 길을 넓힐 생각이다.
성은 강체 물리 대신 받침 규칙으로 무너진다
성은 16px 칸 격자에 놓인 돌 블록이다. 블록이 기울고 구르는 진짜 강체 물리는 넣지 않았다. 같은 곳을 맞혀도 결과가 들쑥날쑥하고, 무너짐이 멈출 때까지 오래 걸린다. 대신 블록이 받쳐져 있는지를 규칙으로 정했다.
- 언덕에 닿은 맨 아래 줄은 받쳐져 있다.
- 위 줄의 블록은 바로 아래 블록이 받쳐져 있으면 받쳐져 있다.
- 바로 아래가 비어 있어도, 같은 줄에서 받쳐진 블록과 끊김 없이 이어져 있고 그 블록이 2칸 안에 있으면 받쳐져 있다. 처마처럼 옆으로 조금 튀어나온 것은 버틴다.
- 받쳐지지 않은 블록은 모두 떨어지는 조각이 된다.
function settle() {
for (let j = 0; j < rows; j++) {
for (let i = 0; i < cols; i++) if (grid[j][i] && (j === 0 || sup[j - 1][i])) sup[j][i] = true;
if (j === 0) continue;
const anchor = sup[j].slice();
for (let i = 0; i < cols; i++) {
if (!grid[j][i] || anchor[i]) continue;
for (const d of [-1, 1]) for (let s = 1; s <= 2; s++) { // look up to 2 cells sideways
const x = i + d * s;
if (x < 0 || x >= cols || !grid[j][x]) break; // gap: no support this way
if (anchor[x]) { sup[j][i] = true; break; }
}
}
}
// every block without support falls
}
돌이 블록에 맞으면 보통 블록은 부서지고 돌은 속도가 0.55배로 줄어든 채 뚫고 지나간다. 단단한 블록은 두 번 맞아야 부서지고, 돌은 그 자리에서 멈춘다. 속도가 150px/s 보다 느린 돌은 블록을 부수지 못한다. 블록이 부서지면 받침 규칙을 다시 돌린다.
처음에는 떨어지는 블록마다 무작위로 조금씩 옆으로 밀고 돌렸다. 높은 탑이 무너질 때도 한쪽으로 기울지 않고 제자리에서 흩어졌다. 그래서 떨어지는 블록마다 돌이 날아온 쪽으로 가로 속도와 회전을 주었다. 떨어지기 시작한 줄에서 높이 있던 블록일수록 더 많이 밀리고 더 빨리 돈다. 실제로는 블록마다 따로 떨어지지만, 위가 더 멀리 가니 탑이 통째로 기울어 쓰러지는 것처럼 보인다.
가까운 성이 쉽지 않았다
첫 성은 투석기 가까이 있다. 그런데 성을 맞히기는 쉬워도 함락은 어려웠다. 함락하려면 깃발이 꽂힌 블록 하나를 부수거나 떨어뜨려야 하는데, 성벽 뒤 본성 꼭대기의 블록 하나는 작은 과녁이다.
얼마나 어려운지 감으로 정하지 않고 쟀다. 게임 코드를 Node 의 vm 에서 돌려, 힘과 놓는 때를 고르게 무작위로 골라 많이 던지게 했다. 첫 성을 첫 돌로 함락하는 비율이 약 1.4% 였다.
그래서 성 1~3 은 모양을 바꿨다. 성벽 대신 다리 위에 높은 탑을 세웠다. 3칸 폭의 몸통이 다리 위에 서 있어서, 다리를 부수면 받침 규칙에 따라 탑 전체가 받침을 잃고 통째로 넘어진다. 다리는 깃발 블록보다 4~6배 맞히기 쉽다. 성 1 은 가운데 다리 하나, 성 2 는 양 끝 다리 둘(하나가 남으면 버틴다), 성 3 은 다리 하나에 앞쪽 낮은 성벽이다. 다시 재 보니 첫 돌 함락 비율이 성 1·2 는 약 8%, 성 3 은 약 12% 가 됐다. 성 4 부터는 예전처럼 성벽과 본성이 있는 성이다.

참고
- Wikipedia, Trebuchet: https://en.wikipedia.org/wiki/Trebuchet
이 글과 관련된 게임
다른 글
- 체스 컴퓨터를 라이브러리 없이 360줄로 만들기 2026년 11월 2일
- 젤리 캐릭터는 뼈대 대신 모양 다섯 개로 움직인다 2026년 11월 1일
- 의사 3D(pseudo 3D) 방식으로 2D 캔버스에서 3D 레이싱 게임처럼 만들기 2026년 11월 1일
- 라이브러리 없이 3D 주사위를 굴려 원하는 면에서 멈추기 2026년 11월 1일
- 카드를 옮길 때마다 아이폰에서 판이 번쩍였다 2026년 11월 1일
