Как создавался CrazyTaxi: классика Sega с Dreamcast, заново собранная на Three.js
CrazyTaxi — это пересборка Crazy Taxi с Sega Dreamcast на Three.js. Машина, трафик, камера, клиенты и меню работают на собственном коде игры для SH-4 в небольшом интерпретаторе и совпадают с декомпиляцией бит в бит. Agent! создал его за три дня и 157 коммитов. Играйте на ct.gokart.games.
Играйте прямо сейчас: ct.gokart.games. Нужен браузер с поддержкой WebGL; играть можно с клавиатуры или с контроллера Xbox (или другого стандартного).
CrazyTaxi заново собирает Crazy Taxi для Sega Dreamcast (USA, MK-51035) на Three.js. Первый коммит появился 22 октября. К вечеру 24 октября их было уже 157: 93 в первый день, 41 во второй и 23 в третий. Всё это написал Agent!, по одному протестированному коммиту за раз, и сообщения коммитов читаются как лабораторный журнал: адреса, количества и проверки почти в каждом.
Это не ремейк по скриншотам. Почти всё, что вы видите на экране, и большая часть того, как оно себя ведёт, берётся с самого диска.
День первый: достаём игру с диска
Большая часть первого дня ушла на извлечение. В репозитории нет игровых данных. Вы делаете образ своего диска, и одна команда извлекает из него всё:
python3 tools/extract_all.py '<Crazy Taxi (USA).7z or .chd>' --output local-assets
За этой командой стоят 17 конвертеров в tools/, и каждый закоммичен в те же три шага: сам инструмент, его тесты, затем документ с найденным. По порядку они обрабатывают:
- Диск: извлекается и сверяется по хешам с сырого трека высокой плотности.
extract_chd.pyчитает образы CHD v5 собственными декодерами, включая декодер FLAC, и восстанавливает коррекцию ошибок секторов CD. Результат совпадает с образом.7zбайт в байт. - Текстуры и спрайты: банки текстур TEXDC, а также банки спрайтов, палитры которых восстановлены из кода загрузчика SH-4 в исполняемом файле.
- Модели и города: модели POLDC и архивы городов BINC, преобразованные в текстурированный glTF.
- Персонажи и анимации: собраны по собственной процедуре отрисовки игры, включая анимации на ключевых кривых и нужную им двухкостную IK.
- Трафик и такси: построены по таблице машин USA и таблицам такси, каждый цвет трафика — отдельный вариант материала glTF.
- Коллизии, линии трассы, объекты сцены, клиенты, остановки, картинки пунктов назначения и повторы — каждое сверено с таблицами самого исполняемого файла.
- Звук: музыка и голоса в ADX и звуковые банки DTPK, с частотами воспроизведения и программами инструментов, прочитанными так, как их читает звуковой драйвер Dreamcast.
Полный прогон записывает 4,744 файла (255 MB) примерно за две минуты.
Запускаем собственный код игры
Игра на three.js появилась в тот же вечер: город, временное такси и вручную настроенная камера преследования.
Меньше чем через час коммит 02bf19e выбросил временную физику. src/sh4.js — это небольшой интерпретатор SH-4, процессора Dreamcast, а src/dc-car.js с его помощью запускает init_CarMain и exec_CarMain прямо из 1ST_READ.BIN игры. Они работают с точками трассы и данными коллизий с диска, загруженными по их исходным адресам Dreamcast. Отрисовка, звук и вибрация заглушены или перехвачены, а three.js рисует всё, что запросила игра.
Остальная игра переезжала в интерпретатор по одной системе за раз:
- Трафик: каждая машина, поезд, канатный трамвай и светофор — это игровое событие, которым управляют собственные
trafficControlиnlExecuteEventигры. Такси врезается в трафик, а трафик даёт сдачи. - Камера: камера преследования из
camUpdateи облёт вокруг такси во время обратного отсчёта. Three.js просто берёт матрицу вида игры. - Клиенты: кто где ждёт, посадка, поездка, а также оценка и бонус времени при высадке.
- Меню: титульный экран, главное меню, настройки, рекорды и выбор водителя. Всё это — код игры, рисующий спрайты, которые
src/menu-view.jsвыводит на canvas.
Каждая из этих систем сверяется с собственным симулятором 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игры, и они открывают следующие боксы. Сохранение — те же 0x25B0 байт, которые бэкапит Dreamcast, хранящиеся в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 это стабильные 60 fps (минимум 58) вместо прежних 37–47.
Нажмите H (или Y на контроллере), чтобы включить пакет текстур высокого разрешения. tools/match_texture_pack.py пересчитывает хеш пользовательских текстур Flycast для каждой текстуры игры, что позволяет наложить пакет текстур Flycast на 1,793 из 2,090 текстур игры. С включённым пакетом объединённый город рисуется из атласов в четыре раза большего размера (8192 × 8192). В релизе эти текстуры поставляются в KTX2: 51 MB против 551 MB JPEG и PNG в пакете.
Остальная полировка третьего дня:
- 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 ведёт такси на автопилоте и сравнивает каждый кадр с эталоном с правильной глубиной: до исправления порядка отрисовки неверными были 66 из 95 сэмплов, после — 0 из 565. tools/mac-probe.sh управляет приложением для Mac без участия человека и каждую секунду выводит fps и число вызовов отрисовки.
Попробуйте
- Играть: ct.gokart.games
- Исходный код: AgentiLoop/CrazyTaxi. Чтобы запустить локально, нужны собственный диск Crazy Taxi (USA), Node.js и Python 3.
Остановитесь рядом с клиентом, внутри его круга, чтобы подобрать его. Затем следуйте за стрелкой к месту на картинке и остановитесь там.
CrazyTaxi — независимый фанатский проект, не связанный с Sega и не одобренный ею. Crazy Taxi — товарный знак Sega. В репозитории нет игровых данных.