Начало удивительного путешествия в мир программирования GameBoy - Novostroika
Не так давно я приобрел себе GameBoy, да не абы какой, а монохромный GameBoy DMG-01. И приобрел я его для того, чтобы сделать игру. Сперва я хотел разработать большую и комплексную игру, похожую на Legend of Zelda или Wolfenstein 3D, но понял, что три года кропотливой разработки я не выдержу (да, я понимаю, что это совершенно разные жанры). Поэтому я решил сделать клон Tower Bloxx. Эту игру знают все те люди моего поколения, которые играли в нее на мобильниках под партами.
Игру решил называть Novostroika, и да, это будет "симулятор" застройщика ПИК=)
На GameBoy можно делать игры по разному. Либо на ассемблере (что выбрал я), либо на Си (никогда не писал на Си что-то кроме Хэллоу Ворлд), либо вообще можно использовать (sic!) готовые движки, вроде GBStudio. Там даже программировать не надо - выставляй себе визуально логические блоки. Но это не наш выбор!
Скооперировавшись с Grongy (известный художник, который рисует на старое железо, в основном ZX Spectrum, и который нарисовал всю графику для моей игры на Commodore 64), мы приступили к работе.
Казалось бы, чего тут сложного, ну дом выезжает из под земли, ну анимация пыли, ну название выплывает вместе с мерцающей надписью Start. А занимает все это около 500 строчек кода на чистом ассемблере (учитывая комментарии, данные, и переиспользуемые функции).
Если говорить про характеристики, то GameBoy интересная штука:
- Процессор Sharp LR35902. Гибрид Zilog Z80 и Intel 8080, что выражается в отсутствии многих полезных команд Z80. Но зато были добавлены новые удобные вещи, например, запись/чтение по адресу в регистре HL с его одновременным инкрементом/декрементом. Причем эта команда выполняется за столько же тактов, сколько и обычная запись/чтение по адресу в HL.
- Процессор может адресовать 64 Кб памяти, но есть нюанс - ПЗУ картриджа (без мультибанкинга) всего 32 Кб, а ОЗУ (также без мультибанкинга) - всего 8 Кб. Остальная память - это регистры I/O (ввод-вывод) и VRAM (видеопамять). Есть даже отдельные зеркальные области памяти, про которые сама Nintendo в официальной документации пишет: "Use of this area is prohibited".
- Есть своеобразная "быстрая" память. Это область с адреса 0xFF00 по 0xFFFF, из которых первые 128 байт - это регистры I/O, а следующие 128 байт предназначены для программиста (не считая прерываний). Запись/чтение этой памяти быстрее на один такт, потому что она зашита в сам процессор. Так сказать, дополнительные внутренние регистры.
- Тайловая система отрисовки, где каждый тайл размером 8*8 пикселей и состоит из четырех цветов. Из этих же тайлов можно собрать спрайты, но один из цветов при этом всегда будет прозрачным. Зато для спрайтов есть аж две палитры по три цвета.
- Спрайтов всего 40 штук на кадр, но есть техники по увеличению этого количества в N раз (например, половина экрана - 40 спрайтов, вторая половина - еще 40 спрайтов). Но при этом на одной линии отрисовки не может быть более 10 спрайтов, и видеочип сам решает, какие спрайты отбросить. Помните знаменитое мерцание спрайтов на NES? Вот тут может быть такая же история.
- Экран - 160*144 пикселя, при этом видеопамять - 256*256 пикселей. Из этого следует очень простая механика скролла экрана - пишем в нужные регистры координаты X и Y и дорисовываем в видеопамяти нужные тайлы. После Commodore 64, где ничего такого не было, это просто мана небесная.
- Куча разных прерываний. И когда я говорю "куча", я это и имею в виду: прерывания на Vblank, на таймер, на линию отрисовки, на режим отрисовки, на джойстик и даже на Serial порт (специальный порт для обмена между двумя и больше консолями). Обычно в играх используются два вида прерываний: на Vblank и на линию отрисовки. И об этом я ниже немного расскажу.
Так как до этого из старой техники я программировал только Commodore 64, то буду сравнивать с ним.
Первое - процессор и система команд.
В Commodore 64 стоит процессор 6510 (аналог 6502 с дополнительными I/O пинами для переключения банков памяти). И это очень удобный для программиста процессор. В нем всего 3 регистра - Аккумулятор и индексные X и Y. Но насколько же удобно писать код. Например, мне надо создать массив. Я размещаю данные в памяти и запоминаю ширину массива, например 32 байта. Потом я создаю лукап таблицу на каждую строчку этого массива и с помощью индексных регистров и указателей получаю доступ к нужному байту.
С GameBoy так не получится. Тут совершенно другая логика работы, которая заключается в том, что у тебя аж семь регистров: аккумулятор и 6 регистров общего назначения, которые формируют регистровые 16-битные пары: HL, DE, BC.
Чтобы реализовать такой же массив, надо знать адрес начала этого массива и смещение. То есть, например, в регистр HL помещается адрес начала массива, в регистр DE - смещение в байтах (которое еще высчитать надо). И эти регистры просто суммируются. А что если у наc где-то зашиты переменные X и Y для этого массива? Тогда надо умножить Y на ширину массива (дай бог, если она кратна двум), а потом прибавить смещение по X. В каких-то случаях это удобно, но после 6502 я, честно говоря, страдаю.
Второе - видеочип.
В Commodore 64 очень интересный видеочип - он имеет доступ к той же памяти, что и процессор, но всего к 16 Кб за раз (и адрес начала видеопамяти можно менять). Он поддерживает очень много графических режимов: тайловый, попиксельный, и, самое интересное, multicolor, в котором на одно знакоместо 8*8 пикселей можно назначить аж четыре цвета из 16, включая цвет фона, но пиксели при этом широкие - 2 пикселя в ширину (потому что 2 бита на пиксель, а восемь пикселей определяет один байт).
И этот чип работает "as is" - захотели мы посреди строки поменять цвет пикселя или фона, или вообще спрайт передвинуть - видеочип это сожрет и выдаст нужный результат.
Кроме того, видеочип Commodore 64 поддерживает аппаратные спрайты - аж 8 штук на линию. И с помощью прерываний это число можно умножить практически на количество линий (очень сложно, но можно).
В GameBoy система совершенно другая. Во-первых, процессор не имеет доступ к видеопамяти 90% кадра. Только в периоды Vblank (возвращение к нулевой линии) или HBlank (возвращение к началу строки) мы можем что-то менять. Все это обычно реализуется с помощью прерываний (как, впрочем, и на Commodore 64). Основной код для отрисовки чего-либо обычно выполняется в период Vblank, но не всегда.
В качестве примера приведу мою заставку. Заметили, что перед появлением названия пыль постепенно темнеет, а потом удаляется с экрана? Это делается с помощью прерываний.
Когда видеочип начинает рисовать линию до пыли, то процессор прыгает в нужную функцию, которая ждет период Hblank, а потом в течение нескольких кадров затемняет основную палитру. А в период Vblank палитра возвращается к исходному значению. Таким образом появляется эффект, который распространяется всего на 16 линий экрана.
Третье - дискеты и картриджи.
О боги, как я ненавижу дискеты. В Commodore 64 было два варианта - дискеты или картриджи, но если хочешь сделать большую игру, то приходится пользоваться дискетами (либо картриджами с мультибанкингом, но компилятор не поддерживает такие вещи из коробки). А потом, когда пытаешься протестировать игру на реальном железе, то ждешь окончания загрузки минут десять. А все потому что Commodore делали очень медленные дисководы!
С GameBoy все идеально. Компилятор изначально поддерживает много-банковые картриджи, и ты сам можешь указать в каком банке, и какие данные, будут лежать. А тестирование - сказка. Я купил себе EZFlash Junior - картридж с SD карточкой, на которую можно заливать любые РОМы для GameBoy и GameBoy Color. И вроде как этот картридж даже GameBoy Advance поддерживает.
Компилируешь код, заливаешь на картридж и тут же тестируешь. Дело на две минуты.
В общем, про геймбой можно много что рассказать. Хотя бы то, что если неправильно запрограммировать выключение экрана (не выключение питания, а именно отключение видеочипа), то можно буквально сломать экран.
А о процессе разработки я буду писать тут, на Enthub, а еще в своем Телеграм канале (пусть это сейчас не самое популярное направление). И конечно, демо-версии я буду выкладывать на itch.io.
