[Перевод] Создаём пасьянс для забытой периферии Nintendo

Недавно я закончил создание пасьянса для Nintendo E-Reader. Мне удалось уместить его на одной карте, и это практически полнофункциональная версия игры. Я очень доволен тем, что получилось.

Что такое E-Reader?

E-Reader — это периферийное устройство для Game Boy Advance, выпущенное компанией Nintendo в 2002 году. Сканируя карты, где есть полоска с кодом из точек, можно загружать мини-игры, дополнительные уровни, анимации и так далее.

The E-Reader and one of its cards

E-Reader и одна из его карт

Мне всегда очень нравился E-Reader, и меня опечалило, что дела у него в Америке шли не особо хорошо. Поэтому я подумал, что, возможно, стоит попробовать писать для него игры самому.

И вот результат:

11d1916d7e9509b01050b6c5e56a4042.png

Если вы хотите попробовать поиграть, то можете получить карту на retrodotcards.com.

Инструменты и документация, начинаем путь назад в прошлое…

С чего начать? Я помнил, что есть старые инструменты и веб-сайты о создании карт для E-Reader ещё из тех времён, когда их только начали выпускать — двадцать лет назад! Мне удалось найти оригинал сайта Тима Шуервегена в Wayback Machine. На нём было несколько примеров, исходный код и инструменты. Кроме того, я заново открыл для себя сайт про E-Reader разработчика CaitSith2, который, к счастью, всё ещё работал. На нём тоже были инструменты и информация.

Эти инструменты — фундамент для разработчика под E-Reader. Благодарю Тима и CaitSith2 за их создание! Изначально они разработаны для Windows, но позже были сделаны многоплатформенными.

Эти первые находки стали отличным началом, позволив мне приступить к изучению программирования приложений E-Reader. Кроме того, в GBATEK есть раздел про E-Reader, в котором содержится множество полезной информации.

Позже я обнаружил репозиторий e-reader-dev AkBKukU, который мне тоже очень помог.

Выберите своё оружие: GBA, NES или … z80?

Какую карту E-Reader я могу создать? Карты E-Reader примерно делятся на четыре формата

Приложения Game Boy Advance

Это программы для GBA, которые пишутся практически так же, как обычные игры для GBA. По большей мере E-Reader просто загружает их, а затем позволяет им выполняться самим.

Игры NES

E-Reader содержит простой эмулятор NES, поэтому на карты E-Reader можно непосредственно записывать простые игры NES. Ключевое слово здесь «простые», потому что он не поддерживает расширенные функции NES. Кроме того, у E-Reader есть ограничение на количество прокатываний карты на одно приложение. Выпущенные Nintendo игры NES требуют до десяти прокатываний карт! Поэтому в конечном итоге, возможно запускать только ранние/маленькие игры NES. Nintendo использовала эту возможность для выпуска на E-Reader игр наподобие Excitebike и Donkey Kong.

Excitebike in E-Reader card format

Excitebike в формате карт E-Reader

Сырые двоичные файлы

Сырые карты E-Reader просто содержат двоичные данные разных типов. Отдельные игры используют их для добавления уровней, персонажей и так далее. Это своего рода примитивная разновидность DLC. Конкретная игра сама интерпретирует данные, как ей удобно.

Подобные карты выпускались для Super Mario Advance 4, они добавляли новые уровни, бонусы и другой контент игры.

Super Mario Advance 4 E-Reader level cards

Карты уровней Super Mario Advance 4 для E-Reader

Приложения z80

И, наконец, E-Reader содержит простой эмулятор z80. Это 8-битный процессор, выпущенный ещё в 1976 году! Он был очень успешен и использовался во множестве разных компьютеров.

Мне кажется, Nintendo так никогда и не использовала процессор z80 ни в одной из своих игровых консолей, поэтому этот выбор интересен. Я уверен, что важным фактором для него была простота z80, его достаточно легко эмулировать.

Дополнение: позже мне сказали, что процессоры Game Boy и Game Boy Color были очень похожи на z80. Возможно, это повлияло на решение Nintendo. Я не знал этого, так что спасибо тем, кто сообщил мне об этом.

Manhole: a simple z80 E-Reader game

Manhole: простая игра для z80 на E-Reader

