지하철 문이 열리는 순간, 등 뒤에서 밀려드는 인파가 하나의 물결처럼 느껴진 적이 있다면 이 게임의 출발점을 이미 아는 셈이다. ‘지옥철’은 서울 출근길을 뱀서라이크(Vampire Survivors류) 실시간 생존 장르로 풍자하는 브라우저 로그라이크다. 플레이어의 목표는 단 하나, 몰려오는 인파를 헤치고 오전 9시까지 회사에 도착하는 것이다. 무기는 총도 칼도 아닌 ‘헛기침’이고, 체력바 대신 멘탈 게이지가 바닥나면 그 자리에서 무너진다.

지옥철 시작 화면, 출근 시계와 멘탈 HUD, 헛기침 조준 프리뷰

왜 카드 덱빌딩을 버리고 뱀서라이크로 갈아탔나

처음 기획서에 적혀 있던 ‘지옥철’은 지금과 완전히 다른 게임이었다. 턴제 카드 덱빌딩, 그러니까 한 턴에 손패를 내고 다음 칸으로 이동하는 슬레이 더 스파이어 계열이었다. 출근이라는 소재와 카드의 ‘선택과 자원 관리’가 잘 붙을 거라는 가설이었지만, 초기 플레이테스트에서 치명적인 위화감이 드러났다. 출근길의 핵심 감각은 ‘차분히 손패를 고르는 것’이 아니라 ‘숨 돌릴 틈 없이 밀려나는 것’이었다. 턴이 끊기는 순간 그 압박감이 통째로 증발했다.

그래서 카드 시스템을 통째로 폐기했다. 대신 화면 사방에서 인파가 실시간으로 쏟아지고, 플레이어는 멈추지 않고 움직이며 자동으로 나가는 헛기침으로 길을 여는 뱀서라이크 실시간 액션으로 피벗했다. 결정을 내리는 단위가 ‘턴’에서 ‘초’로 바뀌자, 소재가 요구하던 압박감이 비로소 게임 메커닉과 맞아떨어졌다. 기획에서 ‘재미’의 최종 판단은 언제나 이 지점에서 갈린다. 문서상 그럴듯한 조합보다, 손으로 만져봤을 때 소재의 감각이 살아나는지가 기준이다.

Godot 프로젝트를 index.html 한 장으로 갈아엎은 이유

기술 스택도 한 번 크게 뒤집혔다. 원래는 Godot 4와 GDScript로 시작했다. 그런데 이 게임은 외부 이미지 에셋이 사실상 필요 없는 구조였다. 캐릭터도, 인파도, 아이템도 전부 이모지 스프라이트와 캔버스 연출로 표현하기로 했기 때문이다. 외부 이미지 에셋은 결국 0개였다. 무거운 엔진 런타임과 에디터, 익스포트 파이프라인을 짊어질 이유가 옅어진 것이다.

결론은 순수 index.html 한 장, vanilla JS, Canvas 2D로의 전면 전환이었다. 지금 게임 전체가 1,691줄의 코드 한 뭉치로 돌아간다. 빌드 단계가 없으니 파일을 브라우저에 던지면 그대로 실행되고, 배포는 정적 호스팅에 파일 하나 올리는 것으로 끝난다. 로그라이크 코어 루프에 필요한 것은 화려한 엔진 기능이 아니라 매 프레임 안정적으로 도는 게임 루프와 렌더 한 줄이었고, Canvas 2D는 그 요구에 정확히 맞았다.

아트 방향도 이 전환과 한 몸이다. 플레이어는 넥타이 이모지, 몰려드는 인파는 사람 이모지, 헛기침은 물방울 이모지처럼, 화면 위 모든 개체가 유니코드 문자 하나에서 출발한다. 여기에 흔들림, 밀려남, 잔상 같은 캔버스 연출을 얹어 ‘살아 움직이는’ 느낌을 만든다. 이미지 파일을 그리지 않으니 다운로드할 것도, 로딩 스피너도 없다. 브라우저 탭을 여는 즉시 게임이 손에 잡히는 것, 그 즉시성이 애초에 이 스택을 고른 이유이기도 하다.

아래는 M1에서 완성한 코어 루프의 뼈대다. 이 순환이 매 프레임 반복되면서 인파가 몰려오고, 헛기침이 나가고, 경험치가 쌓인다.

