← Все статьи

Как создавался CrazyTaxi: классика Sega с Dreamcast, заново собранная на Three.js

CrazyTaxi — это пересборка Crazy Taxi с Sega Dreamcast на Three.js. Машина, трафик, камера, клиенты и меню работают на собственном коде игры для SH-4 в небольшом интерпретаторе и совпадают с декомпиляцией бит в бит. Agent! создал его за три дня и 157 коммитов. Играйте на ct.gokart.games.

CrazyTaxi: жёлтое такси едет по городу Arcade прямо в браузере.
Город Arcade в браузере. Город, такси и трафик взяты с диска Dreamcast. Three.js их рисует, а управляет ими собственный код игры.

Играйте прямо сейчас: 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. В репозитории нет игровых данных.