Astral Republic

✱ LABORATORY · 체험

거미줄은 어떻게 출렁이고 적을 가둘까 — Verlet 거미줄

점과 실 몇 개에 '거리를 지켜라'는 규칙만 반복하면, 진짜처럼 출렁이는 거미줄이 된다. 직접 줄을 치고 적을 가둬보자.

🔍 이게 뭐야?

X에서 본 작은 게임 프로토타입이 출발점이다. 거미가 실을 뽑아 거미줄을 치고, 빨간 점(적)에게서 도망치며, 줄을 넓혀 적을 유인해 가둔다. 적이 줄에 부딪히면 줄이 출렁이며 늘어나고, 거기에 얽히면 잡힌다.

신기한 건 이 출렁이는 거미줄에 사실 “물리 엔진” 같은 게 없다는 점이다. 점 몇 개“서로 거리를 지켜라”는 규칙 하나를 매 프레임 반복하는 게 전부다. 이걸 **Verlet 적분(Verlet integration)**이라고 부른다.

⚙️ 원리 — 위치만으로 움직인다

보통 물리는 속도를 저장하고 갱신한다. Verlet은 다르다. 속도를 저장하지 않고, “이전 위치”만 기억한다.

새 위치 = 현재 위치 + (현재 위치 − 이전 위치) + 가속도
                       └─── 이게 곧 속도 ───┘

현재 − 이전이 자동으로 속도 역할을 한다. 그래서 코드가 단순하고, 오일러 방식보다 잘 터지지 않는다(안정적). 거미줄·천·로프·머리카락이 다 이 방식이다.

거미줄은 두 단계의 반복일 뿐이다:

  1. 적분 — 모든 점을 Verlet으로 한 칸 움직인다(중력 살짝).
  2. 제약(Constraint) — 연결된 두 점의 거리가 원래 길이에서 어긋나면 서로 끌어/밀어 보정한다. 이걸 여러 번 반복할수록 줄이 빳빳해진다(= 강성).

바깥쪽 점들은 고정(앵커) 이라 안 움직이고, 그 사이 줄만 출렁인다.

▶️ 직접 해보기

● LIVE DEMO↑↓←→ 거미줄 타고 주행 · 클릭으로 실 발사
점수 0/50 · 실 5/10 · 0s · 적 0
HP
8/8

방향키 ↑↓←→ (또는 WASD)로 거미줄을 타고 주행. 클릭하면 그 방향으로 실을 쏴 새 줄을 잇는다 — 붙을 줄이 없으면 빈 곳에 앵커를 박는다(지금 탄 줄과 거의 평행한 방향만 실패). 적(빨강)은 줄에 걸리면 한동안 발버둥치다 죽는다 — 발버둥 중 거미에 닿으면 HP가 깎인다(노랑·주황 링 = 강한 적, 2~3 데미지). 죽어 회보라색이 된 적 위를 거미가 지나가면 회수돼 점수 +1, HP도 회복. HP 0이 되면 게임 오버. 커터가 매달린 줄을 모두 끊으면 거미가 추락해 게임 오버 — ↺로 다시 시작.

  • ↑↓←→ (또는 WASD): 거미(녹색)가 그 방향으로 거미줄을 타고 주행한다. 교차점에서는 진행 방향에 맞춰 부드럽게 다른 줄로 갈아탄다. 카메라는 거미를 따라간다.
  • 클릭: 거미가 그 방향으로 실을 쏜다. 그 방향에 붙을 수 있는 거미줄이 있으면 거기 잇고, 너무 평행해서 못 붙는 줄은 통과하며, 아무것도 없으면 일정 거리까지 뻗어 빈 곳에 앵커를 박는다. 실패는 지금 탄 줄과 거의 평행한 방향(±15°) 하나뿐.
  • 적(빨간 점) 은 거미 근처에서 점멸하며 생겨나 거미줄을 타고 다가온다. 줄에 걸리면 벗어나려 발버둥치며(빨강→흰) 서서히 죽는데, 그때 거미가 곁에 있으면 줄이 끌려와 거미에게 데미지를 준다. 죽어 회보라색이 된 적 위를 거미가 지나가면 회수돼 점수 +1, HP 회복. HP 0이 되면 거미가 주홍색으로 변하며 게임 오버.
  • 일부 적은 줄을 끊는 ‘커터’ 다. 커터가 거미가 매달린 줄을 모든 고정점에서 끊어버리면 거미가 지지할 데를 잃고 끝없이 추락 → 잠깐 떨어지는 모습을 보여준 뒤 추락사(게임 오버). ↺로 새 게임을 시작한다. 단, 떨어지는 동안에도 실이 남아 있다면 빈 곳에 쏴서 매달려 살아날 수 있다 — 거미다운 마지막 한 수.
  • 배경은 밤하늘 — 아주 가끔 유성(별똥별) 이 하늘을 가로지른다.