지옥철 게임 흐름도

밀려드는 인파 속에서 60fps는 어떻게 지켰나

뱀서라이크의 생명은 화면을 가득 채우는 적의 물량이다. 그런데 브라우저에서 수십 개의 개체를 매 프레임 그리면서 60fps를 유지하는 것은 만만치 않다. 여기서 세 가지 최적화가 결정적이었다.

첫째, 이모지 렌더링이다. 캔버스에서 이모지를 그릴 때 가장 흔한 실수가 매 프레임 fillText를 호출하는 것이다. 텍스트 렌더링은 생각보다 비싸서, 개체가 늘어나면 프레임이 뚝뚝 떨어진다. 그래서 각 이모지를 게임 시작 시 오프스크린 캔버스에 딱 한 번 그려 스프라이트로 캐싱해 두고, 이후에는 drawImage로 그 비트맵을 복사만 한다. 매 프레임 폰트 래스터라이저를 부르는 대신, 이미 구워둔 그림을 옮기는 셈이다.

둘째, 오브젝트 풀링이다. 인파와 헛기침 투사체는 끊임없이 생성되고 사라진다. 이걸 매번 new로 만들고 버리면 가비지 컬렉터가 주기적으로 개입해 프레임을 갉아먹는 GC 스파이크가 생긴다. 그래서 개체를 미리 만들어 풀에 담아두고, 필요할 때 꺼내 쓰고 죽으면 반납하는 방식으로 바꿨다. 할당과 해제가 사라지자 프레임 그래프의 톱니 모양 스파이크도 함께 사라졌다.

셋째, 충돌 판정이다. 모든 인파와 모든 투사체를 짝지어 검사하면 개체 수의 제곱, 즉 O(n²)로 비용이 폭증한다. 대신 화면을 격자 셀로 나누는 스페이셜 해시를 도입해, 한 개체는 자기가 속한 셀과 근접 셀만 검사한다. 화면 반대편에 있는 인파와는 애초에 비교조차 하지 않는다. 이 세 가지를 합친 결과가 동시 개체 15기 기준 60fps, 콘솔 에러 0이라는 검증 수치다.

지옥철 게임 흐름도 2

지옥철 실시간 전투, 인파를 헛기침으로 밀어내며 콤보 x6

9시까지 도착, 시계를 밸런스의 축으로 삼다

M1이 ‘손맛 나는 코어’였다면, 지금 진행 중인 M2는 그 위에 ‘출근’이라는 서사를 얹는 런 구조 단계다. 스테이지는 6개 구간으로 나뉘고, 게임 내 시계가 오전 7시 30분에서 9시를 향해 흐른다. 이 시계가 단순한 장식이 아니라 밸런스의 중심축이라는 점이 이 게임의 특징이다.

플레이어에게는 급행과 완행이라는 선택지가 주어진다. 시계는 코드 상수로 설계했는데, 완행만 타면 9시 4분에 도착해 지각하고, 급행을 두 번 타면 8시 52분에 도착해 세이프하다. 즉 ‘빠른 경로는 더 위험하고 붐비지만 시간을 번다’는 리스크와 보상의 축이 시계 하나로 성립한다. 여기에 우산이나 노이즈 캔슬링 헤드폰 같은 환경 아이템, 급행과 완행에 따라 다른 혼잡도를 얹으면서 매 판이 다른 출근길이 되도록 만드는 중이다. 상단 HUD에는 현재 시각과 남은 구간, 멘탈 상태가 한눈에 들어온다.

레벨업 설계도 뱀서라이크 문법을 따른다. 경험치가 차면 3개의 강화 옵션 중 하나를 고르는 3택 방식이다. 헛기침의 범위를 넓힐지, 이동 속도를 올릴지, 멘탈 회복을 챙길지를 매번 선택하면서 한 판 안에서 캐릭터가 성장한다. M3부터 M5까지는 아직 착수 전이라, 지금 공개하는 건 코어가 확실히 돌아가는 뼈대와 그 위에 얹히기 시작한 런 구조까지다.

