Два года назад Доминик Заблевски портировал свой JavaScript-движок для игр на C — как он сам признавался, без особой причины. Причина нашлась позже: понадобилось сделать новую игру для Nintendo 64.
Результатом стала Xibalba 64 — шутер от первого лица в духе Wolfenstein 3D. Компания Modretro согласилась издать игру как физический тайтл для запуска своей консоли M64 (современного клона N64), с картриджем, упаковкой и печатной инструкцией.

Насколько известно автору, это лишь второй случай физического релиза полностью новой игры для N64 с момента завершения коммерческой жизни консоли. Печально известная Xeno Crisis от Bitmap Bureau — изначально новая игра для Sega Mega Drive, впоследствии вышедшая на множестве других платформ — добралась до N64 в 2023 году. До этого новых игр для N64 не издавали с момента выхода Tony Hawk's Pro Skater 3 в 2002 году.
Движок
Impact — JavaScript-движок для игр, разработанный ещё в 2010 году. Он был заточен под 2D-action: работа с тайловыми листами, картами фона, спрайтами и обработкой коллизий. Простой, но крепкий фундамент, на который можно навесить почти что угодно.
Два года назад Impact был переписан на C. Причина — просто ради интереса.
Этот C-порт, high_impact, устроен вокруг понятия "платформенного бэкенда". Такой бэкенд отвечает за низкоуровневую рутину — открытие окна, создание поверхности для рисования, чтение ввода и так далее. Из коробки в high_impact есть два платформенных бэкенда (SDL2 и Sokol), под любой из которых можно скомпилировать игру. Уже одно это позволяет играм на high_impact работать на множестве устройств.
Бэкенд рендеринга в high_impact тоже модульный. Игру можно собрать с программным рендерером, с OpenGL или с Metal (для iOS/macOS). Поддержку новых платформенных или рендер-бэкендов можно добавлять, не трогая остальные части движка.
Идеальная стартовая точка для игры на N64.
Железо N64 и платформенная библиотека
N64 — своеобразный зверь. Помимо 93-МГц MIPS-процессора (с big-endian!), у консоли есть два сопроцессора для графики, звука и прочего:
- "Reality Display Processor" (RDP) — графический процессор с фиксированной функциональностью
- "Reality Signal Processor" (RSP) — программируемый векторный процессор
Оба живут в одном физическом корпусе, известном как "Reality Coprocessor" (RCP).
Изображение из N64Brew Wiki
В первые годы жизни N64 Nintendo жёстко контролировала доступ к RSP. Он использовался исключительно официальной платформенной библиотекой Nintendo — "libultra". Только позже компания разрешила студиям писать собственный "микрокод" (по сути, ассемблер MIPS) для RSP.
Заставить это железо работать корректно — задача не из простых, а инструкции для RDP довольно причудливые и сложные. Программировать игру "на голом железе" практически невозможно. В последние годы официальная "libultra" от Nintendo просочилась в интернет, но использование её грозит судебным иском за нарушение авторских прав.
К счастью, за последние годы N64-хоумбрю-сцена набрала серьёзные обороты, и теперь есть вполне работоспособная альтернатива: Libdragon.
Libdragon — это, по сути, SDL для N64. Библиотека предоставляет средства для отрисовки спрайтов и треугольников, вывода звука, обработки ввода с геймпада и много другого.
Написание нового платформенного бэкенда для high_impact на базе Libdragon заняло всего несколько вечеров. Проверка проводилась на игре Biolab Disaster. Код игры остался без изменений; производительность была так себе, но железо N64 использовалось максимально наивным способом.
Среда разработки
Libdragon предоставляет компиляторы и всё необходимое для сборки ROM-файла N64. Инструкции по установке и остальная документация написаны основательно и понятно. В комплекте с библиотекой идёт множество примеров для старта.
В целом работа с Libdragon доставила удовольствие. Небольшая оговорка: лучше использовать ветку preview, поскольку "стабильная" ветка trunk безнадёжно отстала.
Для тестирования незаменим хороший эмулятор. Долгое время эмуляция N64 была крайне неточной. Основная причина проблем — плохая эмуляция сопроцессоров RSP и RDP.
Большинство эмуляторов просто эмулировали платформенную библиотеку Nintendo, libultra. Они воспроизводили намерение нарисовать треугольник, а не то, что реально делало железо. Неточно, но именно это в первые годы вообще делало эмуляцию возможной. Печально известный UltraHLE ("Ultra High Level Emulator") вышел ещё при жизни консоли на рынке и вызвал массу проблем и последующих судебных исков.
Сегодня ядро N64 в Ares куда ближе к реальному железу — RDP и RSP эмулируются полностью, включая точный тайминг для RSP. А вот знаменитую медленную пропускную способность памяти N64 всё ещё можно проверить только на реальном железе (что недавно принесло автору некоторое разочарование).
Так что нужна настоящая N64 и картридж, позволяющий запускать произвольные ROM-файлы формата .z64. Open-source картридж SummerCart64 отлично подходит и выпускается разными производителями. Стоит учесть: некоторые производители (особенно на AliExpress) экономят на компонентах платы.
У SummerCart64 есть обычный слот для SD-карты для хранения ROM-файлов, но по-настоящему удобным для разработки его делает порт USB-C: картридж можно подключить прямо к ПК и загружать ROM в рамках процесса сборки с помощью sc64deployer.
В итоге N64 стояла рядом с компьютером, подключённая по USB, а для вывода видео на экран использовалась дешёвая USB-карта видеозахвата за 10 долларов. На Linux пришлось повозиться с mpv, чтобы получить вывод с низкой задержкой; вот использованный скрипт.
С такой настройкой итерации на реальном железе сводились просто к компиляции и нажатию кнопки reset на N64.
Игра
Оригинальная Xibalba была сделана как демо для JavaScript-движка в 2014 году. WebGL тогда был свежей горячей темой; 3D-игра в браузере была настоящей новинкой. Игра была очень короткой: пара уровней, немного видов оружия и врагов.
Для Xibalba 64 хотелось сделать полноценную игру, а не просто демо. Значит, требовалось не только портировать игру на C и high_impact, но и расширить её — добавить уровни, новых врагов и оружие.
high_impact — это 2D-движок, но Xibalba 64 явно трёхмерная. Точнее, не совсем. Поскольку в игре нет перепадов высоты, её можно в основном рассматривать как 2D. Концептуально Xibalba 64 можно было бы играть с вида сверху в 2D — конечно, не так эффектно, но вся физика, движение и стрельба работали бы точно так же. В этом смысле игра очень похожа на Wolfenstein 3D.
Многие физические функции high_impact принимают аргумент типа vec2_t с компонентами .x и .y. Но для отрисовки нужна была именно 3D-позиция, поэтому появилось следующее определение типа vec3_t, и изменился тип entity_t:
typedef struct {
float x, y;
} vec2_t;
typedef union {
vec2_t xy;
struct {
float x, y, z;
};
} vec3_t;
typedef struct {
// ...
vec3_t pos;
vec3_t vel;
// ...
} entity_t;
Теперь при вызове функции, принимающей vec2_t, "конвертация" из vec3_t происходит бесплатно:
trace_t res = trace(collision_map, entity->pos.xy, entity->vel.xy);
Поскольку внутренняя struct в vec3_t "анонимная", все значения по-прежнему доступны напрямую — entity->pos.z работает без проблем.
Первоначальный порт существующих уровней и типов врагов прошёл довольно гладко и занял около двух недель. Затем ещё несколько месяцев ушло на расширение игры и оптимизацию рендерера.
Большинство функций Libdragon естественно легли в новый платформенный и рендер-бэкенд, хотя пришлось изменить некоторые другие части high_impact, чтобы обойти его микшер (у Libdragon есть свой, с ускорением на RSP) и загрузчик изображений.
На протяжении всего процесса сохранялась возможность собирать игру с бэкендами SDL2 или Sokol. Это очень помогало при плейтестинге игровой логики и поведения врагов. Для создания уровней также был реализован простой механизм hot-reload, срабатывающий при изменении файла уровня.
Редактор уровней, идущий в комплекте с high_impact, — это единый самодостаточный HTML-файл. Пришлось довольно сильно расширить его: добавить лучшую поддержку lightmap, показ реальных спрайтов сущностей (вместо простых боксов), описания для настроек сущностей и другие мелкие фичи. Единственным источником истины остаётся исходный код на C — редактор уровней читает его и автоматически извлекает типы сущностей и поддерживаемые настройки.