Это означает, что приложения E-Reader можно писать на языке ассемблера z80. Главное преимущество этого заключается в том, что приложения z80 обычно бывают достаточно маленькими. В своих экспериментах я выяснил, что приложение для z80 на E-Reader примерно на 30–50% меньше, чем эквивалентное приложение GBA на E-Reader. При создании собственных карт Nintendo почти полностью пошла по этому пути, вероятно, для того, чтобы свести количество прокатываний до минимума.

Приложения для z80 на E-Reader

Я разработал пасьянс как приложение z80, и мне очень понравился этот опыт. Меня радует то, насколько маленькими оказываются двоичные файлы. Но можете не сомневаться, язык ассемблера z80 довольно суров. Особенно учитывая, что можно написать карту GBA E-Reader на C.

ERAPI API

Для игр z80 компания Nintendo встроила в E-Reader простой, но эффективный API. Через этот API можно выполнять такие действия, как создание спрайтов, воспроизведение музыки и даже умножение и деление. Это помогает в снижении размеров карт, потому что общую функциональность не нужно упаковывать в карты, её предоставляет сам E-Reader.

Игры GBA для E-Reader тоже могут получать доступ к ERAPI. Подход местами немного отличается, но в целом это тот же API.

Вот простой пример того, как создаётся спрайт при помощи API:

    ; ERAPI_SpriteCreate()
    ; e  = pal#
    ; hl = sprite data
    ld  e, #2
    ld  hl, #my_sprite_data_struct
    rst 0
    .db ERAPI_SpriteCreate
    ld  (my_sprite_handle), hl

Если вы незнакомы с языком ассемблера z80, то это, вероятно, выглядит безумно. По сути, это эквивалент такого кода:

int palette_index = 2;
int my_sprite_handle = SpriteCreate(
    palette_index,
    my_sprite_data_struct
);

Вызовы ld — это «загрузка»; здесь мы загружаем в регистр e то, какой индекс палитры мы хотим использовать для спрайта. В регистр hl загружается указатель на информацию о спрайте (его тайлах, цветах, кадрах анимации и так далее). В строках rst 0 и .db ERAPI_SpriteCreate мы выполняем сам вызов API. Если не вдаваться в подробности работы z80, можно сказать, что это простой вызов функции. После его завершения он оставляет дескриптор спрайта в регистре hl, так что мы выполняем ld (my_sprite_handle), hl, чтобы скопировать это значение в память для надёжного хранения. Этот дескриптор в дальнейшем используется там, где мы захотим взаимодействовать со спрайтом, например, поменять его позицию.

Урезанный z80

Эмулятор z80 в E-Reader точен не на 100%. Nintendo решила не поддерживать некоторые опкоды и некоторые регистры. Также я обнаружил, что часть опкодов работают некорректно. Надеюсь, я просто неправильно их использую, но некоторые опкоды просто приводят к отображению на GBA чёрного экрана и зависанию.

z80 сам по себе очень ограниченный процессор, а такое урезание ограничивает его ещё сильнее. Иногда разработка для z80 на E-Reader абсолютно мучительна. Но чем сложнее, тем интереснее, так ведь?

Простые вещи, которые воспринимаются как должное, например, копирование одного массива в другой, намного сложнее реализовать на языке ассемблера z80 в E-Reader. К счастью, я начал с ним осваиваться.

Отладка

Ещё одна огромная трудность заключается в отладке игры. Мы никак не можем вести логи, запуск игры на Game Boy Advance — это абсолютно чёрный ящик. В эмуляторах GBA наподобие mGBA есть хорошие функции отладки. Но это эмулятор z80, работающий на процессоре ARM консоли GBA. Я подумал, что пошаговое исполнение команд ARM, чтобы разобраться, как работают команды z80 — это чересчур трудоёмко, поэтому даже не пытался это сделать. К счастью, похоже, мне этого и не понадобится (подробности ниже).

Для своей первой попытки создания отладчика я взял z80js — ядро эмулятора z80, написанное Молли Хауэлл, и создал небольшое приложение, которое должно было исполнять мой двоичный файл и выводить в лог то, что делает процессор. Вывод выглядел так:

