작은 행성계를 1/120초로 쪼개기: SLINGSTAR 중력 물리와 궤적 미리보기
SLINGSTAR의 물리 코드는 js/physics.js 한 파일, 2.5KB 남짓이다. 이 작은 파일이 혜성의 비행, 행성 착지, 코스 생성기의 시험 발사, 조준할 때 보이는 점선까지 전부 맡는다. 이 글에서는 중력 공식에 들어간 상수들이 왜 그 값인지, 적분 방식을 어떻게 골랐는지, 미리보기 점선이 실제 비행과 어긋나지 않는 구조를 정리한다.
필드 단위부터 정했다
물리는 화면 픽셀이 아니라 가로 100, 세로 160짜리 필드 좌표에서 돈다. 렌더러가 이 필드를 화면 크기에 맞춰 늘리거나 줄여서 그린다. 픽셀로 계산하면 화면이 큰 기기에서 행성 사이가 더 멀어지고, 같은 세기로 쏴도 결과가 달라진다. DAILY 코스를 모두가 같은 조건에서 치려면 물리가 화면 크기를 몰라야 했다.
이 좌표계에서 혜성 반지름은 1.6, 블랙홀 반지름은 4다. 혜성 중심이 블랙홀 중심에서 4.8(블랙홀 반지름에 혜성 반지름의 절반을 더한 값) 안으로 들어오면 들어간 것으로 친다. 혜성이 완전히 겹칠 때까지 기다리지 않고 절반쯤 걸치면 인정하는 셈인데, 플레이어 눈에는 그쯤에서 이미 "들어갔다"로 보이기 때문이다.
중력 공식과 상수 세 개
행성 하나가 혜성에 주는 가속도는 다음과 같이 계산한다. 실제 소스에서 그대로 옮겼다.
const mass=p=>(p.r*p.r)/64*(p.kind==='repulsor'?-1:1);
// accel() 안쪽
const d2=dx*dx+dy*dy; const d=Math.sqrt(d2);
const a=K*mass(p)/(d2+SOFT); ax+=a*dx/d; ay+=a*dy/d;
K는 15000, SOFT는 9다. 첫째로 질량을 반지름의 제곱에 비례시켰다. 2D 원의 넓이에 비례하는 셈이라, 화면에서 큰 행성이 그만큼 무겁다는 직관과 맞는다. 64로 나눈 것은 반지름 8짜리 행성의 질량이 정확히 1이 되도록 맞춘 정규화다. 행성 반지름은 6~13 사이에서 뽑으므로 질량은 0.56에서 2.64까지 다섯 배 가까이 차이 난다.
둘째로 거리 제곱에 9를 더하는 완화항(softening)을 넣었다. 거리가 0에 가까워지면 가속도가 무한대로 튀는 것을 막는 장치다. 다만 SLINGSTAR에서는 혜성이 행성 표면에 닿는 순간 착지 처리를 하므로, 실제로 혜성이 행성 중심에 가장 가까이 가는 거리는 반지름 6짜리 행성 기준 7.6이다. 이때 거리 제곱은 57.8이니 9를 더하면 힘이 13%쯤 약해진다. 큰 행성일수록 이 비율은 더 작아진다. 결국 완화항은 표면 근처를 약간 무디게 만드는 정도이고, 주된 역할은 혹시 모를 0 나누기를 막는 안전장치다.
반발 행성은 질량에 −1을 곱하는 것으로 끝이다. 같은 공식, 같은 범위에서 방향만 반대가 된다. 코드에 분기가 하나 늘었을 뿐 물리 코드 전체에서 반발 행성을 따로 다루는 곳은 없다.
크기가 달라도 표면 근처 힘은 비슷하다
이 공식에는 눈여겨볼 성질이 하나 있다. 질량이 반지름 제곱에 비례하고 힘은 거리 제곱에 반비례하므로, 행성 반지름의 몇 배 거리라는 식으로 재면 행성 크기가 거의 상쇄된다. 직접 계산해 보면 다음과 같다.
| 행성 반지름 | 질량 | 표면에서 가속도 | 반지름 2.2배 거리에서 |
|---|---|---|---|
| 6 | 0.56 | 126.4 | 46.0 |
| 8 | 1.00 | 148.3 | 47.1 |
| 13 | 2.64 | 178.3 | 47.9 |
반지름 2.2배 지점에서는 작은 행성이나 큰 행성이나 가속도가 47 안팎으로 같다. 큰 행성이 더 세게 당기는 게 아니라 더 넓은 범위를 당긴다는 뜻이다. 이 성질 덕분에 행성 둘레에 그리는 보라색 글로(반지름 2.2배)와 점선 영향 링(2.6배)이 크기와 상관없이 "여기부터 휜다"는 같은 의미를 갖는다. 점수 쪽의 중력 어시스트 보너스(+200)도 날아가는 동안 행성 반지름 2.2배 안쪽을 지났는지로 판정하는데, 같은 이유로 행성 크기마다 문턱을 따로 둘 필요가 없었다.
블랙홀도 조금 당긴다
블랙홀은 판정만 있는 구멍으로 두지 않고 질량 0.12짜리 약한 인력원으로 만들었다. 반지름 3쯤 되는 아주 작은 행성과 맞먹는 힘이다. 들어가기 직전(4.8 거리)에서 가속도가 56, 거리 15에서는 7.7로 빠르게 줄어든다. 빠르게 스치는 혜성에는 거의 영향이 없고, 블랙홀 옆을 느리게 지나가는 혜성만 말려 들어간다. 아슬아슬하게 빗나간 샷이 빨려 들어가는 장면을 만들려는 장치다. 반대로 이 힘을 키우면 대충 쏴도 들어가는 발사가 늘어나고, 코스 생성기는 들어가는 발사가 너무 많은 홀을 버리므로 쓸 수 있는 배치가 줄어든다. 행성 질량 범위(0.56~2.64)의 한참 아래인 0.12로 둔 이유다.
적분: 반암시적 오일러, dt = 1/120
매 단계는 가속도로 속도를 먼저 갱신하고, 마찰을 곱한 뒤, 새 속도로 위치를 옮긴다. 순서만 보면 사소하지만 결과는 크게 다르다. 옛 속도로 위치를 먼저 옮기는 일반 오일러 방식은 중력 궤도에서 에너지가 조금씩 늘어나, 행성을 도는 혜성이 점점 바깥으로 풀려 나가는 나선이 생긴다. 속도를 먼저 갱신하는 반암시적(symplectic) 오일러는 에너지가 한쪽으로 새지 않아서 같은 간격으로도 궤도가 안정적이다. 계산량은 똑같으니 고르지 않을 이유가 없었다.
시간 간격은 1/120초로 고정했다. 60fps 화면이면 한 프레임에 두 단계를 돈다. 게임 루프는 지난 프레임에서 흐른 시간을 누적했다가 1/120초씩 잘라 쓰고, 탭을 전환했다 돌아오는 경우를 대비해 한 프레임의 시간은 최대 0.05초(6단계)로 자른다. 화면이 몇 fps로 돌든 물리 한 단계의 크기는 언제나 같다.
마찰은 매 단계 속도에 (1 − 0.12·dt)를 곱한다. 1초면 약 11.3%가 줄어든다. 우주에 마찰이 왜 있느냐고 물을 수 있는데, 없으면 행성 주위를 영원히 도는 궤도가 생겨 샷이 끝나지 않는다. 그래서 샷이 끝나는 조건을 다섯 가지로 정했다. 블랙홀 진입, 행성 착지, 필드 이탈, 0.4초가 지난 뒤 속도 3 미만으로 정지, 15초 경과. 판정 순서도 이 순서다. 블랙홀과 행성 표면이 동시에 닿는 드문 경우라면 플레이어에게 유리한 쪽인 진입을 먼저 본다.
착지: 튕기지 않고 붙는다
혜성 중심이 행성 표면에서 혜성 반지름 안쪽으로 들어오면, 행성 중심에서 혜성 쪽으로 향하는 방향을 따라 표면 바로 바깥으로 위치를 옮기고 속도를 0으로 만든다. 튕겨 나가게 하는 방법도 있었지만 그러면 한 샷의 결과가 반사각에 크게 좌우되어 예측이 어려워진다. 착지로 처리하면 행성이 "다음 샷의 발판"이 되고, 한 번에 넣기 어려운 홀을 두 번에 나눠 공략하는 길이 생긴다. 설계 문서에서 이 게임의 독자 요소로 꼽은 것도 이 착지 규칙이다.
점선이 실제 궤적과 똑같은 이유
조준할 때 보이는 점선은 근사 계산이 아니다. 손가락을 움직일 때마다 PHYS.simulate로 실제 샷을 72단계(0.6초) 미리 돌려 보고, 4단계마다 위치를 찍어 점 18개로 그린다. 발사하면 게임 루프는 같은 혜성 상태에서 같은 PHYS.step을 같은 1/120초 간격으로 부른다. 누적기는 한 프레임에 몇 단계를 돌지만 정할 뿐 단계의 크기를 바꾸지 않으므로, 실제 비행의 처음 0.6초는 점선과 비트 단위로 같다. 미리보기용 근사 함수를 따로 두면 언젠가 둘이 어긋나고, 플레이어는 그 어긋남을 "점선이 거짓말한다"로 받아들인다. 함수를 하나만 두는 것이 가장 확실한 방법이었다.
미리보기 길이는 1.0.0에서 1.5초였다가 1.1.0에서 0.6초로 줄였다. 변경 이력에는 "끝까지 보여 주지 않음"이라고만 적었는데, 1.5초 분량이면 점선이 블랙홀 근처까지 닿는 경우가 생겨 점선을 블랙홀에 맞추고 놓기만 하면 되는 홀이 나올 수 있었다. 지금은 첫 번째 꺾임까지만 보여 주고 그 뒤는 행성 배치를 보고 짐작하게 했다. 미리보기가 정확하다는 사실은 그대로 두고 길이로만 난이도를 조절한 것이다.

정리
SLINGSTAR의 물리는 가속도 공식 하나, 단계 함수 하나, 시뮬레이션 함수 하나로 이뤄져 있고 게임 플레이, 조준 점선, 코스 생성기가 이 셋을 같이 쓴다. 상수 몇 개를 고를 때 기준은 "보이는 것과 계산이 같은 뜻을 갖는가"였다. 행성 크기와 영향 범위, 점선과 실제 비행, 블랙홀의 모습과 끌어당기는 범위가 서로 어긋나지 않게 맞췄다. 설계 문서의 로드맵에 있는 움직이는 행성과 웜홀은 아직 구현하지 않았다. 움직이는 행성을 넣으면 미리보기와 생성기 모두 시간에 따라 바뀌는 행성 위치를 다뤄야 하므로, 지금의 "함수 하나 공유" 구조를 유지할 수 있는지부터 따져 볼 생각이다.