Поскольку редактор уровней работает с JSON-файлами, был написан небольшой компилятор карт, который читает JSON и выдаёт бинарные данные. Загрузка JSON на N64 в принципе возможна, но добавляла лишние ~100 мс времени загрузки. Поэтому в процессе сборки каждый JSON-файл уровня конвертируется в структуру, выглядящую примерно так:
typedef struct {
uint16_t magic;
uint16_t entities_len;
uint16_t map_width;
uint16_t map_height;
struct {
uint16_t type_id;
uint16_t x;
uint16_t y;
uint16_t settings_len;
struct {
uint16_t setting_type; // such as "name", "target", "size", ...
union {
float16_t float_value;
int16_t int_value;
struct {
int16_t string_len;
char string_value;
};
} value;
} settings[settings_len];
} entities[entities_len];
uint16_t collision_map[map_width * map_height];
uint16_t floor_map[map_width * map_height];
uint16_t wall_map[map_width * map_height];
uint16_t ceiling_map[map_width * map_height];
uint16_t light_map[map_width * map_height];
} level_t;
Компилятор уровней записывает эти значения в big-endian формате для N64 и в little-endian формате для x86 (SDL2, Sokol, WASM), чтобы всё можно было читать на любой платформе без перестановки байтов.
Рендеринг
У самой Libdragon есть функция для отрисовки треугольников: rdpq_triangle() вставляет вызов отрисовки одного треугольника в очередь RDP. Это работает, но на самом деле правильнее отправлять команды отрисовки в RSP, применять собственный микрокод для трансформаций, освещения, расчёта глубины и так далее, а затем позволить RSP командовать RDP для итоговой отрисовки треугольника.
Тонкости работы RDP и RSP были в новинку, но, к счастью, ещё одна выдающаяся open-source библиотека, Tiny3D, берёт всё это на себя и предлагает простой в использовании API. Вывести что-то на экран оказалось лёгкой частью; добиться производительности — совсем другое дело.
N64 знаменита тем, что у неё всего 4 КБ памяти текстур. Максимальный размер загружаемых текстур — жалкие 64×64 пикселя. Хуже того, задержка при загрузке текстуры просто чудовищная. Одно из решений, используемое в Mario 64 и многих других играх, — рисовать нетекстурированные полигоны везде, где возможно.
Такой подход не подходил стилю игры, поэтому пришлось очень тщательно следить за порядком отрисовки тайлов уровня, чтобы минимизировать загрузки текстур. Вдобавок Tiny3D может загружать и отправлять до 17 квадов за раз, и не воспользоваться этим было бы расточительно. В итоге треугольники с одинаковой текстурой собирались в пакеты через 64-битные вызовы отрисовки:
typedef union render_call {
uint64_t packed;
uint32_t hashable;
uint64_t ident : 46;
struct {
uint64_t translucent : 1;
uint64_t texture_index : 9;
uint64_t x : 10;
uint64_t y : 10;
uint64_t w : 8;
uint64_t h : 8;
uint64_t vbi : 14;
uint64_t len : 4;
};
} render_call_t;
Здесь vbi — индекс соответствующего буфера вершин, а len — количество квадов в этом вызове. Поскольку каждый вызов занимает всего 64 бита, их можно эффективно сортировать в конце кадра и отправлять в Tiny3D.
Но прежде чем всё это делать, нужно было выяснить, какие части уровня реально видны. Оригинальная JavaScript-версия Xibalba использовала систему порталов, разделяя каждый уровень на секторы и предвычисляя, какие секторы видны из текущего. Это работало неплохо, но давало больше избыточной отрисовки (overdraw), чем хотелось бы.
Поэтому выбрали другой подход — raycasting. Игра буквально выпускает 320 лучей в сцену, покрывающих всё поле зрения. Каждый луч отмечает пройденные тайлы в битовой карте для передачи рендереру.
Позже алгоритм трассировки лучей был доработан: поле зрения в 320 пикселей рекурсивно делится, пока два луча не попадут в один и тот же тайл. В этом же процессе проверяется, отсутствует ли у какого-либо из пройденных тайлов потолок — если да, нужно рисовать скайбокс.
Забавный факт: скайбокс в Xibalba 64 — это одна-единственная текстура 32×32 пикселя, красиво размазанная по горизонту.