...
0B52: call _deck_gfx_render_column            | a:  17, b:  00, c:  03, d:  08, e:  5c, h:  00, l:  17, bc: 0003, de: 085c, hl: 0017
0B5B: ld   b,#0x13                            | a:  17, b:  00, c:  03, d:  08, e:  5c, h:  00, l:  17, bc: 0003, de: 085c, hl: 0017
0B5D: ld   c,#0x00                            | a:  17, b:  13, c:  03, d:  08, e:  5c, h:  00, l:  17, bc: 1303, de: 085c, hl: 0017
0B5F: ld   hl,(_deck_gfx_cur_column_addr)     | a:  17, b:  13, c:  00, d:  08, e:  5c, h:  00, l:  17, bc: 1300, de: 085c, hl: 0017
0B62: ld   d,#0x00                            | a:  17, b:  13, c:  00, d:  08, e:  5c, h:  08, l:  5c, bc: 1300, de: 085c, hl: 085c
0B64: ld   e,c                                | a:  17, b:  13, c:  00, d:  00, e:  5c, h:  08, l:  5c, bc: 1300, de: 005c, hl: 085c
0B65: add  hl,de                              | a:  17, b:  13, c:  00, d:  00, e:  00, h:  08, l:  5c, bc: 1300, de: 0000, hl: 085c
...

Каждая строка содержит исполненный процессором опкод и состояние регистров в этот момент.

Вот немного подчищенная одна строка:

0B5B: ld b,#0x13 | a:17, b:00, c:03, d:08, e:5c, h:00, l:17,
    bc:0003, de:085c,hl: 0017

Это как будто сработало: выполнило свою задачу и я смог устранять баги, изучая этот вывод. Но это было не очень увлекательно. Огромный недостаток такого подхода заключается в отсутствии интерактивности. Я просто вслепую запускал игру без возможности проверки нажатий на кнопки и тому подобного. Из-за этого мне иногда приходилось быть очень изобретательным, чтобы заставить эмулятор запустить ту часть игры, с которой возникали проблемы.

Качественный отладчик

Я использовал эту методику с трассировкой для разработки большей части игры. Но ближе к концу обнаружились два загадочных бага, в которых я никак не мог разобраться. Стало понятно, что нужно решение получше.

Я нашёл проект DeZog — расширение общего назначения для отладки z80 в VS Code. Он выглядел очень многообещающим, но потом мне подвернулся ZX81-Debugger. Себастиен Андривет взял за основу DeZog и создал расширение VS Code специально для написания и отладки приложений ZX81.

Мне сразу понравился ZX81-Debugger, это замечательный инструмент! Достаточно просто установить его, и вы сразу получаете полнофункциональную среду разработки для ZX81. Я форкнул код и начал адаптировать его под работу с приложениями E-Reader. Так как для обеих платформ процессор z80 был общим, это оказалось не так сложно, как я предполагал.

После долгих выходных хакинга я, к своему удивлению, получил отладчик E-Reader, работающий в VS Code! В конечном итоге меня приятно поразило то, насколько быстро у меня получилось заставить его работать. Я поистине стою на плечах великанов… поэтому благодарю всех тех, кто сделал это возможным.

The E-Reader debugger running in VS Code

Отладчик E-Reader, работающий в VS Code

Изображение в полном размере.

Чтобы всё это заработало, я убрал бóльшую часть того, что относилось к ZX81, а затем написал простой эмулятор ERAPI. При поступлении вызовов ERAPI отладчик отправляет их в мой маленький эмулятор, который затем транслирует их на графический экран GBA.

A close up of the ERAPI emulator screen output

Экранный вывод эмулятора ERAPI

Фон имеет зелёный цвет, потому что я пока не добавил основную часть функций API, связанных с фоном. А ниже показан экран, на который я сбрасываю дамп текущего состояния всех спрайтов, созданных через ERAPI.

Можно взять даже выпущенную для E-Reader игру и запустить её в отладчике. Он дизассемблирует двоичный файл и позволит удобно его отлаживать. Это будет полезно для дальнейшего изучения работы карт E-Reader и ERAPI.

An official Nintendo E-Reader card, running in the debugger

Официальная карта Nintendo E-Reader, запущенная в отладчике

