포스트

모노레포에 처음 들인 외부 라이브러리, 물리 엔진 planck — 성 무너뜨리기를 만들며 정한 것들

미니게임 40여 개를 외부 런타임 라이브러리 없이 만들어 오다가, 성 무너뜨리기에서 처음으로 물리 엔진 planck(Box2D 포팅)를 들였습니다. 들이기 전에 노드에서 잰 다섯 가지, 화면과 떼어 놓은 물리 모듈, 충격을 쌓아 블록을 부수는 방식, 콜백 안에서 세계를 바꾸지 않는 규칙, 7층 탑이 혼자 흔들리던 문제까지 정리했습니다

모노레포에 처음 들인 외부 라이브러리, 물리 엔진 planck — 성 무너뜨리기를 만들며 정한 것들

10월 12일 구글 플레이에 성 무너뜨리기를 출시했고, 이름을 파괴왕 공성전으로 바꿔 10월 14일 토스에도 냈습니다. 대포를 당겨 쏴서 성을 무너뜨리고, 성을 지키는 기사를 쓰러뜨리는 물리 퍼즐입니다.

이 게임에는 지금까지 만든 미니게임과 다른 점이 하나 있습니다. 처음으로 외부 런타임 라이브러리를 들였습니다. 물리 엔진 planck입니다.

왜 지금까지는 안 썼나

지금까지 만든 미니게임은 40개가 넘지만, 게임이 돌아갈 때 쓰는 코드는 전부 직접 짰습니다. 공을 튀기는 벽돌깨기도, 궤적을 그리는 버블 팝도, 머지 팝의 원형 물리도 그렇습니다. 이유는 세 가지였습니다.

  • 번들이 작아야 합니다. 토스는 첫 로딩이 느리면 검수에서 반려합니다.
  • 의존성 하나가 앱 수십 개에 퍼집니다. 공용 패키지에 넣으면 모든 앱이 그 라이브러리의 버전과 버그를 같이 갖게 됩니다.
  • 필요한 만큼은 직접 짤 수 있었습니다. 공 하나, 원 몇 개 정도의 물리는 수백 줄이면 됩니다.

성 무너뜨리기는 달랐습니다. 블록 수십 개가 쌓이고, 기울고, 서로 밀고, 무너져야 합니다. 쌓인 물체가 가만히 서 있게 만드는 것부터가 물리 엔진의 가장 어려운 부분이라, 이건 직접 짜서는 안 되겠다고 판단했습니다.

planck를 고른 이유

planck는 2D 물리 엔진 Box2D를 자바스크립트로 옮긴 것이고 MIT 라이선스입니다. Box2D는 수많은 물리 퍼즐 게임이 써 온 엔진이라, 쌓기·무너뜨리기에 필요한 기능이 다 있습니다.

버전은 1.5.0으로 정확히 고정했습니다. 이 게임 패키지에만 넣고 공용 패키지에는 넣지 않았습니다. 다른 게임은 지금처럼 planck 없이 빌드됩니다.

들이기 전에 노드에서 잰 다섯 가지

코드를 쓰기 전에, 브라우저 없이 노드에서 planck만 돌려 보며 다섯 가지를 확인했습니다.

  1. 쌓은 탑이 서 있는가. 상자 14단 탑이 10초 뒤 완전히 멈추고 ‘잠들었습니다’(엔진이 계산을 쉬는 상태). 기둥 위에 보를 얹은 탁자 모양도 안정적이었습니다.
  2. 충분히 빠른가. 600스텝(10초 분량)이 56ms 걸렸습니다. 폰에서도 여유가 있습니다.
  3. 같은 입력이면 같은 결과인가(결정론). 같은 레벨에 같은 각도·같은 힘으로 쏘면 매번 똑같이 무너졌습니다. 이게 중요한 이유는 아래에 적었습니다.
  4. 빠른 공이 얇은 판자를 뚫지 않는가. 초속 70m 공이 두께 10cm 판자를 통과하지 않고 밀어냈습니다. 빠른 물체를 따로 처리하는 기능(연속 충돌 감지)이 제대로 돕니다.
  5. 충돌의 세기를 받을 수 있는가. 부딪힐 때마다 얼마나 세게 부딪혔는지(충격량)를 콜백으로 받을 수 있었습니다. 블록을 부수려면 이게 필요합니다.

다섯 가지가 다 통과하고 나서 설계에 들어갔습니다.

물리는 화면과 떼어 놓는다

물리 모듈은 화면(DOM)을 전혀 모르게 만들었습니다. 그래서 브라우저 없이 노드에서 그대로 불러 테스트할 수 있습니다.

여기에 결정론이 더해지면 쓸모가 커집니다. 레벨이 풀리는지 프로그램이 직접 확인할 수 있습니다. 해답 찾는 프로그램이 각도와 힘을 바꿔 가며 쏴 보고, 몇 발로 깰 수 있는지 셉니다. 결정론이 없으면 같은 해답이 실제 게임에서는 안 통할 수 있으니 이 검증은 의미가 없어집니다.

그래서 지킬 것을 정했습니다.

  • 시간은 1/60초 고정 스텝으로만 흐릅니다. 화면 프레임이 느려도 물리는 같은 간격으로 계산합니다.
  • 물체는 늘 같은 순서로 만듭니다. 바닥 → 고정물 → 블록 → 시소 → 기사.
  • 물리 안에서 Math.random을 쓰지 않습니다. 부서진 조각이 튀는 방향도 정해진 패턴입니다.
  • 슬로모션이나 타격감을 위한 멈춤은 그리기 쪽에서만 합니다. 물리 시간은 건드리지 않습니다.