Ещё одна оптимизация: каждый тайловый лист, не помещавшийся в одну загрузку, выстраивался в единый столбец, и там, где возможно, использовались 4-битные индексированные цвета. Меньшее число цветов позволяло уместить больше пикселей в память текстур, а расположение столбцом гарантировало, что каждый тайл загружается единым непрерывным блоком памяти.

В итоге игра работает со стабильными 60 кадрами в секунду — то, чем могут похвастаться далеко не все игры на N64.
Режим разделённого экрана для четырёх игроков не всегда держит все 60 FPS, но остаётся плавным. Для сравнения, GoldenEye 007 в этом режиме печально известна проседаниями до однозначных значений частоты кадров.

Звук и музыка
Как и во всех предыдущих играх, отличную музыку написал давний друг разработчика Андреас Лёш. Полный саундтрек Xibalba 64 доступен на Bandcamp.
В самой игре также есть встроенный музыкальный проигрыватель, который открывается в однопользовательской кампании.
Места на картридже мало, и даже сжатое аудио обычно либо весит слишком много, либо слишком дорого декодировать.
Джованни Бахо — один из мейнтейнеров Libdragon и настоящий волшебник во всём, что касается N64, — героическим усилием реализовал декодер Opus с ускорением на RSP. Для контекста: Opus — аудиокодек, впервые опубликованный в 2012 году, то есть спустя 16(!) лет после выхода N64. Уже само то, что это вообще работает, — чудо, но, к сожалению, пока это всё ещё слишком затратно по вычислениям для использования непосредственно во время игры.
(Кстати: тот же Джованни Бахо позже реализовал для N64 и декодер H.264 в реальном времени.)
На данный момент лучший вариант — простой 4-битный формат VADPCM, который Libdragon прозрачно декодирует на RSP во время воспроизведения. Степень сжатия, конечно, не блестящая, но заметно лучше, чем несжатый WAV. Около 31 МБ из 32-мегабайтного ROM занимают звук и музыка.
После релиза Xibalba 64 началась работа над новым форматом сжатия аудио, лучше балансирующим объём, качество и сложность декодирования. Подробности — в следующем посте.
Издание и продажи
Ранее Modretro выпустила клон Game Boy Color — Chromatic, совместимый со всеми существующими играми для Game Boy и Game Boy Color, и компания охотно издавала новые игры от разработчиков-энтузиастов.
Когда объявили о M64, стало ясно, что это отличный шанс присоединиться. Как только рабочий прототип был готов и появилась уверенность в возможности довести игру до конца (причём сделать её действительно хорошей), было отправлено письмо на общий адрес поддержки Modretro — и, к удивлению, ответ от главы отдела публикации пришёл в течение дня.

