CrazyTaxi 제작기: Sega Dreamcast의 고전을 Three.js로 다시 만들다
CrazyTaxi는 Sega Dreamcast용 Crazy Taxi를 Three.js로 다시 만든 프로젝트입니다. 차량, 교통, 카메라, 손님, 메뉴가 모두 작은 인터프리터 안에서 게임 자체의 SH-4 코드로 실행되며, 디컴파일 결과와 비트 단위까지 일치합니다. Agent!가 3일 동안 157개의 커밋으로 만들었습니다. ct.gokart.games에서 플레이할 수 있습니다.
지금 바로 플레이: ct.gokart.games. WebGL을 지원하는 브라우저가 필요하며, 키보드나 Xbox(또는 기타 표준) 컨트롤러로 플레이할 수 있습니다.
CrazyTaxi는 Sega Dreamcast용 Crazy Taxi(USA, MK-51035)를 Three.js로 다시 만든 것입니다. 첫 커밋은 10월 22일에 들어갔습니다. 10월 24일 밤에는 커밋이 157개가 되었습니다. 첫날 93개, 둘째 날 41개, 셋째 날 23개입니다. 이 모두를 Agent!가 테스트된 커밋을 하나씩 쌓아 가며 작성했고, 커밋 메시지는 거의 모든 항목에 주소, 개수, 검증 결과가 적혀 있어 마치 실험 노트처럼 읽힙니다.
스크린샷을 보고 만든 리메이크가 아닙니다. 화면에 보이는 거의 모든 것, 그리고 그것이 움직이는 방식의 대부분은 디스크 자체에서 나옵니다.
첫째 날: 디스크에서 게임 꺼내기
첫날의 대부분은 추출 작업에 들어갔습니다. 저장소에는 게임 데이터가 전혀 없습니다. 자신의 디스크를 직접 립하면, 명령 하나로 모든 것을 꺼낼 수 있습니다.
python3 tools/extract_all.py '<Crazy Taxi (USA).7z or .chd>' --output local-assets
이 명령 뒤에는 tools/에 있는 17개의 변환기가 있고, 각각 같은 세 단계로 커밋되었습니다. 도구, 그 테스트, 그리고 발견한 내용을 정리한 문서입니다. 순서대로 다음을 처리합니다.
- 디스크: 원본 고밀도 트랙에서 추출하고 해시로 검증합니다.
extract_chd.py는 FLAC 디코더를 포함한 자체 디코더로 CHD v5 이미지를 읽고, CD 섹터 오류 정정을 다시 구성합니다. 출력은.7z립과 바이트 단위까지 일치합니다. - 텍스처와 스프라이트: TEXDC 텍스처 뱅크와 스프라이트 뱅크입니다. 스프라이트 팔레트는 실행 파일의 SH-4 로더 코드에서 복원했습니다.
- 모델과 도시: POLDC 모델과 BINC 도시 아카이브를 텍스처가 입혀진 glTF로 변환합니다.
- 캐릭터와 모션: 게임 자체의 그리기 루틴을 따라가며 조립했습니다. 키 커브 모션과 그에 필요한 2본 IK도 포함됩니다.
- 교통과 택시: USA 차량 테이블과 택시 테이블로 만들었으며, 교통 차량의 각 색상은 glTF 머티리얼 변형으로 들어갑니다.
- 충돌, 코스 라인, 세트 오브젝트, 손님, 정류장, 목적지 사진, 리플레이: 각각 실행 파일 자체의 테이블과 대조해 검증했습니다.
- 오디오: ADX 음악과 음성, DTPK 사운드 뱅크입니다. 재생 속도와 악기 프로그램은 Dreamcast의 사운드 드라이버가 읽는 방식 그대로 읽습니다.
전체 실행은 약 2분 만에 4,744개의 파일(255 MB)을 씁니다.
게임 자체의 코드를 실행하기
three.js 게임은 그날 저녁, 도시와 임시 택시, 손으로 조정한 추격 카메라로 시작했습니다.
한 시간도 지나지 않아 커밋 02bf19e가 임시 물리를 걷어냈습니다. src/sh4.js는 Dreamcast의 CPU인 SH-4를 위한 작은 인터프리터이고, src/dc-car.js는 이를 이용해 게임의 1ST_READ.BIN에서 init_CarMain과 exec_CarMain을 그대로 실행합니다. 이 함수들은 원래의 Dreamcast 주소에 로드된 디스크의 코스 포인트와 충돌 데이터 위에서 실행됩니다. 그리기, 사운드, 진동은 스텁으로 대체하거나 가로채고, three.js는 게임이 요청한 것을 그대로 그립니다.
게임의 나머지 부분도 시스템 하나씩 인터프리터로 옮겨 갔습니다.
- 교통: 모든 차, 기차, 케이블카, 신호등은 게임 이벤트이며, 게임 자체의
trafficControl과nlExecuteEvent가 실행합니다. 택시가 교통 차량을 들이받으면, 교통 차량도 되받아칩니다. - 카메라:
camUpdate의 추격 카메라와 카운트다운 중 택시 주위를 휘감아 도는 연출입니다. Three.js는 게임의 뷰 행렬을 그대로 가져옵니다. - 손님: 누가 어디서 기다리는지, 탑승, 운행, 그리고 하차 시의 평가와 타임 보너스까지입니다.
- 메뉴: 타이틀 화면, 메인 메뉴, 옵션, 기록, 드라이버 선택. 모두 게임의 코드가 스프라이트를 그리는 것이며,
src/menu-view.js가 이를 캔버스에 올립니다.
이 모든 것은 디컴파일 프로젝트 자체의 SH-4 시뮬레이터와 대조해 검증됩니다. tools/car_trace.py는 카운트다운, 탑승, 주행, 하차까지 손님 한 명의 운행 전체를 기록합니다. 택시, 카메라, 모든 교통 차량, 게임 시계, 모든 손님의 매 프레임이 두 도시 모두에서 비트 단위까지 일치합니다.
이 검증으로 실제 버그를 하나 잡았습니다. 게임 수학의 서로 다른 두 부분이 어긋났는데, 인터프리터의 64비트 float 이동 명령이 SH-4가 실제로 쓰는 순서대로 레지스터 쌍을 저장하도록 고친 뒤에야 일치했습니다. 디컴파일 시뮬레이터에도 car_trace.py에서 같은 패치가 적용됩니다.
Crazy Box
둘째 날에는 Crazy Box가 들어왔습니다. 게임의 미니 게임으로, 16개 모두 Init_DCmini와 execDCmini가 실행합니다. 게임의 새로운 부분은 모두 같은 방식으로 찾았습니다. 실행 파일에서 루틴을 찾고, 인터프리터에서 실행하고, 그것이 그리는 것을 가로채는 방식입니다.
- Crazy Bowling의 핀은
ExecSetObject에서 나옵니다. 부딪히면 날아가고, 게임이 그 개수를 셉니다. - 초록색 안내 화살표는
ExecArrow에서 나오며, 뷰 공간에서 그려진 뒤 다시 월드에 배치됩니다. - TRY AGAIN 창은 메뉴 커서까지 포함해 게임 자체의
windowTryAgain입니다. - 클리어 기록은 게임 자체의
CheckRanking이 저장하며, 다음 박스를 잠금 해제합니다. 저장 데이터는 Dreamcast가 백업하는 것과 같은 0x25B0바이트이며,localStorage에 보관됩니다.
이 작업에서 얻은 발견 하나. carRumble이라는 이름의 루틴은 단순한 진동이 아닙니다. Crazy Dash를 실행합니다. 이 루틴을 스텁으로 막아 두었을 때 Crazy Jump는 132 m가 한계였습니다. 실행되게 하자 GO 시점의 대시로 173 m에 도달합니다.
60 fps와 고해상도 텍스처
정확해진 다음에는 빨라져야 했습니다. 도시는 13,779개의 개별 메시로 들어왔고, 브라우저는 프레임마다 3,000~4,500번의 드로 콜을 수행했습니다. Auto-Pilot 브랜치에서 src/static-batch.js가 정적인 월드를 병합합니다. 도시 텍스처 374개를 모두 하나의 아틀라스에 넣고, 메시는 4,000단위 셀마다 병합합니다. 이렇게 하면 프레임당 드로 콜이 약 300번으로 줄어듭니다. Mac 앱에서 2560 × 1920 해상도로 실행하면 이전의 37~47에서 안정적인 60 fps(최소 58)로 올라갑니다.
H(컨트롤러에서는 Y)를 누르면 고해상도 텍스처 팩이 켜집니다. tools/match_texture_pack.py는 게임의 모든 텍스처에 대해 Flycast의 커스텀 텍스처 해시를 다시 계산하여, Flycast 텍스처 팩을 게임의 텍스처 2,090개 중 1,793개에 대응시킵니다. 팩을 켜면 배칭된 도시는 네 배 크기(8192 × 8192)의 아틀라스로 그려집니다. 릴리스에서는 이 텍스처들을 KTX2로 제공합니다. 팩의 JPEG와 PNG가 551 MB인 데 비해 51 MB입니다.
셋째 날의 나머지 다듬기 작업은 다음과 같습니다.
- TAA: 지터를 준 프레임을 재투영한 히스토리(
src/taa.js)와 섞고, 그 위에 4× MSAA를 더해 먼 도시가 더 이상 지글거리지 않습니다. - 깜빡임 제거: 보도 위 건물 그림자처럼 겹치는 동일 평면의 디테일을 시점 쪽으로 당겨서 깊이 다툼을 멈추게 했습니다.
- 더 작은 다운로드: 음악과 음성은 Opus와 AAC로(약 150 MB에서 37 MB로), 나머지는 모두 Brotli로(전송량 103 MB에서 28 MB로) 줄였습니다.
- 새로운 기능: Xbox 컨트롤러 지원, 목적지까지의 최단 도로 경로를 초록색으로 보여 주는 도시 미니맵, 그리고 Arcade 규칙에서의 하차 타임 보너스.
- 데스크톱 앱: 서명과 공증을 마친 Mac 앱, 그리고 Windows와 Linux용 런처. 모두 ct.gokart.games의 웹 빌드를 실행합니다.
어떻게 검증했나
이 중 어느 것도 그냥 믿고 넘어가지 않았습니다. 디컴파일 트레이스 픽스처가 게임 로직을 프레임 단위로 검증합니다. 변경할 때마다 Headless Chrome이 두 도시에서 운행을 한 번씩 돌리고 오류를 셉니다. tools/draw-order-check.mjs는 오토파일럿으로 택시를 몰면서 매 프레임을 깊이가 올바른 기준 이미지와 비교합니다. 그리기 순서를 고치기 전에는 샘플 95개 중 66개가 틀렸고, 고친 뒤에는 565개 중 0개였습니다. tools/mac-probe.sh는 사람 없이 Mac 앱을 조작하며 매초 fps와 드로 콜 수를 출력합니다.
직접 해 보세요
- 플레이: ct.gokart.games
- 소스: AgentiLoop/CrazyTaxi. 로컬에서 실행하려면 직접 소유한 Crazy Taxi (USA) 디스크, Node.js, Python 3이 필요합니다.
손님을 태우려면 손님 옆, 원 안에 멈추세요. 그런 다음 화살표를 따라 사진 속 장소로 가서 멈추면 됩니다.
CrazyTaxi는 독립적인 팬 프로젝트이며 Sega와 제휴하거나 Sega의 승인을 받은 것이 아닙니다. Crazy Taxi는 Sega의 상표입니다. 저장소에는 게임 데이터가 포함되어 있지 않습니다.