Все цвета на этой карте Kirby странные, потому что мой эмулятор ERAPI ещё крайне сырой и многое делает неправильно. Он отрисовывает изображение, используя не те палитры. Предстоит ещё много работы.

Это просто потрясающе! Я никогда не думал, что смогу достичь такого удобства отладки на забытой двадцатилетней периферии Nintendo. Мы живём в потрясающие времена.

В конечном итоге я выложу E-Reader-Debugger в опенсорс. Но на текущем этапе он по качеству не дотягивает даже до альфы. После того, как я разберусь в некоторых тонкостях, он попадёт на GitHub.

Сложности с ERAPI API

В целом ERAPI устройства E-Reader очень полезен, в нём есть много возможностей, упрощающих жизнь разработчика. Однако я обнаружил кое-что неработающее, или из-за багов, или потому, что ещё не разобрался, как его правильно использовать. Надеюсь, что второе.

У меня возникали очень серьёзные проблемы при рендеринге игрового поля Solitaire. GBA — это старая система, и у неё есть ограничения по отрисовке спрайтов на экране. Нельзя одновременно помещать на экран слишком много, в противном случае может происходить вот такое:

В этом видео показан Solitaire, когда я только начал работать над ним. Мне хотелось проверить, смогу ли я использовать спрайты для отрисовки всех карт. Выяснилось, что не могу, и придётся отрисовывать карты в виде фона. Подобное использование фона для графики очень часто применялось на старых игровых системах. К счастью, в ERAPI есть функция SpriteDrawOnBackground, которая вроде бы как раз для этого и предназначена.

При помощи этой функции я смог легко отрисовывать спрайты в фоне и избегать графических глитчей… в первый раз, когда отрисовывалось игровое поле. Когда оно отрисовывалось многократно, казалось, что тайлы в видеопамяти повреждаются

В этом видео я многократно перетасовывал и пересдавал колоду. И при каждой новой пересдаче возникали графические глитчи.

Я пробовал разное, но так и не смог заставить всё работать правильно. У меня не было уверенности, что не совершаю каких-то ошибок. Но ведь ERAPI и способ работы приложений z80 с эмулятором казались достаточно простыми, поэтому я думаю, что эта функция не предназначена для такого быстрого применения.

В ERAPI также есть функция LoadCustomBackground, и именно она обеспечила мне нужный результат. Она более низкоуровневая и сложная в использовании по сравнению с SpriteDrawOnBackground, но зато ни разу не создавала графических глитчей.

При использовании этой функции я сам должен разбираться в том, куда должны идти какие тайлы для формирования фона. Чтобы сделать это, нужно понимать, как на GBA работают фоны. После того, как я разобрался, достаточно было отправлять всё GBA в одной функции, и оно отображалось на экране.

Приложения z80 для E-Reader похожи на скрипты

Оказалось, что приложения z80 для E-Reader работают довольно любопытным образом. Эмулятор использует опкод halt, чтобы сказать «нарисуй кадр на экране». Можно загрузить в регистр a количество отрисовываемых кадров, что позволяет очень простым способом добавлять в игру ожидание.

Возьмём для примера эту анимацию. Я создал её так:

    logoHandle = createSprite(logo);
    setSpritePosition(logoHandle, 120, 20);
    footerHandle = createSprite(footer);
    setSpritePosition(footerHandle, 120, 60)
    playSystemSound(DRUM_ROLL);
    for (let i = 0; i < NUM_CARDS_TO_DEAL) {
        dealOneCard(i);
        // рендерим один кадр, чтобы показать новую сданную карту
        halt(1);
    }
    playSystemSound(CYMBAL);
    // ждём 30 кадров
    halt(30)
    // стираем логотипы
    freeSprite(logoHandle);
    freeSprite(footerHandle);
    // дальше идёт обычный геймплейный цикл

В реальной игре всё это делается на языке ассемблера, я перевёл всё в псевдокод для простоты чтения.

Любопытно, что здесь для простой сдачи карт я прямо на месте создал типичный цикл игрового движка. Мне не нужно было подключать это всё к основному циклу игры, как это часто бывает в разработке игр. Остальная часть игры не знает о происходящем и её это не волнует.

Ассеты E-Reader

