Несколько недель назад я решила поработать над игрой для Game Boy, создание которой доставило мне большое удовольствие. Её рабочее название «Aqua and Ashes». Игра имеет открытые исходники и выложена на GitHub. Предыдущая часть статьи находится здесь.
Фантастические спрайты и где они обитают
В прошлой части я закончила рендеринг нескольких спрайтов на экран. Это было сделано очень произвольным и хаотичным образом. По сути, мне приходилось указывать в коде, что и где я хочу отображать. Это сделало создание анимации почти невозможным, тратило много времени ЦП и усложняло поддержку кода. Мне нужен был способ получше.
Если конкретно, то мне нужна была система, в которой бы я могла для каждой отдельной анимации просто выполнять итерацию номера анимации, номаре кадра и таймера. Если бы мне необходимо было изменить анимацию, я бы просто меняла анимацию и сбрасывала счётчик кадров. Процедура анимации, выполняемая в каждом кадре, должна просто выбирать подходящие для отображения спрайты и кидать их на экран без малейших усилий с моей стороны.
И как оказалось, эта задача практически решена. То, что я искала, называется картами спрайтов (sprite mappings). Карты спрайтов — это структуры данных, которые (грубо говоря) содержат список спрайтов. Каждая карта спрайтов содержит все спрайты для рендеринга одного объекта. Также с ними связаны карты анимаций (animation mappings), которые являются списками карт спрайтов с информацией о том, как из зацикливать.
Довольно забавно то, что ещё в мае я добавила редактор карт анимаций в уже готовый редактор карт спрайтов 16-битных игр про Соника. (Он находится здесь, можете изучить) Он ещё не завершён, потому что довольно шероховат, мучительно тормозит и неудобен в использовании. Но с технической точки зрения он работает. И мне кажется, что он довольно классный… (Одной из причин шероховатостей стало то, что я в буквальном смысле впервые работала с фреймворком JavaScript.) Sonic — это старая игра, поэтому идеально подходит в качестве фундамента для моей новой-старой игры.
Формат карт Sonic 2
Я намеревалась использовать редактор в Sonic 2, потому что хотела создать хак для Genesis. Sonic 1 и 3K в основном почти аналогичны, но чтобы не усложнять, я ограничусь рассказом про вторую часть.
Во-первых, давайте рассмотрим карты спрайтов. Вот довольно типичный спрайт Тейлза, часть анимации моргания.
Консоль Genesis создаёт спрайты немного иначе. Тайл на Genesis (большинство программистов называет его «паттерном») имеет размер 8×8, как и на Game Boy. Спрайт состоит из прямоугольника размером до тайлов 4×4, во многом похожего на режим спрайтов 8×16 на Game Boy, но более гибкого. Хитрость здесь в том, что в памяти эти тайлы должны находиться друг рядом с другом. Разработчики Sonic 2 хотели повторно использовать как можно больше тайлов для кадра моргающего Тейлза из кадра стоящего Тейлза. Поэтому Тейлз разделён на 2 аппаратных спрайта, состоящих из тайлов 3×2 — одного для головы, другого для тела. Они показаны на рисунке ниже.
Верхняя часть этого диалогового окна — это атрибуты аппаратных спрайтов. Она содержит их позицию относительно начальной точки (отрицательные числа отсекаются; на самом деле это -16 и -12 для первого спрайта и -12 для второго), начальный тайл, который используется во VRAM, ширину и высоту спрайта, а также различные биты состояний для зеркального отражения спрайта и палитры.
В нижней части показаны тайлы так, как они загружаются из ПЗУ во VRAM. Для хранения всех спрайтов Тейлза во VRAM недостаточно места, поэтому необходимые тайлы приходится копировать в память в каждом кадре. Они называются Dynamic Pattern Load Cues. Однако пока мы можем их пропустить, потому что они почти не зависят от карт спрайтов, а поэтому их легко можно добавить позже.
Что касается анимации, то здесь всё немного проще. Карта анимаций в Sonic — это список карт спрайтов с двумя фрагментами метаданных — значением скорости и действием, выполняемым после завершения анимации. Три самых часто используемых действия: цикл по всем кадрам, цикл по последним N кадрам или переход к совершенно другой анимации (например, при переходе от анимации стоящего Соника к анимации его нетерпеливого потопывания ногой). Ещё есть пара команд, задающих внутренние флаги в памяти объектов, но их использует не так много объектов. (Сейчас мне пришло в голову, что можно задавать биту в ОЗУ объекта значение при зацикливании анимации. Это будет полезно для звуковых эффектов и других вещей.)
Если посмотреть на дизассемблированный код Sonic 1 (код Sonic 2 слишком большой, чтобы ставить на него ссылку), то можно заметить, что ссылка на анимации не производится ни по какому ID. Каждому объекту задаётся список анимаций, а индекс анимации хранится в памяти. Для рендеринга конкретной анимации игра берёт индекс, ищет его в списке анимаций, а затем рендерит её. Это немного упрощает работу, потому что не нужно сканировать анимации, чтобы найти нужную.
Очищаем суп из структур
Давайте рассмотрим типы карт:
- Карты спрайтов: список спрайтов, состоящий из начального тайла, количества тайлов, позиции, состояния отражения (отзеркален спрайт, или нет) и палитры.
- DPLC: список тайлов из ПЗУ, которые нужно загрузить во VRAM. Каждый элемент в DPLC состоит из начального тайла и длины; каждый элемент размещается во VRAM после последнего.
- Карты анимаций: список анимаций, состоящий из списка карт спрайтов, значения скорости и действия цикла.
- Список анимаций: список указателей на действие каждой анимации.
Учитывая то, что мы работаем с Game Boy, можно внести некоторые упрощения. Мы знаем, что в картах спрайтов в спрайте 8×16 всегда будет два тайла. Однако всё остальное нужно сохранить. Пока мы можем полностью отказаться от DPLC и просто хранить всё во VRAM. Это временное решение, но, как я говорила, эту проблему легко будет решить. Наконец, мы можем отбросить значение скорости, если предположим, что каждая анимация работает с одинаковой скоростью.
Давайте начнём разбираться, как же реализовать подобную систему в моей игре.
Сверяетесь с коммитом 2e5e5b7!
Начнём с карт спрайтов. Каждый элемент в карте должен зеркально отображать OAM (Object Attribute Memory — спрайтовую VRAM) и таким образом для отображения объекта достаточно будет простого цикла и memcpy. Напомню, что элемент в OAM состоит из Y, X, начального тайла и байта атрибутов. Мне всего лишь нужно создать их список. С помощью ассемблерного псевдо-оператора EQU я заранее подготовила байт атрибутов, чтобы у меня было читаемое название для каждой возможной комбинации атрибутов. (Можно заметить, что в предыдущем коммите я заменила Y/X тайлом в картах. Это произошло, потому что я невнимательно прочитала спецификации OAM. Также я добавила счётчик спрайтов, чтобы знать, как долго выполнять цикл.)
Вы заметите, что тело и хвост полярной лисы хранятся по отдельности. Если бы они хранились вместе, то существовала бы большая избыточность, потому что каждую анимацию пришлось бы дублировать для каждого состояния хвоста. И масштабы избыточности быстро бы увеличивались. В Sonic 2 та же проблема возникла с Тейлзом. Там её решили, сделав хвосты Тейлза отдельным объектом с собственным состоянием анимации и таймером. Я не хочу этого делать, потому что не стремлюсь решать проблемы сохранения правильной позиции хвоста относительно лисы.
Я решила проблему через карты анимаций. Если посмотреть на мою (единственную) карту анимаций, то в ней есть три фрагмента метаданных. Там указывается количество карт анимаций, поэтому я знаю, когда они закончатся. (В Sonic проверяется, недействительность следующей анимации, аналогично концепции нулевого байта в строках C. Решение из Sonic освобождает регистр, но добавляет сравнение, которое бы сработало против меня.) Разумеется, есть ещё действие цикла. (Я превратила 2-байтную схему Sonic в 1-байтное число, в котором бит 7 является битом режима.) Но у меня есть ещё и количество карт спрайтов, а в Sonic его не было. Наличие нескольких карт спрайтов на один кадр анимации позволяет мне повторно использовать анимации в нескольких анимациях, что, как мне кажется, сэкономит много драгоценного места. Можно также заметить, что анимации дублированы для каждого направления. Так сделано потому, что карты для каждого направления отличаются, и их необходимо добавить.
Танцы с регистрами
Сверяйтесь с этим файлом в коммите 1713848.
Давайте начнём с отрисовки на экране единственного спрайта. Так, признаюсь, я соврала. Напомню, что мы не можем выполнять запись на экран за пределами VBlank. А весь этот процесс слишком долгий, чтобы уместить его во VBlank. Поэтому нам нужно выполнять запись области памяти, которую мы выделим для DMA. В конце концов это ничего не меняет, то важно выполнять запись в правильное место.
Давайте начнём считать регистры. В процессоре GBZ80 есть 6 регистров, от A до E, H и L. H и L — это особые регистры, поэтому они хорошо подходят для выполнения итераций по памяти. (Так как они используются вместе, их называют HL.) В одном опкоде я могу выполнить запись по адресу памяти, содержащемуся в HL, и прибавить к нему единицу. С этим сложно справиться. Можно использовать его или как источник, или как адресат. Я использовала его как адреса, а сочетание регистров BC как источник, потому что это было удобнее всего. У нас остаются только A, D и E. Регистр A нужен мне для математических операций и тому подобного. Для чего же можно использовать DE? Я использую D как счётчик цикла, а E — как рабочее пространство. И на этом регистры у нас закончились.
Допустим, у нас есть 4 спрайта. Регистру D (счётчику цикла) мы задаём значение 4, регистру HL (адресату) — адрес буфера OAM, а BC (источнику) — место в ПЗУ, в котором хранятся наши карты. Теперь мне бы хотелось вызвать memcpy. Однако возникает небольшая проблема. Помните координаты X и Y? Они указываются относительно начальной точки, центр объекта используется для коллизий и тому подобного. Если бы мы записывали их как есть, то каждый объект отображался бы в верхнем левом углу экрана. Это нас не устраивает. Чтобы это исправить, нам нужно прибавить к X и Y спрайта координаты X и Y объекта.
Краткое примечание: я рассказываю про «объекты», но не объяснила вам эту концепцию. Объект — это просто набор атрибутов, связанных с предметом в игре. Атрибуты — это позиция, скорость, направление. описание предмета и т.д. Я рассказываю об этом, потому что мне нужно вытащить из этих объектов данные X и Y. Для этого потребуется третий набор регистров, указывающих на то место в ОЗУ объектов, где находятся координатыare. И тогда нам нужно где-то хранить X и Y. То же относится и к направлению, потому что оно помогает нам определить, в какую сторону смотрят спрайты. Кроме того, нам нужно отрендерить все объекты, так что им тоже требуется счётчик цикла. И мы ещё не добрались до анимаций! Всё очень быстро выходит из под контроля…
Пересмотр решения
Так, я слишком забегаю вперёд. Давайте вернёмся назад и подумаем о каждом фрагменте данных, которые мне нужно отслеживать, и о том, куда их записать.
Для начала давайте разделим это на «этапы». Каждый этап должен только получать данные для следующего, за исключением последнего, который выполняет копирование.
- Объект (цикл) — выясняет, должен ли рендериться объект, и рендерит его.
- Список анимаций — определяет, какую анимацию нужно отображать. Также получает атрибуты объекта.
- Анимация (цикл) — определяет, какой список карт нужно использовать, и рендерит из него каждую карту.
- Карта (цикл) — итеративно проходит по каждому спрайту в списке спрайтов
- Спрайт — копирует атрибуты спрайта в буфер OAM
Для каждого из этапов я перечислила необходимые им переменные, играемые ими роли и места для их хранения. Эта таблица выглядит примерно так.
Описание | Размер | Этап | Использование | Откуда | Место | Куда |
---|---|---|---|---|---|---|
Буфер OAM | 2 | Спрайт | Указатель | HL | HL | |
Источник карт | 2 | Спрайт | Указатель | BC | BC | |
Текущий байт | 1 | Спрайт | Рабочее пространство | Источник карт | E | |
X | 1 | Спрайт | Переменная | HiRAM | A | |
Y | 1 | Спрайт | Переменная | HiRAM | A | |
Начало карты анимаций | 2 | Карта спрайтов | Указатель | Stack3 | DE | |
Источник карт | 2 | Карта спрайтов | Указатель | [DE] | BC | |
Оставшиеся спрайты | 1 | Карта спрайтов | Scratch | Источник карт | D | |
Буфер OAM | 1 | Карта спрайтов | Указатель | HL | HL | Stack1 |
Начало карты анимаций | 2 | Анимация | Рабочее пространство | BC/Stack3 | BC | Stack3 |
Оставшиеся карты | 1 | Анимация | Рабочее пространство | Начало анимации | HiRAM | |
Общее количество карт | 1 | Анимания | Переменная | Начало анимации | HiRAM | |
Направление объекта | 1 | Анимация | Переменная | HiRAM | HiRAM | |
Карт на кадр | 1 | Анимация | Переменная | Начало анимации | НЕ ИСПОЛЬЗУЕТСЯ!!! | |
Номер кадра | 1 | Анимация | Переменная | HiRAM | A | |
Указатель на карту | 2 | Анимация | Указатель | AnimStart + Dir*TMC + MpF*F# | BC | DE |
Буфер OAM | 2 | Анимация | Указатель | Stack1 | HL | |
Начало таблицы анимаций | 2 | Список анимаций | Рабочее пространство | Задаётся жёстко | DE | |
Источник объекта | 2 | Список анимаций | Указатель | HL | HL | Stack2 |
Номер кадра | 1 | Список анимаций | Переменная | Источник объекта | HiRAM | |
Номер анимации | 1 | Список анимаций | Рабочее пространство | Источник объекта | A | |
X объекта | 1 | Список объектов | Переменная | Источник объекта | HiRAM | |
Y объекта | 1 | Список анимаций | Переменная | Источник объекта | HiRAM | |
Направление объекта | 1 | Список анимаций | Переменная | Obj Src | HiRAM | |
Начало карты анимаций | 2 | Список анимаций | Указатель | [Anim Table + Anim #] | BC | |
Буфер OAM | 2 | Список анимаций | Указатель | DE | Stack1 | |
Источник объекта | 2 | Цикл объекта | Указтель | Задаётся жёстко/Stack2 | HL | |
Оставшиеся объекты | 1 | Цикл объекта | Переменная | Вычисляется | B | |
Активное битовое поле объекта | 1 | Цикл объекта | Переменная | Вычисляется | C | |
Буфер OAM | 2 | Цикл объекта | Указатель | Задаётся жёстко | DE |
Да, очень запутанно. Если быть совершенно честной, то я сделала эту таблицу только для поста, чтобы понятнее объяснить, но она уже начала приносить пользу. Попробую её объяснить Начнём с конца и дойдём до самого начала. Вы увидите каждый фрагмент данных, с которого я начинаю: источник объекта, буфер OAM и предварительно вычисленные переменные цикла. В каждом цикле мы начинаем с этого и только этого, за исключением того, что источник объекта обновляется в каждом цикле.
Для каждого объекта, который мы рендерим, необходимо определить отображаемую анимацию. Пока мы этим занимаемся, можно также сохранить атрибуты X, Y, Frame # и Direction, прежде чем выполнять инкремент указателя объекта на следующий объект и сохранение их в стек, чтобы взять обратно при выходе. Мы используем номер анимации в сочетании с жёстко заданной в коде таблицей анимаций, чтобы определить, где начинается карта анимаций. (Здесь я упрощаю, подразумевая, что каждый объект имеет одну и ту же таблицу анимаций. Это ограничивает меня 256 анимациями на игру, но я вряд ли превзойду это значение.) Также мы можем записать буфер OAM, чтобы сэкономить несколько регистров.
После извлечения карты анимаций нам нужно найти, где находится список карт спрайтов для заданного кадра и направления, а также сколько карт нужно рендерить. Можно заметить, что переменная карт на кадр не используется. Так получилось, потому что я не подумала и задала неизменное значение 2. Мне нужно это исправить. Ещё нам нужно извлечь из стека буфер OAM. Также можно заметить полное отсутствие управления циклом. Он выполняется в отдельной, гораздо более простой подпроцедуре, которая позволяет избавиться от жонглирования регистрами.
После этого всё становится довольно просто. Карта — это куча спрайтов, поэтому мы обходим их в цикле и отрисовываем с учётом сохранённых координат X и Y. Однако мы снова сохраняем указатель OAM в конец списка спрайтов, чтобы следующая карта начиналась там, где мы закончили.
Каким же стал итоговый результат всего этого? Точно таким же, как и раньше: размахивающей в темноте хвостом полярной лисой. Но добавлять новые анимации или спрайты теперь намного проще. В следующей части я расскажу о сложных фонах и параллаксном скроллинге.
Часть 4. Параллаксный фон
Напомню, на текущем этапе у нас есть анимированные спрайты на сплошном чёрном фоне. Если я не планирую делать аркадную игру 70-х, то этого явно будет недостаточно. Мне нужно какое-то фоновое изображение.
В первой части, когда я рисовала графику, я создала ещё и несколько тайлов фона. Настало время их использовать. У нас будет три «основных» типа тайлов (небо, трава и земля) и два переходных тайла. Все они загружены во VRAM и готовы к использованию. Теперь нам остаётся только записать их в фон.
Фон
Фоны на Game Boy хранятся в памяти в массиве 32×32 из тайлов размером 8×8. Каждые 32 байта соответствуют одной строке тайлов.
Пока я планирую повторять одинаковый столбец тайлов во всём пространстве 32×32. Это здорово, но создаёт небольшую проблему: мне нужно будет задать каждый тайл 32 раза подряд. Писать это будет долго.
Инстинктивно я решила использовать команду REPT, чтобы прибавлять 32 байт/строку, а затем применять memcpy для копирования фона во VRAM.
REPT 32 db BG_SKY ENDR REPT 32 db BG_GRASS ENDR ...
Однако это будет означать, что придётся выделить 256 байт только под один фон, что довольно много. Эта проблема усугубляется, если вспомнить, что копирование с помощью memcpy предварительно созданной карты фона не позволит добавлять другие типы столбцов (например ворот, препятствий) без значительной сложности и кучи потраченного впустую ПЗУ картриджа.
Поэтому вместо этого я решила задать один столбец целиком следующим образом:
db BG_SKY, BG_SKY, BG_SKY, ..., BG_GRASS
а затем использовать простой цикл, чтобы скопировать каждый элемент этого списка 32 раза. (см. функцию LoadGFX
в файле main.z80
коммита 739986a.)
Удобство этого подхода в том, что позже я смогу добавить очередь, чтобы написать нечто подобное:
BGCOL_Field: db BG_SKY, ... BGCOL_LeftGoal: db BG_SKY, ... BGCOL_RightGoal: db BG_SKY, ... ... BGMAP_overview: db 1 dw BGCOL_LeftGoal db 30 dw BGCOL_Field db 1 dw BGCOL_RightGoal db $FF
Если я решу отрисовывать BGMAP_overview, то он нарисует 1 столбец LeftGoal, после чего будет 30 столбцов Field и 1 столбец RightGoal. Если BGMAP_overview
находится в ОЗУ, то я смогу изменять его на лету в зависимости позиции камеры по X.
Камера и позиция
Ах да, камера. Это важная концепция, о которой я пока не говорила. Здесь мы имеем дело со множеством координат, поэтому прежде чем говорить о камере, сначала всё это разберём.
Нам нужно работать с двумя системами координат. Первая — это экранные координаты. Это область размером 256×256, которая может содержаться во VRAM консоли Game Boy. Мы можем скроллить видимую часть экрана в пределах этих 256×256, но при выходе за границы происходит сворачивание.
В ширину мне требуется больше, чем 256 пикселей, поэтому я добавляю мировые координаты, которые в этой игре будут иметь размеры 65536×256. (Мне не нужна дополнительная высота по Y, потому что игра происходит на плоском поле.) Эта система полностью отдельна от системы экранных координат. Вся физика и коллизии должны выполняться в мировых координатах, ведь в противном случае объекты будут сталкиваться с объектами на других экранах.
Сравнение экранных и мировых координат
Так как позиции всех объектов представлены в мировых координатах, перед рендерингом их необходимо преобразовывать в экранные координаты. На самом левом крае мира мировые координаты совпадают с экранными. Если нам нужно отображать на экране вещи правее, то нам необходимо взять всё в мировых координатах и сдвинуть влево, чтобы они находились в экранных координатах.
Для этого мы зададим переменную «camera X», которая определяется как левая граница экрана в мире. Например, если camera X
равна 1000, то мы можем видеть мировые координаты 1000-1192, потому что видимый экран имеет ширину 192 пикселя.
Для обработки объектов мы просто берём их позицию по X (например 1002), вычитаем позицию камеры, равную 1000, и отрисовываем объект в позиции, заданной разностью (в нашем случае — 2). Для фона, который не находится в мировых координатах, а уже описывается в экранных, мы задаём позицию равной нижнему байту переменной camera X
. Благодаря этому фон будет скроллиться влево и вправо вместе с камерой.
Параллакс
Созданная нами система выглядит довольно плоской. Каждый слой фона перемещается с одинаковой скоростью. Он не ощущается трёхмерным, и нам нужно это исправить.
Простой способ добавления имитации 3D называется «параллаксным скроллингом» (parallax scrolling). Представьте, что вы едете по дороге и очень устали. В Game Boy сели батарейки, и вам приходится смотреть из окна машины. Если посмотреть на землю рядом с собой, то вы увидите. что она движется со скоростью 70 миль в час. Однако если посмотреть на поля в отдалении, то будет казаться, что они движутся намного медленнее. А если посмотреть на очень далёкие горы, то они как будто едва перемещаются.
Мы можем симулировать этот эффект с помощью трёх листов бумаги. Если на одном листе нарисовать горную гряду, на втором — поле, а на третьем — дорогу, и наложить их друг на друга так. чтобы виден был каждый слой, то это будет имитацией того, что мы видим из окна машины. Если мы хотим переместить «машину» влево, то мы сдвигаем самый верхний лист (с дорогой) далеко вправо, следующий — немного вправо, а последний — чуть-чуть вправо.
Однако при реализации такой системы на Game Boy возникает небольшая проблема. У консоли есть только один слой фона. Это аналогично тому, что у нас есть только один лист бумаги. Нельзя создать эффект параллакса с помощью только одного листа бумаги. Или можно?
H-Blank
Экран Game Boy отрисовывается построчно. В результате эмуляции поведения старых ЭЛТ-телевизоров между каждой строкой есть небольшая задержка. Что если мы сможем ею как-то воспользоваться? Оказывается, у Game Boy есть особое аппаратное прерывание специально для этой цели.
Аналогично прерыванию VBlank, которое мы постоянно использовали, чтобы дождаться конца кадра для записи во VRAM, существует и прерывание HBlank. Задав бит 6 регистра по адресу $FF41
, включив прерывание LCD STAT
и записав номер строки по адресу $FF45
, мы можем приказать Game Boy запустить прерывание LCD STAT
, когда он соберётся отрисовывать указанную строку (и когда находится в её HBlank).
В течение этого времени мы можем изменять любые переменные VRAM. Это не куча времени, поэтому мы не можем изменить больше, чем пару регистров, но у нас всё-таки есть кое-какие возможности. Мы хотим изменить регистр горизонтального скроллинга по адресу $FF43
. При этом всё на экране ниже указанной строки переместится на некую величину сдвига, создавая эффект параллакса.
Если вернуться к примеру с горой, то можно заметить потенциальную проблему. Горы, облака и цветы — это не плоские линии! Мы не можем сдвигать выбранную строку вверх и вниз в процессе отрисовки; если мы её выбрали, то она остаётся такой же по крайней мере до следующего HBlank. То есть мы можем делать срезы только по прямым линиям.
Чтобы решить эту проблему, нам придётся поступить немного умнее. Мы можем объявить какую-нибудь линию в фоне линией, которую ничто не может пересекать, а значит менять режимы объектов над и под ней, и игрок ничего не сможет заметить. Например, вот где эти линии находятся в сцене с горой.
Здесь я сделала срезы прямо над и под горой. Всё от верха до первой линии движется медленно, всё до второй линии движется со средней скоростью, а всё ниже этой линии движется быстро. Это простой, но умный трюк. И узнав о нём, вы сможете заметить его во многих ретро-играх, в основном для Genesis/Mega Drive, но и на других консолях тоже. Один из самых очевидных примеров — это часть пещеры из Mickey Mania. Можно заметить, что сталагмиты и сталактиты на фоне разделены ровно по горизонтальной линии с очевидной чёрной границей между слоями.
То же самое я реализовала и в своём фоне. Однако тут есть одна хитрость. Предположим, что передний план движется со скоростью, один в один совпадающей с движением камеры, а скорость фона составляет одну треть попиксельного движения камеры, то есть фон движется как одна треть переднего плана. Но, разумеется, трети пикселя не существует. Поэтому мне нужно двигать фон на один пиксель за каждые три пикселя движения.
Если бы работали с компьютерами, способными на математические вычисления, то брали бы позицию камеры, делили её на 3 и делали бы это значение смещением фона. К сожалению, Game Boy не способен выполнять деление, не говоря уже о том, что программное деление — это очень медленный и мучительный процесс. Добавление устройства для деления (или умножения) в слабенький ЦП для портативной развлекательной консоли в 80-х не казалось экономически эффективным шагом, поэтому нам придётся выдумывать другой способ.
В коде я сделала следующее: вместо считывания позиции камеры из переменной, я потребовала, чтобы она увеличивалась или уменьшалась. Благодаря этому при каждом третьем инкременте я смогу выполнять инкремент позиции фона, а при каждом первом инкременте — инкремент позиции переднего плана. Это немного усложняет скроллинг к позиции с другого края поля (проще всего просто выполнять сброс позиций слоёв после определённого перехода), но избавляет нас от необходимости деления.
Результат
После всего этого я получила следующее:
Для игры на Game Boy это на самом деле довольно круто. Насколько я знаю, не во всех них параллаксный скроллинг реализован так.
Источник