단위는 Box2D가 권하는 대로 미터와 초로 했습니다. 블록은 0.5~2m, 대포알은 반지름 0.3m입니다. 화면 픽셀로는 카메라가 바꿔 그립니다.

블록은 ‘쌓인 충격’으로 부서진다

Box2D에는 ‘부서짐’이 없습니다. 물체는 부딪히고 튕길 뿐입니다. 그래서 부서지는 규칙은 직접 만들었습니다.

  • 블록마다 체력이 있습니다. 체력은 재료 계수 × 질량입니다. 나무, 얼음, 돌, 황금은 계수가 다르고, 얼음이 가장 약하고 돌이 가장 단단합니다.
  • 부딪힐 때마다 충격량만큼 체력이 깎이고, 0이 되면 부서집니다. 한 방에 부서지지 않아도 여러 번 맞으면 부서집니다.
  • 살짝 닿은 건 세지 않습니다. 서로 다가오는 속도가 초속 1.5m보다 느리거나 충격이 아주 작으면 무시합니다. 이게 없으면 그냥 쌓여 있는 블록끼리도 조금씩 체력이 깎여 혼자 무너집니다.
  • 공격하는 쪽에 따라 배수가 다릅니다. 보통 대포알은 돌에 피해를 주지 못하고, 철구와 폭발만 돌을 부술 수 있습니다.

수치는 전부 실제로 쏴 보며 정했습니다. 예를 들어 나무 기둥은 초속 14m 대포알에 부서지고 12~13m에는 버팁니다.

콜백 안에서는 세계를 바꾸지 않는다

가장 먼저 정한 규칙이고, 가장 중요한 규칙입니다.

planck는 계산 도중에 “이 둘이 부딪혔다”를 콜백으로 알려 줍니다. 여기서 바로 블록을 지우거나 폭발을 일으키면 엔진이 계산하던 목록이 중간에 바뀌어 이상하게 동작합니다. 그래서 콜백에서는 기록만 하고, 한 스텝 계산이 다 끝난 뒤에 모아서 처리합니다. 처리 순서도 물체 번호 순으로 고정해서 결정론을 지켰습니다.

7층 탑이 혼자 흔들렸다

레벨을 불러오면 블록이 아주 조금씩 겹치거나 떠 있습니다. 그대로 시작하면 첫 몇 초 동안 탑이 들썩입니다. 그래서 레벨을 불러올 때 피해 계산을 끈 채로 80스텝을 미리 돌려 자리를 잡게 하고, 그 뒤 속도를 0으로 만들었습니다.

그런데 7층 탑이 그 뒤에도 5초 동안 흔들리며 잠들지 않았습니다. 80스텝 끝에 한 번만 속도를 0으로 만들면, 그 순간 탑이 기울어 있던 상태가 그대로 남아 다시 흔들리기 시작한 것입니다. 20스텝마다 속도를 0으로 되돌리게 바꾸자 흔들림이 10분의 1 아래로 줄고 17스텝 만에 잠들었습니다.

층 사이에 1cm 간격을 자동으로 넣는 것도 같은 이유입니다. 간격이 그보다 좁으면 엔진이 블록끼리 밀어내느라 오히려 더 흔들렸습니다.

그래서 얼마나 커졌나

planck를 넣고 토스판으로 빌드한 자바스크립트는 처음 판에서 약 360KB, 가로 화면과 60레벨로 키운 1.1.0에서 약 450KB입니다. 설계 때 정한 목표 500KB 안에 들어왔습니다. 이미지 파일은 하나도 없고, 블록 무늬와 성은 전부 코드로 그립니다. 테스트는 90개가 넘고, 물리·카메라·레벨 검사가 모두 노드에서 돕니다.

잘못 정한 것 하나

물리는 잘 됐는데 구도를 잘못 잡았습니다. 원래 원했던 건 세로로 높은 성이 화면을 채우고 와르르 무너지는 느낌이었는데, 만들고 보니 멀리 있는 성을 향해 포물선으로 던지는 구도가 됐습니다. 지금 앱은 이 구도로 계속 다듬고, 세로 성채 버전은 물리 엔진을 그대로 재사용해 별도 앱으로 만들기로 했습니다. 엔진을 화면과 떼어 놓은 덕분에 이런 재사용이 쉽습니다.

정리

  • 외부 라이브러리는 필요한 이유가 분명할 때만, 그리고 그 게임에만 넣습니다. 버전은 정확히 고정합니다.
  • 들이기 전에 노드에서 핵심 동작부터 잽니다. 쌓기, 속도, 결정론, 빠른 물체, 충격량.
  • 물리를 화면과 떼어 놓고 결정론을 지키면 레벨 검증을 프로그램에 맡길 수 있습니다.
  • 콜백 안에서는 기록만 하고, 처리는 스텝 밖에서 정해진 순서로.
  • 살짝 닿은 것까지 피해로 세면, 쌓인 성이 혼자 무너집니다.

다음에 이 엔진을 쓸 곳은 세로 성채 버전입니다. 그때 무엇을 그대로 가져왔고 무엇을 새로 만들었는지 다시 적겠습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.