Вы могли заметить, что в моей игре есть музыка, звуковые эффекты и каменистый фон, напоминающий поверхность Марса. В самом E-Reader есть множество ассетов, которые может использовать игра. Это позволяет уменьшить объём данных в штрих-коде. Можно добавлять собственную графику (например колоду карт в моей игре) и звуки, но они обычно довольно большие и занимают драгоценное место.

Some of the backgrounds found on the E-Reader

Некоторые из фонов, имеющихся в E-Reader

Всего в устройстве есть более ста фонов, более восьмисот звуков (и звуковых эффектов, и музыки) и более двухсот спрайтов покемонов, хранящихся в ROM E-Reader размером 8 МБ. Если вы любитель E-Reader, то могли заметить, что в мини-играх Nintendo часто используются одни и те же звуковые эффекты, музыка и часто одинаковые фоны. Теперь вы знаете причину.

Обычно для их использования достаточно одного вызова API. Например, вот как я воспроизвожу звуковой эффект барабанной дроби:

    ld hl, 755
    rst 8
    .db ERAPI_PlaySystemSound

Если закрыть глаза на безумный синтаксис языка ассемблера, то это практически PlaySystemSound(755), где 755 — это идентификатор барабанной дроби.

Насколько большими могут быть приложения для E-Reader?

Одна полоска штрих-кода E-Reader может содержать 2192 байта данных, то есть чуть больше 2 КБ. Но дополнительное место также занимают заголовки и корректировка ошибок, так что максимально мне удавалось сохранить в одной полоске чуть меньше 2 КБ. Впрочем, эти данные штрих-кода сжаты, что сильно помогает.

An example dotcode strip

Пример полосы штрих-кода

A closer view at part of the dotstip

Увеличенный фрагмент штрих-кода

Из-за сжатия общий размер хранилища становится нечётким. Он сильно зависит от того, насколько хорошо сжимаются ваши данные. Например, вот тайлы для моих карт:

The card graphic tiles for Solitaire

Тайлы графики карт для Solitaire

Я бы мог сэкономить пространство, не повторяя один и тот же тайл снова и снова. Но такая схема сжимается очень хорошо. Настолько хорошо, что я просто остановился на ней. Избавление от повторений в этих тайлах сильно усложнило бы подпрограммы отрисовки; возможно, это бы даже уничтожило всю экономию пространства, которой мне удалось достичь.

В конечном итоге оказалось, что мне не нужно было слишком много оптимизаций, чтобы уместить Solitaire на двух полосах штрих-кода (которые можно напечатать на одной карте). Я действительно занял всё пространство, которое было доступно на этих двух полосах, поэтому любые новые возможности, вероятно, потребовали бы записи игры на три полосы, чего я сильно старался избежать.

Честно говоря, я немного удивлён тем, сколько полос требуется для некоторых игр Nintendo. Судя по самой игре и моему опыту написания Solitaire, некоторые из игр компании могли бы уместиться на меньшем количестве полос. Но это лишь предположение, наверняка я не знаю.

E-Reader и ограничения GBA по пространству

Сам E-Reader позволяет сканировать для одного приложения не более двенадцати полос. Впрочем, мне пока не удалось найти ни одного приложения, которому бы требовалось столько полос. Играм для NES требуется десять:

The E-Reader waiting for 9 more strips to load Excitebike

E-Reader ждёт ещё девять полос, чтобы загрузить Excitebike

Что же касается самого Game Boy Advance, то у него есть 256 КБ ОЗУ, чего более чем достаточно для хранения любого приложения E-Reader. Устройство E-Reader распаковывает данные в ОЗУ, а затем выполняет его. Я предполагаю, что теоретически объём данных может превзойти при распаковке 256 КБ, но в реальности никогда такого не видел.

Новые карты E-Reader

Я поставил перед собой задачу выпускать новые приложения для E-Reader. Я уже довольно далеко продвинулся в создании следующей игры. Надеюсь собрать как минимум десяток приложений и изготовить карты на профессиональном оборудовании. Было бы здорово ещё упаковать их в единую коллекцию. Зачем? А почему бы и нет. Я считаю, что E-Reader — это очень крутое устройство.

Если вас интересуют новые карты для E-Reader, то загляните на https://retrodotcards.com.

© Habrahabr.ru