Бюрократии оказалось минимум, контракт был простым и понятным. Были запрошены некоторые изменения, позволяющие впоследствии выпустить движок игры как open source, и Modretro охотно на это пошла.
Как это обычно бывает с такими проектами, случились задержки, и предпродажный экземпляр M64 добрался до разработчика только на позднем этапе разработки. Но это не имело значения — консоль работала именно так, как обещано, никаких изменений в игре под M64 не потребовалось.
Modretro также предлагала помощь с обложкой, но за это взялся другой давний друг разработчика. Позже он рассказал, что в 90-х отвечал за упаковку многих игр, изданных Sierra в Германии, включая Half-Life. Неудивительно, что обложка Xibalba 64 получилась отличной!
Для инструкции текст и иллюстрации были переданы Modretro, а верстку под печать компания сделала сама. Всё прошло гладко. PDF-версию инструкции можно посмотреть на странице Xibalba 64 в магазине.
Подробности о продажах или условиях контракта с Modretro раскрывать нельзя, а игра вышла всего несколько дней назад, но пока результаты выглядят неплохо. Разумеется, разработка игр для N64 вряд ли позволит уйти с основной работы, но, похоже, хватит на пару приятных отпусков.
Огромная благодарность Джованни Бахо за Libdragon, Максу Бебёку за Tiny3D, всему сообществу N64brew Discord и, конечно, команде Modretro за то, что довели этот проект до конца!
Финальный трейлер игры:
Ресурсы
Тем, кто хочет попробовать себя в разработке для N64, могут пригодиться следующие ресурсы:
- n64.dev — собрание всего, что касается разработки для N64
- N64brew Discord — дружелюбное и отзывчивое сообщество, где часто можно найти разработчиков самих библиотек
- Libdragon — главная системная библиотека для N64
- Tiny3D — простая и быстрая библиотека для 3D-графики
- Pyrite64 — визуальный редактор и runtime для создания 3D-игр