Несколько недель назад я решила поработать над игрой для Game Boy, создание которой доставило мне большое удовольствие. Её рабочее название «Aqua and Ashes». Игра имеет открытые исходники и выложена на GitHub.
Как мне пришла в голову эта идея
Недавно я получила работу в интернатуре по созданию бэкенда на PHP и Python для веб-сайта моего университета. Это хорошая и интересная работа, за которую я очень благодарна. Но… в то же время весь этот высокоуровневый код веб-разработки заразил меня неутолимым стремлением. И это было стремление к низкоуровневой работе с битами.
Мне на почту пришёл еженедельный дайджест itch.io о гейм-джемах, в котором объявлялось начало Mini Jam 4. Это был 48-часовой (ну, на самом деле чуть больше) джем, в котором ограничением было создание графики в стиле Game Boy. Моей первой вполне логичной реакцией стало желание создать homebrew-игру для Game Boy. Темой джема были «времена года» и «пламя».
Немного подумав над сюжетом и механиками, которые можно реализовать за 48 часов и вписывающиеся в ограничения темы, я придумала
Мне всегда нравилось, как создатели этого уровня взяли невероятно сложный вид спорта, избавились от всех хитростей, позиций и стратегических элементов, в результате получив чрезвычайно интересную и лёгкую игру. Очевидно, что такой упрощённый взгляд на американский футбол не заменит вам Madden, так же, как NBA Jam (аналогичная идея: всего 4 игрока на гораздо меньшем поле с более прямолинейным геймплеем, чем в обычной игре) не заменит серию 2K. Но у этой идеи есть определённое очарование, и цифры продаж NBA Jam подтверждают это.
Как всё это относится к моей идее? Я задумала взять этот футбольный уровень и переделать его, чтобы он оставался похожим на оригинал и в то же время был свежим. Во-первых, я урезала игру всего до четырёх игроков — по одному защитнику и нападающему на команду. В основном это сделано из-за ограничений «железа», но в то же время это позволит мне немного поэкспериментировать с более умным ИИ, не ограничивающимся принципом «беги влево и иногда подпрыгивай» из игры на SNES.
Ради соответствия теме я заменю ворота на горящие колонны, или на костры, или на что-то подобное (пока не решила), а футбольный мяч — на факелы и вёдра с водой. Победителем будет команда, контролирующая оба костра, и вокруг этой простой концепции легко можно придумать сюжет. Времена года тоже учитываются: я решила что сезоны будут меняться на каждом ходу, чтобы огненная команда получала преимущество летом, а противопожарная команда — зимой. Это преимущество выглядит как препятствия на поле, мешающие только команде противника.
Разумеется, при создании двух команд нужны были два животных, которые любят и не любят огонь. Сначала я подумала об огненных муравьях и каком-нибудь водном жуке, богомоле и тому подобном, но изучив вопрос, не нашла насекомых, активных зимой, поэтому заменила их на полярных лис и гекконов. Полярные лисы любят снег, гекконы любят лежать на солнце, поэтому всё кажется логичным. В конце концов, это просто игра для Game Boy.
Кроме того, на случай, если это ещё непонятно, к концу джема игра и близко не была к завершению. Ну да ладно, всё равно было весело.
Подготовка Game Boy
Для начала нужно определиться с требованиями. Я решила писать для DMG (внутреннее название модели Game Boy, сокращение от Dot Matrix Game). В основном для того, чтобы соответствовать требованиям гейм-джема, но ещё и потому, что мне так хотелось. Лично у меня никогда не было игр для DMG (хотя и есть несколько игр для Game Boy Color), но я нахожу 2-битную эстетику очень милым и интересным ограничением для экспериментов. Возможно, я добавлю дополнительный цвет для SGB и CGB, но пока над этим не думала.
Также я решила использовать картридж с 32K ПЗУ + без ОЗУ, просто на случай, если мне захочется создать физическую копию игры. У CatSkull, опубликовавшего несколько игр Game Boy, например Sheep it Up!, есть в продаже очень дешёвые 32-килобайтные флеш-картриджи, которые идеально мне подойдут. Это ещё одно дополнительное ограничение, но я не считаю, что в ближайшее время смогу с такой простой игрой преодолеть объём в 32K. Сложнее всего будет с графикой, и если всё будет совсем плохо, то я попытаюсь её сжать.
Что касается самой работы Game Boy, то тут всё достаточно сложно. Однако, честно говоря, из всех ретроконсолей, с которыми мне приходилось работать, Game Boy была самой приятной. Я начала с превосходного туториала (по крайней мере, на первое время, потому что он так и не был дописан) автора «AssemblyDigest». Я знала, что лучше всего писать на ASM, как бы мучительно это иногда ни было, потому что «железо» не рассчитано на C, и я не была уверена, что упоминаемый в туториале крутой язык Wiz окажется применимым на долгосрочную перспективу. Плюс, я делаю это в основном потому, что могу работать с ASM.
Сверяйтесь с коммитом 8c0a4ea
Первое, что нужно было сделать — заставить Game Boy загружаться. Если по смещению
$104
не будет найден логотип Nintendo, а остальная часть заголовка не будет настроена правильно, то оборудование Game Boy предположит, что картридж вставлен неправильно и откажется загружаться. Решить эту проблему очень просто, потому что об этом написана уже куча туториалов. Вот, как решила проблему с заголовком я. Здесь нет ничего достойного особого внимания.Сложнее будет выполнять осмысленные действия после загрузки. Очень просто заставить систему перейти в бесконечный цикл занятости, в котором она снова и снова выполняет одну строку кода. Выполнение кода начинается с метки
main
(куда указывает переход по адресу $100
), поэтому туда нужно вставить какой-нибудь простой код. Например:main:
.loop:
halt
jr .loop
и он не делает ровным счётом ничего, кроме как ждёт запуска прерывания, после чего возвращается к метке
.loop
. (Здесь и далее я буду опускать подробное описание работы ASM. Если вы запутаетесь, то изучите документацию по ассемблеру, которую я использую.) Возможно, вам любопытно, почему я просто не возвращаюсь к метке main
. Это сделано потому, что я хочу, чтобы всё до метки .loop
было инициализацией программы, а всё, что после неё, происходило каждый кадр. Таким образом мне не придётся обходить в цикле загрузку данных с картриджа и очищать память в каждом кадре.Давайте сделаем ещё один шаг. Используемый мной ассемблерный пакет RGBDS содержит конвертер изображений. Так как на этом этапе я пока не нарисовала никаких ресурсов для игры, то решила использовать в качестве тестового битового изображения монохромную кнопку с моей страницы About. С помощью RGBGFX я преобразовала её в формат Game Boy и воспользовалась ассемблерной командой .incbin, чтобы вставить её после функции
main
.Чтобы отобразить её на экране, мне необходимо следующее:
- Отключить ЖК-дисплей
- Задать палитру
- Задать позицию скроллинга
- Очистить видеопамять (VRAM)
- Загрузить во VRAM тайловую графику
- Загрузить во VRAM тайловую карту фона
- Снова включить ЖК-дисплей
Отключение ЖК-дисплея
Для начинающих это становится самым серьёзным препятствием. На первом Game Boy невозможно просто в любое время записывать данные во VRAM. Необходимо дождаться момента, когда система ничего не отрисовывает. Имитируя свечение фосфора в старых ЭЛТ-телевизорах, интервал между каждым кадром, когда открыта VRAM, назван Vertical-Blank, или VBlank (в ЭЛТ это импульс для гашения луча кинескопа во время обратного хода кадровой развёртки). (Существует также HBlank между каждой строкой дисплея, но он очень короткий.) Однако можно обойти эту проблему, отключая ЖК-экран, то есть мы можем выполнять запись во VRAM вне зависимости от того, где находится «фосфорный след» ЭЛТ-экрана.
Если вы запутались, то этот обзор должен многое вам объяснить. В нём вопрос рассматривается с точки зрения SNES, поэтому не забывайте, что пучка электронов нет, а числа отличаются, но во всём остальном он вполне применим. По сути, нам нужно задать флаг «FBlank».
Однако хитрость Game Boy в том, что отключать ЖК-дисплей можно только во время VBlank. То есть нам придётся ждать VBlank. Для этого необходимо использовать прерывания. Прерывания — это сигналы, которые «железо» Game Boy отправляет центральному процессору. Если обработчик прерывания задан, то процессор останавливает свою работу и вызывает обработчик. Game Boy поддерживает пять прерываний, и одно из них запускается при начале VBlank.
Обрабатывать прерывания можно двумя разными способами. Первый, и наиболее распространённый — задание обработчика прерываний, который работает так, как я объяснила выше. Однако мы можем включить определённое прерывание и отключить все обработчики, задав флаг включения этого прерывания и воспользовавшись опкодом
di
. Обычно он ничего не делает, но имеет побочный эффект выхода из опкода HALT, останавливающего ЦП до возникновения прерывания. (Это также происходит и при включенных обработчиках, что позволяет нам выходить из цикла HALT в main
.) На случай, если вам интересно, мы со временем создадим и обработчик VBlank, но в нём многое будет зависеть от определённых значений по определённым адресам. Поскольку в ОЗУ у нас пока ничего не задано, попытка вызова обработчика VBlank может привести к сбою системы.Чтобы задать значения, мы должны отправлять команды аппаратным регистрам Game Boy. Существуют специальные адреса памяти, непосредственно связанные с различными частями оборудования, в нашем случае — с ЦП, которые позволяют изменять образ его работы. Особо нас интересуют адреса
$FFFF
(битовое поле включения прерывания), $FF0F
(битовое поле активированного, но необработанного прерывания) и $FF40
(управление ЖК-дисплеем). Список этих регистров можно найти на страницах, связанных с разделом «Documentation» списка Awesome Game Boy Development.Для отключения ЖК-дисплея мы включаем только прерывание VBlank, присвоив
$FFFF
значение $01
, выполняем HALT пока не выполнится условие $FF0F == $01
, а затем присваиваем биту 7 адреса $FF40
значение 0.Задание палитры и позиции скроллинга
Это сделать просто. Теперь, когда ЖК-дисплей отключен, нам не нужно волноваться о VBlank. Для задания позиции скроллинга достаточно задать регистрам X и Y значения 0. С палитрой всё немного хитрее. В Game Boy можно присвоить оттенкам с первого по четвёртый графики любой из 4 оттенков серого (или болотно-зелёного, если хотите), что полезно для выполнения переходов и тому подобного. Я задаю в качестве палитры простой градиент, определяемый как список битов
%11100100
.Очистка VRAM и загрузка тайловой графики
При запуске все графические данные и карта фона будут состоять только из скроллящегося логотипа Nintendo, который отображается при загрузке системы. Если я включу спрайты (по умолчанию они отключены), то они будут разбросанным по экрану мусором. Необходимо очистить видеопамять, чтобы начать с чистого листа.
Для этого мне потребуется функция наподобие
memset
из C. (Также мне понадобится аналог memcpy
для копирования данных графики.) Функция memset
задаёт указанному фрагменту памяти равенство определённому байту. Это мне будет легко реализовать самой, но в туториале AssemblyDigest уже есть эти функции, поэтому я использую их.На этом этапе я могу очистить VRAM с помощью
memset
, записав в неё $00
(хотя в первом коммите использовалось значение $FF
, которое тоже подходило), а затем загрузить во VRAM тайловую графику с помощью memcpy
. Конкретнее мне нужно скопировать её по адресу $9000
, потому что это тайлы, используемые только для фоновой графики. (адреса $8000-$87FF
используются только для спрайтовых тайлов, а адреса $8800-$8FFF
являются общими для обоих типов.)Задание тайловой карты
Game Boy имеет один слой фона, разделённый на тайлы 8x8. Сам слой фона занимает около 32x32 тайлов, то есть имеет общий размер 256x256. (Для сравнения: экран консоли имеет разрешение 160x144.) Мне необходимо было строка за строкой вручную указывать тайлы, из которых состоит моё изображение. К счастью, все тайлы были расположены по порядку, поэтому мне всего лишь нужно было заполнять каждую строку значениями с
N*11
по N*11 + 10
, где N
— это номер строки, а остальные 22 элемента тайлов заполнить $FF
.Включение ЖК-дисплея
Здесь нам не нужно ждать VBlank, потому что экран всё равно не включится до VBlank, поэтому я просто снова выполнила запись в регистр управления ЖК-дисплеем. Также я включила слои фона и спрайтов, а также указала правильные адреса тайловой карты и тайловой графики. После этого я получила следующие результаты. Также я снова включила обработчики прерываний с помощью опкода
ei
.На этом этапе, чтобы было ещё интереснее, я написала очень простой обработчик прерывания для VBlank. Добавив по адресу
$40
опкод перехода, я могу сделать обработчиком любую нужную мне функцию. В данном случае я написала простую функцию, выполняющую скроллинг экрана вверх-влево.Вот готовые результаты. [Дополнение: только что поняла, что GIF зациклен неправильно, он должен постоянно переносить изображение.]
Пока ничего особо удивительного, но всё равно здорово, что теоретически я могу достать свой старый Game Boy Color и увидеть, как на нём выполняется мой собственный код.
Забавы с листами в клетку
Чтобы отрисовывать что-нибудь на экране, мне, естественно, нужны какие-то спрайты. Изучив PPU (Picture Processing Unit) консоли Game Boy, я решила остановиться на спрайтах размером 8x8 или 8x16. Вероятно, мне понадобится последний вариант, но просто чтобы ощутить размеры, я быстро набросала на клетчатой бумаге скриншот игры в масштабе 1:8.
Я хотела оставить верхнюю часть экрана под HUD. Мне казалось, так он будет выглядеть естественней, чем снизу, потому что когда он наверху, то если персонажам нужно будет временно перекрыть HUD, как в Super Mario Bros, они смогут это сделать. В этой игре не будет какого-то сложного платформинга, да и на самом деле дизайна уровней тоже, поэтому мне не нужно показывать сильно общий вид поля. Вполне достаточно будет позиции персонажей на экране и, возможно, появляющихся время от времени препятствий. Поэтому я могу позволить себе достаточно большие спрайты.
Итак, если один квадрат был одним тайлом 8x8, то одного спрайта не будет достаточно, какой бы размер я ни выбрала. Это особенно справедливо с учётом того, что в игре почти не будет движения по вертикали, за исключением прыжков. Поэтому я решила создавать спрайты из четырёх спрайтов размером 8x16. Исключение составил хвост лисы, занимающий два спрайта 8x16. После простых подсчётов стало понятно, что две лисы и два геккона займут 20 из 40 спрайтов, то есть можно будет добавить ещё много дополнительных спрайтов. (Спрайты размером 8x8 быстро бы исчерпали мой лимит, чего не хочется делать на ранних этапах разработки.)
Пока мне нужно только отрисовать спрайты. Ниже представлены грубые эскизы на клетчатой бумаге. У меня есть спрайт ожидания, «думающий» спрайт для выбора, нужно ли сделать пас или бежать, как в игре на SNES… и на этом всё. Я планировала ещё сделать спрайты бегущих персонажей, прыгающих персонажей и персонажей, которых хватают противники. Но для начала я нарисовала только ожидающий и думающий спрайты, чтобы не усложнять. Остальные я по-прежнему не сделала, надо этим заняться.
Да, знаю, рисую я не очень хорошо. Перспектива — сложная штука. (Да и эта морда полярной лисицы ужасна.) Но меня это вполне устраивает. Дизайн персонажей не имеет каких-то особых черт, но для гейм-джема подходит. Разумеется, я использовала в качестве референсов настоящих гекконов и полярных лис. Разве незаметно?
Не отличишь. (Для протокола: только что снова посмотрев на эти картинки, я осознала, что между гекконами и ящерицами есть огромная разница. Не знаю, что с этим делать, кроме как считать себя глупой...) Думаю, можно догадаться, что источником вдохновения для головы лисы служил Blaze the Cat из серии игр про Соника.
Изначально я хотела, чтобы защитники и нападающие в каждой команде были разного пола и их было легче различать. (Ещё я собиралась позволить игрокам выбирать пол своего персонажа.) Однако для этого потребовалось бы гораздо больше рисовать. Поэтому я остановилась на гекконах мужского пола и лисах женского.
И, наконец, я нарисовала экран заставки, потому что для него осталось место на листе клетчатой бумаги.
Да, позы действий ещё далеки от идеала. Полярный лис должен быть более расстроенным и бежать, а геккон выглядеть угрожающе. Защитник-лис на заднем плане — забавная отсылка к арту на коробке Doom.
Оцифровка спрайтов
Затем я приступила к превращению бумажных рисунков в спрайты. Для этого я использовала программу GraphicsGale, которую недавно сделали бесплатной. (Знаю, можно было пользоваться и asesprite, но я предпочитаю GraphicsGale.) Работа над спрайтами оказалась гораздо сложнее, чем я ожидала. Каждый из этих квадратов из показанных выше спрайтов занимает до 4 пикселей в сетке 2x2. И в этих квадратах часто было НАМНОГО больше деталей, чем в 4 пикселях. Поэтому мне пришлось избавиться от множества деталей эскизов. Иногда даже было сложно придерживаться простой формы, потому что нужно было оставить место допустим для глаз или носа. Но мне кажется, что всё выглядит неплохо, даже если спрайт стал совершенно другим.
Глаза лисы потеряли свою миндалевидную форму и превратились в линию высотой два пикселя. Глаза геккона сохранили свою округлость. Голову геккона пришлось увеличить, избавившись от широких плеч, а все изгибы, которые могла иметь лиса, существенно сглажены. Но честно говоря, все эти лёгкие изменения не так плохи. Иногда мне с трудом удавалось выбрать, какая из вариаций лучше.
В GraphicsGale также есть удобные функции слоёв и анимаций. Это значит, что я могу анимировать хвост лисы отдельно от её тела. Это очень помогает экономить драгоценное пространство VRAM, потому что мне не нужно дублировать хвост в каждом кадре. Кроме того, это означало, что можно вилять хвостом с переменной скоростью, замедляясь, когда персонаж стоит, и ускоряясь при беге. Однако при этом немного усложняется программирование. Но я всё же возьмусь за эту задачу. Я остановилась на 4 кадрах анимации, потому что этого достаточно.
Можно заметить, что в полярном лисе используются три самых светлых оттенка серого, а в гекконе — три самых тёмных. На GameBoy это допустимо, потому что хотя в спрайте может быть всего три цвета, консоль позволяет задавать две палитры. Я сделала так, что для лисы используется палитра 0, а для геккона — палитра 1. На этом весь доступный набор палитр закончился, но я не думаю, что мне понадобятся другие.
Также мне нужно было позаботиться о фоне. Я не стала заморачиваться его эскизами, потому что планировала, что он будет сплошным цветом или простым геометрическим узором. Экран заставки я тоже пока не оцифровала, потому что не хватило времени.
Загрузка спрайтов в игру
Сверяйтесь с коммитом be99d97.
После того, как каждый отдельный кадр графики персонажей был сохранён, можно было начинать преобразовывать их в формат GameBoy. Оказалось, что в RGBDS для этого есть очень удобная утилита под названием RGBGFX. Её можно вызвать командой
rgbgfx -h -o output.bin input.png
и она создаст совместимый с GameBoy набор тайлов. (Ключ -h задаёт режим тайлов, совместимый с размером 8x16, чтобы преобразование выполнялось сверху вниз, а не слева направо.) Однако он не обеспечивает привязки и не может отслеживать дублирующиеся тайлы, когда каждый кадр является отдельной картинкой. Но эту проблему мы оставим на потом.После генерации выходных файлов .bin достаточно просто добавить их в ассемблере с помощью
incbin "output.bin"
. Чтобы держать всё вместе, я создала общий файл «gfxinclude.z80», в котором содержится вся добавляемая графика.Тем не менее, было очень скучно каждый раз вручную заново генерировать графику, когда что-нибудь изменится. Поэтому я отредактировала файл build.bat, добавив строку
for %%f in (gfx/*.png) do rgbds\rgbgfx -h -o gfx/bin/%%f.bin gfx/%%f
, которая преобразует каждый файл .png в папке gfx/ в файл bin и сохраняет его в gfx/bin. Это сильно упростило мою жизнь.Для создания графики фона я использовала гораздо более ленивый способ. У RGBASM есть директива
dw `
. За ней следует строка 8 значений от 0 до 4, равных одной строке пиксельных данных. Так как спрайты фона были очень простыми, оказалось проще копировать и вставлять простой геометрический узор для создания сплошного, полосатого или шахматного узора. Вот, например, как выглядит тайл земли.
bg_dirt:
dw `00110011
dw `00000000
dw `01100110
dw `00000000
dw `11001100
dw `00000000
dw `10011001
dw `00000000
Он создаёт серию сдвинутых полосок с иллюзией перспективы. Это простой, но умный подход. С травой всё было чуть сложнее. Изначально она была группой горизонтальных линий высотой 2 пикселя, но я вручную добавила несколько пикселей, придающих немного шума, с которым трава выглядит лучше:
bg_grass:
dw `12121112
dw `12121212
dw `22112211
dw `11121212
dw `22112211
dw `21212121
dw `12121212
dw `12211222
Рендеринг графики
В памяти GameBoy спрайты хранятся в области под названием OAM, или Object Attribute Memory. Она содержит только атрибуты (направление, палитру и приоритет), а также номер тайла. Мне было достаточно было заполнить эту область памяти, чтобы отобразить спрайты на экране.
Хотя здесь есть небольшие особенности. Во-первых, необходимо загрузить графику из ПЗУ во VRAM. GameBoy может рендерить только те тайлы, которые хранятся в особой области памяти, называемой VRAM. К счастью, для копирования из ПЗУ во VRAM достаточно выполнить
memcpy
на этапе инициализации программы. При этом выяснилось, что всего 6 спрайтами персонажей и 4 тайлами хвостов я уже заняла четверть выделенной под спрайты области VRAM. (VRAM обычно разделена на области фона и спрайтов, а 128 байт являются общими для них.)Кроме того, доступ к OAM возможен только во время VBlank. Я начала с того, что перед выполнением вычислений спрайтов дожидалась VBlank, но столкнулась с проблемами, потому что вычисления спрайтов растянулись на всё выделенное VBlank время и их невозможно было закончить. Решение здесь заключается в том, чтобы выполнять запись в отдельную область памяти за пределами VBlank и просто копировать их в OAM во время VBlank.
Как оказалось, у GameBoy есть специальная аппаратная процедура копирования, своего рода DMA (Direct Memory Access, прямой доступ к памяти), которая занимается именно этим. Выполнив запись в определённый регистр и перейдя к циклу занятости в HiRAM (потому что во время DMA ПЗУ недоступно), можно скопировать данные из ОЗУ в OAM гораздо быстрее, чем с помощью функции
memcpy
. Если интересно, то сочные подробности можно узнать здесь.На этом этапе мне оставалось только создать процедуру, определяющую, что же в конце концов будет записано в DMA. Для этого мне нужно было хранить где-то в другом месте состояние объектов. Как минимум, требовалось следующее:
- Тип (геккон, полярная лиса или переносимый предмет одной из команд)
- Направление
- Позиция по X
- Позиция по Y
- Кадр анимации
- Таймер анимации
В первом, очень неряшливом решении я проверяла тип объекта, и в зависимости от него выполняла переход к процедуре, поспрайтово отрисовывающей данный тип объекта. Процедура полярной лисы, например, брала позицию по X, в зависимости от направления прибавляла или вычитала 16, добавляла два спрайта хвоста, а затем перемещалась вверх и вниз по основному спрайту.
Вот скриншот того, как выглядел во VRAM спрайт при отрисовке на экране. Левая часть — это отдельные спрайты, шестнадцатеричные числа рядом с ними, сверху вниз — позиция по вертикали и горизонтали, тайл и флаги атрибутов. Справа видно, как всё это выглядело после сборки.
С анимацией хвоста всё было немного сложнее. В первом решении я просто выполняла в каждом кадре инкремент таймера анимации и производила логическое
and
со значением %11
для получения номера кадра. Затем можно было просто прибавить к первому тайлу хвоста во VRAM 4 * номер кадра (каждый кадр анимации состоит из 4 тайлов), чтобы получить 4 разных кадра, хранящихся во VRAM. Это работало (особенно та часть с поиском тайла хвоста), но хвост вилял безумно быстро, и мне нужно было найти способ его замедлить.Во втором, более качественном решении, я выполняла в каждом кадре инкремент глобального таймера, и когда значение операции
and
с ним и выбранной мной степенью двойки равнялось 0, выполнялся инкремент таймера объекта. Таким образом, каждый отдельный объект мог выполнять отсчёт своего таймера анимации с любой нужной ему скоростью. Это сработало отлично и позволило мне замедлить хвост до разумного уровня.Сложности
Но если бы всё было так просто. Не забывайте, что я управляла всем этим в коде, используя для каждого объекта собственную подпроцедуру, и если нужно было продолжать, то делать это необходимо в каждом кадре. Мне приходилось указывать, как переходить к следующему спрайту, а также из какого тайла он состоит, манипулируя регистрами вручную.
Это была совершенно неустойчивая система. Для отрисовки одного кадра необходимо было жонглировать достаточно большим количеством регистров и временем ЦП. Добавить поддержку других кадров было почти невозможно, и даже если бы мне это удалось, поддержка системы была бы очень мучительной. Поверьте, это был настоящий хаос. Мне требовалась система, в которой код рендеринга спрайтов был бы обобщённым и прямолинейным, чтобы он не представлял из себя переплетение условий, манипулирования регистрами и математических операторов.
Как мне удалось это исправить? Об этом я расскажу в следующей части статьи.