환경 아이템도 시계와 짝을 이룬다. 우산은 비 오는 구간에서 시야와 이동을 지켜주는 방패 역할이고, 노이즈 캔슬링 헤드폰은 멘탈이 깎이는 속도를 늦춰준다. 급행이 붐비는 대신 시간을 벌어주는 것처럼, 아이템 하나하나가 ‘무엇을 포기하고 무엇을 얻을지’라는 선택을 만든다. 이 선택들이 겹치면서 완행 위주의 안전한 지각 회피 빌드와 급행 돌파형 고위험 빌드가 갈리고, 같은 6구간이라도 매 판의 출근길이 달라진다.

무엇을 넣지 않기로 했는가

풍자 게임에서 가장 조심해야 하는 건 웃음의 방향이다. 출근길이라는 소재는 자칫 사회적 약자를 조롱의 대상으로 삼기 쉽다. ‘지옥철’은 여기서 분명한 선을 그었다. 임산부, 휠체어 이용자, 노숙인 같은 존재는 절대 적이나 조롱의 대상이 되지 않는다.

대신 이들은 상황과 환경 요소로만 등장한다. 예를 들어 임산부석은 잠깐 멘탈을 회복할 수 있는 회복존으로 작동한다. 게임이 겨냥하는 풍자의 표적은 ‘사람’이 아니라 ‘지옥 같은 출근 시스템 그 자체’다. 무엇을 넣느냐만큼 무엇을 넣지 않기로 했는가를 밝히는 것이, 이런 소재를 다루는 팀이 스스로에게 지우는 최소한의 책임이라고 본다.

패배 조건도 이 원칙 위에 서 있다. 지는 방법은 두 가지다. 하나는 밀려드는 압박을 견디지 못해 멘탈이 붕괴하는 것, 다른 하나는 시계가 9시를 넘겨 지각하는 것이다. 둘 다 ‘출근에 실패했다’는 하나의 감정으로 수렴한다. 그리고 패배 화면은 좌절로 끝나지 않고 ‘다시 출근하기’ 버튼으로 곧장 재도전을 유도한다. 로그라이크의 본질은 실패를 통해 다음 판을 준비하는 리듬에 있다.

두 패배 조건이 나뉘어 있다는 점도 의도된 설계다. 멘탈 붕괴는 ‘너무 무리하게 인파를 뚫었다’는 신호이고, 지각은 ‘너무 몸을 사려 시간을 못 벌었다’는 신호다. 한쪽으로 치우친 플레이는 각기 다른 방식으로 벌을 받는다. 그래서 잘 죽은 판일수록 다음 판에서 무엇을 바꿔야 할지가 선명해지고, 플레이어는 ‘조금만 더 빨리 탈걸’ 하며 자연스럽게 재도전 버튼으로 손이 간다.

지옥철 멘탈 붕괴 패배 화면과 다시 출근하기 재도전

다음 정거장은 어디인가

지금까지의 좌표를 정리하면 이렇다. 장르는 카드 덱빌딩에서 뱀서라이크로, 스택은 Godot 4에서 순수 브라우저 코드로, 두 번의 큰 방향 전환을 거쳐 지금의 형태에 도달했다. 코어를 담당한 M1은 게임 루프, 이동, 인파 스폰과 추적, 헛기침 오토 어택, 3택 레벨업, 멘탈 HP, 그리고 무엇보다 손맛까지 완성한 상태다. 런 구조를 담당하는 M2는 6구간과 게임 내 시계, 급행과 완행, HUD, 우산과 노이즈 캔슬링 같은 요소를 얹는 중이다.

앞으로 남은 M3에서 M5까지는 콘텐츠 확장과 메타 진행, 다듬기의 영역이다. 브라우저에서 열자마자 30초 안에 ‘아, 이거 딱 내 출근길이네’ 하고 웃게 만드는 것이 이 게임의 완성 기준이다. 그 웃음이 조롱이 아니라 공감에서 나오도록, 시계와 인파와 헛기침이라는 세 축을 계속 다듬어 나갈 예정이다.

이 프로젝트를 만드는 팀

  • game-lead — 기획과 규칙 최종 결정, ‘재미’의 최종 판단.
  • game-design-team — 규칙과 밸런스, 절차생성 스펙 설계.
  • game-code-team — Godot 및 vanilla JS 구현.
  • game-code-reviewer — 독립 코드 리뷰로 스펙과 구현을 교차검증.
  • game-asset-team — 그래픽과 사운드 에셋 제작.
  • game-playtest-team — 클리어타임과 사망률 등 수치 완료조건 자동테스트.