Starship Mission 의 15단계 Phase 시스템 — 복잡한 비행 흐름을 어떻게 관리했나
스타쉽 미션 시뮬레이션을 만들면서 phase 15개를 다룬 경험. 상태 머신 설계, P-controller 기반 오토파일럿 착륙, Mechazilla Catch 판정까지.
시작 — Phase 가 15개쯤 되니까...
스페이스X 의 스타쉽 미션을 간략 재현하는 시뮬레이션을 만들고 있었습니다. 처음엔 state === 'flying' 이면 된다고 생각했는데, 실제 미션 흐름을 따라가 보니 phase 가 끝없이 늘어났습니다.
pre_launch → countdown → launch → ascent → sep_prompt (분리 대기)
→ separation → boostback → boost_coast → reentry → landing_burn
→ catch_anim → result
거기에 2단 로켓 궤도 진입·착륙까지 포함하면 15단계. 단순 boolean 이나 enum 하나로는 관리가 어려웠습니다.
이 포스트는 그 복잡도를 어떻게 정리했는지, 특히 오토파일럿 착륙 제어 와 Mechazilla 캐치 판정 같은 까다로운 부분을 어떻게 풀었는지 남깁니다.
상태 머신 — phase 전환 규칙
단순 enum 대신 "어느 phase 에서 어느 phase 로 갈 수 있는지" 를 명시적으로 정의했습니다.
type Phase =
| 'pre_launch'
| 'countdown'
| 'launch'
| 'ascent'
| 'separation'
| 'boostback'
| 'boost_coast'
| 'reentry'
| 'landing_burn'
| 'catch_anim'
| 'result'
| 'failed';
// 각 phase 에서 전환 가능한 다음 phase 집합
const TRANSITIONS: Record<Phase, Phase[]> = {
pre_launch: ['countdown'],
countdown: ['launch', 'pre_launch'],
launch: ['ascent', 'failed'],
ascent: ['separation', 'failed'],
separation: ['boostback'],
boostback: ['boost_coast', 'failed'],
// ...
};
잘못된 전환은 런타임에서 차단. 예를 들어 ascent 에서 바로 catch_anim 으로 뛰어넘는 버그를 차단합니다.
phase timer
각 phase 는 자체 frame counter (phaseTimer) 를 가집니다. phase 전환 시 이 값이 0으로 리셋되며, "이 phase 에 머문 시간" 을 기준으로 애니메이션·사운드·판정을 트리거.
// 매 프레임
state.phaseTimer += 1;
switch (state.phase) {
case 'landing_burn':
if (state.phaseTimer > 45) {
// 45 프레임 이후 MECO 페이드
state.thrustLevel = Math.max(0, 1 - state.phaseTimer / 45);
}
break;
}
오토파일럿 착륙 — P-controller
가장 까다로웠던 부분. 부스터를 천천히 안전하게 착륙시키려면 지상에 가까워질수록 추력을 정교하게 조절 해야 합니다. 실제 스페이스X 는 복잡한 제어기를 쓰지만, 게임에서는 P-controller (Proportional controller) 로 충분했습니다.
목표 속도 곡선
고도에 따라 "이 속도 이하여야 안전" 한 목표 속도를 정의:
function getTargetVelocity(altM: number): number {
// 200m 이상: 완만한 하강
// 15m~200m: 선형 감속
// 15m 이하: 거의 정지
if (altM > 200) return -10; // 초당 10m 하강 OK
if (altM > 15) {
return -(2 + ((altM - 15) / 185) * 8); // -2 ~ -10 m/s
}
return -2; // 15m 이하: 2m/s 까지 줄이기
}
P-controller 로 추력 결정
현재 속도와 목표 속도의 차이에 비례해서 추력 조절:
const targetVel = getTargetVelocity(state.altM);
const velError = state.velMS - targetVel; // + = 너무 빠름, - = 너무 느림
// hover throttle 근처로 기본값 잡고, 오차에 비례해 조정
const hoverT = 0.93;
let throttle = hoverT + velError * 0.015;
throttle = Math.max(0, Math.min(1, throttle));
state.thrustLevel = throttle;
hover throttle 0.93 은 "딱 중력과 균형" 인 값. 이 값보다 살짝 높이면 감속하고, 낮추면 낙하. P-gain 0.015 는 튜닝으로 찾은 값.
static hoverT*0.93 금지
초기엔 state.thrustLevel = hoverT * 0.93 같이 고정값으로 착륙시켰는데, 상황에 따라 너무 빠르거나 너무 느리게 내려왔습니다. 동적 P-controller 로 바꾼 뒤에야 부드럽고 안정적인 착륙이 가능해졌습니다.
Mechazilla Catch 판정
스타쉽의 상징적 기동 — 착륙 타워의 "젓가락" 팔이 부스터를 잡아내는 Catch. 이 판정을 어떻게 구현할지 고민이었습니다.
고스트 가이드 렌더링
타워 암 위치에 반투명 "이상적 정렬" 고스트를 표시해 사용자 시선을 유도:
// 고스트 렌더 특징
ctx.strokeStyle = 'rgba(255, 255, 255, 0.55)';
ctx.setLineDash([6, 4]);
ctx.lineWidth = 3;
// 반투명 채움까지 추가해 시각적 "타겟" 느낌
자동 캐치 조건
Catch 판정이 성립하는 3조건 전부 충족해야 성공:
if (
state.altM <= 15 && // 낮은 고도
catchScore >= 0.8 && // 정렬 점수 0.8 이상
Math.abs(state.velMS) <= 8 // 저속 하강
) {
transitionPhase('catch_anim');
}
catchScore 는 부스터와 타워 암 사이의 수평 거리·각도 정렬을 조합한 정규화된 값.
terminal state
Catch 성공 후 m2_result phase 에 catchProgress 값을 전달. result phase 는 terminal state 로 자동 전환되지 않게 설계 — 사용자가 "Retry" / "Assembly" 버튼으로 명시적으로 빠져나가도록.
실패 분류
한 가지 더 중요했던 것 — 실패 원인을 구분해서 UX 피드백. 단순 "실패" 가 아니라 왜 실패했는지:
structural_failure— MAX-Q 초과 (5km 이상 고도에서)sep_timeout— 80km 초과까지 분리 미수행fuel_exhausted— 연료 소진impact— 지면 충돌 (속도 과대)g_force— 15G 초과 (구조 한계)
각 실패에 대응하는 힌트 텍스트를 result 화면에 표시. 사용자가 무엇을 개선할지 단서를 얻게 합니다.
연쇄 사이드 이펙트 방지
phase 기반 게임에서 흔한 버그 중 하나는 phase 전환 시 필요한 값 일부만 업데이트 하는 것입니다. 이 프로젝트의 비행 시뮬에서는 8곳 동기화 체크리스트를 만들었습니다:
- 타입/상수 정의
- State 필드 추가
createInitialState()- Physics step
- Failure check
- Score calc
- HUD draw
- Result draw
신규 물리 요소 하나 추가할 때마다 위 8곳을 모두 돌봐야 합니다. 빠뜨리면 런타임 NaN 이나 undefined 가 튀어나오는 식.
⚠️ 특히 주의: multi-phase 물리 요소는 모든 해당 phase 에 적용 필수. 예를 들어 부스터 연료 소모 로직을
ascent에만 넣고launch에 안 넣으면, launch 단계에서 연료가 안 줄어드는 버그.
회고
15단계 phase 를 관리하는 건 처음엔 overkill 처럼 보였습니다. "그냥 flying / landing / done 세 개면 되지 않나?" 싶었지만, 실제 비행 흐름을 정확히 표현하려면 각 단계별로 다른 물리·오디오·카메라·UI 가 필요했습니다.
P-controller 같은 제어 이론의 기초도 배울 수 있었던 점이 보너스. 수식 한 줄이 수십 분의 실험을 대체해줬습니다. "게임에서 물리 시뮬 같은 걸 만들 때 제어 이론의 힌트를 빌려오면 꽤 많은 문제가 자연스럽게 풀린다" 는 것을 느낀 프로젝트였습니다.
복잡한 시퀀스 게임을 만드신다면, 명시적 상태 머신 + phase timer + 전환 규칙 세 가지 축을 처음부터 도입하시길 권합니다. 나중에 확장하기가 훨씬 편합니다.
방명록
이 글에 대한 한 줄을 남겨주세요
불러오는 중...