[Перевод] Создаём пасьянс для забытой периферии Nintendo
Недавно я закончил создание пасьянса для Nintendo E-Reader. Мне удалось уместить его на одной карте, и это практически полнофункциональная версия игры. Я очень доволен тем, что получилось.
Что такое E-Reader?
E-Reader — это периферийное устройство для Game Boy Advance, выпущенное компанией Nintendo в 2002 году. Сканируя карты, где есть полоска с кодом из точек, можно загружать мини-игры, дополнительные уровни, анимации и так далее.
E-Reader и одна из его карт
Мне всегда очень нравился E-Reader, и меня опечалило, что дела у него в Америке шли не особо хорошо. Поэтому я подумал, что, возможно, стоит попробовать писать для него игры самому.
И вот результат:
Если вы хотите попробовать поиграть, то можете получить карту на 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 в формате карт E-Reader
Сырые двоичные файлы
Сырые карты E-Reader просто содержат двоичные данные разных типов. Отдельные игры используют их для добавления уровней, персонажей и так далее. Это своего рода примитивная разновидность DLC. Конкретная игра сама интерпретирует данные, как ей удобно.
Подобные карты выпускались для Super Mario Advance 4, они добавляли новые уровни, бонусы и другой контент игры.
Карты уровней Super Mario Advance 4 для E-Reader
Приложения z80
И, наконец, E-Reader содержит простой эмулятор z80. Это 8-битный процессор, выпущенный ещё в 1976 году! Он был очень успешен и использовался во множестве разных компьютеров.
Мне кажется, Nintendo так никогда и не использовала процессор z80 ни в одной из своих игровых консолей, поэтому этот выбор интересен. Я уверен, что важным фактором для него была простота z80, его достаточно легко эмулировать.
Дополнение: позже мне сказали, что процессоры Game Boy и Game Boy Color были очень похожи на z80. Возможно, это повлияло на решение Nintendo. Я не знал этого, так что спасибо тем, кто сообщил мне об этом.
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! В конечном итоге меня приятно поразило то, насколько быстро у меня получилось заставить его работать. Я поистине стою на плечах великанов… поэтому благодарю всех тех, кто сделал это возможным.
Отладчик E-Reader, работающий в VS Code
Изображение в полном размере.
Чтобы всё это заработало, я убрал бóльшую часть того, что относилось к ZX81, а затем написал простой эмулятор ERAPI. При поступлении вызовов ERAPI отладчик отправляет их в мой маленький эмулятор, который затем транслирует их на графический экран GBA.
Экранный вывод эмулятора ERAPI
Фон имеет зелёный цвет, потому что я пока не добавил основную часть функций API, связанных с фоном. А ниже показан экран, на который я сбрасываю дамп текущего состояния всех спрайтов, созданных через ERAPI.
Можно взять даже выпущенную для E-Reader игру и запустить её в отладчике. Он дизассемблирует двоичный файл и позволит удобно его отлаживать. Это будет полезно для дальнейшего изучения работы карт E-Reader и ERAPI.
Официальная карта 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 есть множество ассетов, которые может использовать игра. Это позволяет уменьшить объём данных в штрих-коде. Можно добавлять собственную графику (например колоду карт в моей игре) и звуки, но они обычно довольно большие и занимают драгоценное место.
Некоторые из фонов, имеющихся в 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 КБ. Впрочем, эти данные штрих-кода сжаты, что сильно помогает.
Пример полосы штрих-кода
Увеличенный фрагмент штрих-кода
Из-за сжатия общий размер хранилища становится нечётким. Он сильно зависит от того, насколько хорошо сжимаются ваши данные. Например, вот тайлы для моих карт:
Тайлы графики карт для Solitaire
Я бы мог сэкономить пространство, не повторяя один и тот же тайл снова и снова. Но такая схема сжимается очень хорошо. Настолько хорошо, что я просто остановился на ней. Избавление от повторений в этих тайлах сильно усложнило бы подпрограммы отрисовки; возможно, это бы даже уничтожило всю экономию пространства, которой мне удалось достичь.
В конечном итоге оказалось, что мне не нужно было слишком много оптимизаций, чтобы уместить Solitaire на двух полосах штрих-кода (которые можно напечатать на одной карте). Я действительно занял всё пространство, которое было доступно на этих двух полосах, поэтому любые новые возможности, вероятно, потребовали бы записи игры на три полосы, чего я сильно старался избежать.
Честно говоря, я немного удивлён тем, сколько полос требуется для некоторых игр Nintendo. Судя по самой игре и моему опыту написания Solitaire, некоторые из игр компании могли бы уместиться на меньшем количестве полос. Но это лишь предположение, наверняка я не знаю.
E-Reader и ограничения GBA по пространству
Сам E-Reader позволяет сканировать для одного приложения не более двенадцати полос. Впрочем, мне пока не удалось найти ни одного приложения, которому бы требовалось столько полос. Играм для NES требуется десять:
E-Reader ждёт ещё девять полос, чтобы загрузить Excitebike
Что же касается самого Game Boy Advance, то у него есть 256 КБ ОЗУ, чего более чем достаточно для хранения любого приложения E-Reader. Устройство E-Reader распаковывает данные в ОЗУ, а затем выполняет его. Я предполагаю, что теоретически объём данных может превзойти при распаковке 256 КБ, но в реальности никогда такого не видел.
Новые карты E-Reader
Я поставил перед собой задачу выпускать новые приложения для E-Reader. Я уже довольно далеко продвинулся в создании следующей игры. Надеюсь собрать как минимум десяток приложений и изготовить карты на профессиональном оборудовании. Было бы здорово ещё упаковать их в единую коллекцию. Зачем? А почему бы и нет. Я считаю, что E-Reader — это очень крутое устройство.
Если вас интересуют новые карты для E-Reader, то загляните на https://retrodotcards.com.