💡 슬라이더로 물리를 만져보자. 거미줄 강성을 올리면 줄이 팽팽해지고, 중력을 올리면 아래로 처진다. 걸림 힘은 적이 걸릴 때 줄이 흔들리는 세기, 복원력은 죽은 적이 매달린 줄이 다시 펴지는 속도다.

🛠 내 재현

핵심은 딱 두 함수다. 적분 후 제약을 강성번 반복한다.

// ① Verlet 적분 — 속도 = 현재-이전
for (const n of nodes) {
  if (n.pinned) continue;
  const vx = n.x - n.px, vy = n.y - n.py;
  n.px = n.x; n.py = n.y;
  n.x += vx;  n.y += vy + gravity;
}

// ② 거리 제약 — 어긋난 만큼 양쪽을 절반씩 당김 (여러 번 반복)
for (const e of edges) {
  const a = nodes[e.a], b = nodes[e.b];
  const dx = b.x-a.x, dy = b.y-a.y, d = Math.hypot(dx,dy);
  const diff = (d - e.rest) / d;      // rest = 원래 길이
  a.x += dx*0.5*diff;  a.y += dy*0.5*diff;
  b.x -= dx*0.5*diff;  b.y -= dy*0.5*diff;
}

적의 변형은 따로 만들 게 없었다. 적이 줄에 걸리면 그 지점에 노드를 끼워 넣고(줄을 둘로 쪼갬), 그 노드를 진행 방향으로 살짝 당기기만 하면, 다음 프레임의 거리 제약 보정이 알아서 흔들림과 복원을 만들어 준다. “밀고 → 당겨지고 → 다시 펴지는” 탄성이 공짜로 따라온다. 원본 영상을 프레임 단위로 뜯어보니, 적은 무게추처럼 천천히 줄을 당기지 발작적으로 튀지 않았다 — 그래서 걸릴 때 힘을 아주 작게(catchForce) 주고, 감쇠를 크게 해 부드럽게 잦아들게 했다.

걸림 → 죽음 → 회수의 3단계다. 걸린 적은 벗어나려 발버둥치며 서서히 죽고(빨강→흰), 그 사이 줄이 거미 쪽으로 끌려와 닿으면 거미가 데미지를 입는다. 죽으면 시체가 되어 매달린 채 남고, 거미가 지나가면 회수된다. 죽은 적이 매달린 줄은 그대로 꺾여 있으면 부자연스러우니, relaxForce로 양옆 중점 쪽으로 조금씩 당겨 서서히 편다 — 중력이 있으면 그만큼 처진 완만한 곡선에서 균형을 이룬다.

// 죽은 적이 매달린 줄을 서서히 일직선으로 편다(장력 복원).
const mid = midpoint(neighborA, neighborB);
node.x += (mid.x - node.x) * relaxForce;
node.y += (mid.y - node.y) * relaxForce;

실 발사는 거미에서 클릭 방향으로 광선을 쏴 처음 닿는 줄에 붙는다(빈 곳이면 일정 거리까지 뻗음). 거미의 이동은 줄(엣지) 위의 위치값 t(0~1)로 표현해, 끝에 닿으면 입력 방향에 맞는 다른 줄로 갈아탄다. facing을 실제 이동 방향으로 보간해 교차점에서 부드럽게 돈다.

📝 느낀 점

“속도를 저장하지 마라”는 한 줄 발상이 이렇게 큰 차이를 만든다는 게 인상적이었다. 복잡한 스프링 공식 없이, 거리를 지키라는 제약을 반복하는 것만으로 출렁이는 천과 거미줄이 나온다. 제약 반복 횟수 하나가 곧 재질의 빳빳함이 되는 것도 직관적이다.

원본은 Gemini로 빠르게 만든 게임 프로토타입이었는데, 그 안의 진짜 알맹이는 이 작은 물리 루프였다. 같은 엔진으로 망토, 깃발, 밧줄 다리, 머리카락을 전부 만들 수 있다 — 게임 속 출렁이는 천은 거의 다 이 사촌이다.