Реактивное введение в программирование Game Boy Advance (часть 3 из 8)
Тайловые видеорежимы…
Часть 1: Инструментарий, основы, кнопки, таймеры
Часть 2: Пиксельные видеорежимы
Часть 3: Тайловые видеорежимы
Часть 4: Спрайты
Часть 5: Вращающиеся фоны и прозрачность
Часть 6: Прерывания, DMA
Часть 7: Звук (Direct Sound)
Часть 8: Сохранения
Итак, по настоящему продуктивным в 2D-графике Game Boy Advance может быть в тайловых видеорежимах. Тайловые режимы базированы, как следует из названия, на тайлах — минимальных порциях изображения размером (в GBA и многих других консолях) 8x8 пикселей. Любые виды тайлов в GBA используют палитру (одну из двух — палитру фонов или палитру спрайтов). Тайлы могут быть либо 256-цветными: тогда каждый пиксель является 1 байтом индекса (от 0 до 255) цвета из соответствующей палитры, а сам тайл занимает в памяти 64 байта двумерного построчного массива 8x8. Либо 16-цветными, тогда каждый байт в тайле хранит 4-битные (от 0 до 15) значения для каждых двух смежных пикселей по горизонтали (при этом в нижних 4 битах хранится информация о том, который на экране отобразится левее), а сам тайл занимает в памяти 32 байта массива 4x8. Причём эти 4-битные индексы будут использованы как нижние 4 бита всё-таки полноценного 8-битного индекса в палитре — а откуда будут взяты верхние 4 бита будет вскользь рассказано позже. 16-цветные тайлы могут быть интересны для экономии памяти — они и сами занимают меньше места, чем 256-цветные, но кроме того позволяют раскрашивать одинаковые тайлы разными цветами предоставлением разных баз в палитре. Однако нас здесь не должны сильно заботить вопросы экономии памяти, поэтому я не буду сосредотачивать много внимания на 16-цветных тайлах и отдам всё предпочтение в примерах 256-цветным. Важно заметить, что в любом случае нулевой индекс цвета в GBA всегда означает полную прозрачность — пиксели такого цвета просто не рисуются, оставляя изображение под ними нетронутым.
Эти пиксельные изображения тайлов формируют тайловые данные (tile data) в виде массивов u8 data[][8][8] (или u8 data[][8][4]) в видеопамяти, причём в GBA они могут начинаться только в четырёх местах — в самом начале видеопамяти (VID_RAM_START) или еще 3 раза через каждые 0x4000 байт (16Кб) от него. Можно сказать, что нам доступно 4 страницы для тайловых массивов, вмещающих по 256 256-цветных тайлов (или 512 16-цветных) каждая. Здесь напрашивается 1-байтный индекс тайла в рамках одной страницы — и это справедливо для некоторых случаев — однако, в других случаях под индексацию тайла выделяется целых 10 бит, так что ссылаться можно на 1024 тайла (от 0 до 1023), что теоретически позволяет нам адресовать 1024 256-цветных тайла в тайловом массиве занимающем все эти первые 64Кб видеопамяти — границы страниц без проблем можно пересекать. Но лишь теоретически, т.к. кроме тайловых данных в этих 64Кб нам надо разместить еще как минимум одну тайловую карту (фон).
Основная идея тайловых видеорежимов состоит в том, что изображение формируется из двух компонент — тайловых карт (фонов) и тайловых спрайтов.
Тайловые карты (tile map) это прямоугольные массивы из тайлов (вернее их индексов), выступающие как правило в роли клеток лабиринта, заднего фона неба и т.п. вещей. Одновременно на экране может отображаться сразу несколько тайловых карт (в GBA — до четырёх), при этом порядок их отрисовки управляется соответствующими регистрами видеоадаптера (через прозрачные пиксели одного фона мы видим «более задние» фоны) и каждая из карт может быть прокручена как по горизонтали так и по вертикали на произвольное количество пикселей посредством других регистров. Комбинируя прокрутку разных тайловых карт с разной скоростью мы можем создавать эффект параллакса (удалённого неба и задних планов) а правильным образом комбинируя обновление и прокрутку тайловой карты создавать иллюзию её неограниченного размера.
Одной из прогрессивных на свой момент особенностью такой консоли от Nintendo как SNES был видеорежим MODE 7 в котором был хотя и единственный тайловый фон, но он мог произвольно вращаться и масштабироваться видеоадаптером. GBA уверенно обгоняет в этом SNES — в нём таких фонов может быть одновременно целых два.
Таким образом в GBA тайловые карты бывают двух типов — те что попроще поддерживают только прокрутку и называются «текстовыми» (видимо из-за прямой аналогии с компьютерными текстовыми видеорежимами, где символы текста были теми же самыми тайлами), те же, что могут быть произвольно отмасштабированы и повёрнуты мы будем называть далее «вращающимися».
Все тайловые видеорежимы имеют разрешение 240x160 пикселей. Опишем их теперь подробнее:
Элементами вращающихся тайловых карт выступают байты с индексами тайлов с прямолинейной адресацией как массива tiles[ x + width * y ]. Однако всегда помните, что запись в видеопамять ведется минимально 2-байтными словами, поэтому учитывайте это при работе с такими картами. Таким образом именно этот вид тайловых карт может отображать и индексировать как максимум 256 разных тайла.
Элементами же текстовых тайловых карт выступают уже двухбайтные значения. Нижние 10 бит отведены под номер тайла, следующие 2 бита включают переворот изображения тайла в ячейке по горизонтали и вертикали (в таком порядке) и вот оставшиеся верхние 4 бита и используются как верхние 4 бита индекса палитры в случае, если у тайловой карты включен 16-цветный режим. Если размеры текстовой карты составляет 32x32 или 32x64 тайлов, то общаться к конкретному тайлу через указатель u16* tiles можно как tiles[ x + 32 * y ], где x это координата по горизонтали (от 0 до 31), а y — координата по вертикали. Хотелось бы ожидать подобной формулы и от режимов 64x32 или 64x64, но на самом деле такие карты образованы пристыкованными вплотную областями 32x32 тайла, таким образом, что раскладка в памяти карты 64x64 представлена как u16 tiles[4][32][32].
Визуально же сами эти блоки на экране консоли отображаются в 2 строки и 2 столбца в следующем порядке:
На самом деле то же самое происходит и в блоке 32x64, но в нём раскладка в памяти просто совпадает с «естественной». Таким образом в текстовых тайловых картах размерностью 64x32 или 64x64 тайла требуется немного модифицировать адресацию ячейки (x, y), что будет показано ниже.
Скомпонуем теперь все накопившиеся знания о работе с видеоадаптером в ряд макросов с комментариями:
(добавим эти строки в наш gba_defs.h или воспользуемся уже готовым из исходников)
Обратите еще внимание на регистры смещения/прокрутки текстовых фонов — их 4 пары для каждого. Регистры с буквой H — это горизонтальные координаты пикселя фона, который будет отображаться в левом-верхнем углу экрана, а с буквой V — соответственно вертикальные. Важно отметить, что число может принимать отрицательные значения (фон при этом на экране будет уезжать вниз-вправо). При этом регистры имеют только 10 значащих (нижних) бит, но это не вызывает никаких проблем в силу природы целых отрицательных чисел на основной массе компьютеров, включая GBA — спокойно присваивайте этим регистрам любые и положительные и отрицательные значения — результат будет логичным.
В этом уроке я еще пока не описываю подробно спрайты и вращающиеся фоны, хотя и некоторые макросы ответственные за них в списке появились.
Не буду я так же в нём реализовывать зеркалирование тайлов или 16-цветные режимы — но в описании есть всё необходимое, чтобы вы могли сделать это самостоятельно.
Итак, создадим программу, выводящую на экран 2 текстовых фона — на одном будут по порядку 256 тайлов залитых однотонными цветами, а на другом, сверху, имитация падающего снега. DPad-ом можно бесконечно скроллить и задний и передний фоны с лёгким эффектом парралакса, а кнопка A возвращает координаты прокрутки в начальное значение.
05_simple_bg.cpp
чтобы скомпилировать этот код делаем привычный батник build_05_simple.bat:
Итак, здесь мы схватили тайлы за хвост в самых простых — текстовых фонах.
Здесь же видно, что благодаря зацикливанию при прокрутке, немного помозговав, можно реализовать иллюзию скроллинга по сколь угодно большому полю. Причём с помощью арифметики с обрезкой по битам (чему способствует то, что размеры тайловых карт в пикселях всегда степени двойки) это можно реализовать очень экономно — дорисовыванием только одной полоски краевых тайлов один раз на каждые 8 продвинутых пикселей. Это очень небольшой объём работы для процессора и падения FPS из-за возни с графикой тут не предвидятся. Реализуем такой принцип «бесконечного» скроллинга в следующей программе. В ней мы создадим большую карту размером 256x256 клеток в основной памяти GBA, а в тайловой карте видеопамяти будем поддерживать только небольшой кусок её отображения 31x21 тайла (ровно на 1 тайл больше по каждой из осей, чем влезает на экран — именно столько может быть видно одновременно тайлов если есть небольшая прокрутка). При помощи регистров прокрутки и обрезки координат в тайловой карте по «модулю 32» мы сведем к минимуму число операций, которые нужно выполнять для плавного скроллинга.
По большому игровому полю можно двигаться D-pad-ом, кнопки A и B включают малое и большое ускорение прокрутки соответственно. А кнопками L и R можно закрашивать клетки поля в центре экрана черными и белыми тайлами. Зажав SELECT можно увидеть тайловую карту без прокруток, чтобы увидеть её наполнение «как оно есть». Если при этом двигаться, то легко заметить полоски обновления новых областей и оценить изящность трюка с другой точки зрения. Тайловая карта, являясь «окном» в огромную карту поля, «прорублена» тем не менее хитрым образом — координаты конкретной клетки поля в тайловой карте никогда не плавают, как можно было бы ожидать, а являются фиксированными через отбрасывание битов выше пятого, т.е. операциями x & 31 и y & 31. Плавают в тайловой карте границы отображения, а не клетки поля. Таким образом мы и добиваемся минимальных затрат на перерисовку тайловой карты в движении.
05_scroll_bg.cpp
и батник build_05_scroll.bat:
О вращающихся тайловых картах я расскажу в последних уроках. Но даже текстовых уже достаточно, чтобы на их основе сделать неплохой скроллер.
Следующий же урок будет посвящён второму краеугольному столпу тайловой графики — спрайтам.
Продолжение...
Оглавление
Часть 1: Инструментарий, основы, кнопки, таймеры
Часть 2: Пиксельные видеорежимы
Часть 3: Тайловые видеорежимы
Часть 4: Спрайты
Часть 5: Вращающиеся фоны и прозрачность
Часть 6: Прерывания, DMA
Часть 7: Звук (Direct Sound)
Часть 8: Сохранения
5. Тайловые видеорежимы
Итак, по настоящему продуктивным в 2D-графике Game Boy Advance может быть в тайловых видеорежимах. Тайловые режимы базированы, как следует из названия, на тайлах — минимальных порциях изображения размером (в GBA и многих других консолях) 8x8 пикселей. Любые виды тайлов в GBA используют палитру (одну из двух — палитру фонов или палитру спрайтов). Тайлы могут быть либо 256-цветными: тогда каждый пиксель является 1 байтом индекса (от 0 до 255) цвета из соответствующей палитры, а сам тайл занимает в памяти 64 байта двумерного построчного массива 8x8. Либо 16-цветными, тогда каждый байт в тайле хранит 4-битные (от 0 до 15) значения для каждых двух смежных пикселей по горизонтали (при этом в нижних 4 битах хранится информация о том, который на экране отобразится левее), а сам тайл занимает в памяти 32 байта массива 4x8. Причём эти 4-битные индексы будут использованы как нижние 4 бита всё-таки полноценного 8-битного индекса в палитре — а откуда будут взяты верхние 4 бита будет вскользь рассказано позже. 16-цветные тайлы могут быть интересны для экономии памяти — они и сами занимают меньше места, чем 256-цветные, но кроме того позволяют раскрашивать одинаковые тайлы разными цветами предоставлением разных баз в палитре. Однако нас здесь не должны сильно заботить вопросы экономии памяти, поэтому я не буду сосредотачивать много внимания на 16-цветных тайлах и отдам всё предпочтение в примерах 256-цветным. Важно заметить, что в любом случае нулевой индекс цвета в GBA всегда означает полную прозрачность — пиксели такого цвета просто не рисуются, оставляя изображение под ними нетронутым.
Эти пиксельные изображения тайлов формируют тайловые данные (tile data) в виде массивов u8 data[][8][8] (или u8 data[][8][4]) в видеопамяти, причём в GBA они могут начинаться только в четырёх местах — в самом начале видеопамяти (VID_RAM_START) или еще 3 раза через каждые 0x4000 байт (16Кб) от него. Можно сказать, что нам доступно 4 страницы для тайловых массивов, вмещающих по 256 256-цветных тайлов (или 512 16-цветных) каждая. Здесь напрашивается 1-байтный индекс тайла в рамках одной страницы — и это справедливо для некоторых случаев — однако, в других случаях под индексацию тайла выделяется целых 10 бит, так что ссылаться можно на 1024 тайла (от 0 до 1023), что теоретически позволяет нам адресовать 1024 256-цветных тайла в тайловом массиве занимающем все эти первые 64Кб видеопамяти — границы страниц без проблем можно пересекать. Но лишь теоретически, т.к. кроме тайловых данных в этих 64Кб нам надо разместить еще как минимум одну тайловую карту (фон).
Основная идея тайловых видеорежимов состоит в том, что изображение формируется из двух компонент — тайловых карт (фонов) и тайловых спрайтов.
Тайловые карты (tile map) это прямоугольные массивы из тайлов (вернее их индексов), выступающие как правило в роли клеток лабиринта, заднего фона неба и т.п. вещей. Одновременно на экране может отображаться сразу несколько тайловых карт (в GBA — до четырёх), при этом порядок их отрисовки управляется соответствующими регистрами видеоадаптера (через прозрачные пиксели одного фона мы видим «более задние» фоны) и каждая из карт может быть прокручена как по горизонтали так и по вертикали на произвольное количество пикселей посредством других регистров. Комбинируя прокрутку разных тайловых карт с разной скоростью мы можем создавать эффект параллакса (удалённого неба и задних планов) а правильным образом комбинируя обновление и прокрутку тайловой карты создавать иллюзию её неограниченного размера.
Одной из прогрессивных на свой момент особенностью такой консоли от Nintendo как SNES был видеорежим MODE 7 в котором был хотя и единственный тайловый фон, но он мог произвольно вращаться и масштабироваться видеоадаптером. GBA уверенно обгоняет в этом SNES — в нём таких фонов может быть одновременно целых два.
Таким образом в GBA тайловые карты бывают двух типов — те что попроще поддерживают только прокрутку и называются «текстовыми» (видимо из-за прямой аналогии с компьютерными текстовыми видеорежимами, где символы текста были теми же самыми тайлами), те же, что могут быть произвольно отмасштабированы и повёрнуты мы будем называть далее «вращающимися».
Все тайловые видеорежимы имеют разрешение 240x160 пикселей. Опишем их теперь подробнее:
- Режим 0: поддерживает до 4-х текстовых фонов (BG0-BG3).
- Режим 1: до 2-х текстовых фонов (BG0-BG1) и 1 вращающийся (BG2)
- Режим 2: до 2-х вращающихся фонов (BG2-BG3)
- Приоритет отрисовки — число от 0 до 3, фоны с меньшим численно приоритетом рисуются поверх фонов с более высоким. Если два фона имеют одинаковый приоритет, фон с более низким номером по порядку отрисуется поверх фона с более высоким.
- Цветность тайловых данных — 256 или 16 цветов на пиксель.
- Адрес начала тайловых данных (tile data — массив с данными пикселей тайлов) — доступно только 4 варианта: 0x0000, 0x4000, 0x8000 или 0xC000 от начала видеопамяти (VID_RAM_START).
- Адрес начала тайловой карты (tile map) — как и адрес начала тайловых данных это значение ограничено диапазоном значений, находящихся в первых 64Кб видеопамяти, но оно шире, потому что гранулярность составляет 2Кб, таким образом давая 32 различных возможных адреса. Формула для вычисления: VID_RAM_START + x * 0x800, где x меняется от 0 до 31.
- Размер тайловой карты. Для текстовых возможны варианты: 256x256, 256x512, 512x256 или 512x512 пикселей (т.е. 32x32, 32x64, 64x32 или 64x64 тайла). Для вращающихся варинты отличаются: 128x128, 256x256, 512x512 или 1024x1024 пикселя (т.е. квадраты со сторонами из 16, 32, 64 или 128 тайлов).
- Координаты пикселя в карте, который будет выводится в левом-верхнем углу экрана (регистры прокрутки). Важно отметить, что текстовые карты всегда зациклены — при попытке при отображении выйти за любой из краёв они просто показывают себя же с другого края, таким образом всегда выполняя режим бесконечного замощения во все стороны. Вращающиеся же тайловые карты могут быть настроены на режим обрезки — когда вне их плоскости изображение не затрагивается.
Элементами вращающихся тайловых карт выступают байты с индексами тайлов с прямолинейной адресацией как массива tiles[ x + width * y ]. Однако всегда помните, что запись в видеопамять ведется минимально 2-байтными словами, поэтому учитывайте это при работе с такими картами. Таким образом именно этот вид тайловых карт может отображать и индексировать как максимум 256 разных тайла.
Элементами же текстовых тайловых карт выступают уже двухбайтные значения. Нижние 10 бит отведены под номер тайла, следующие 2 бита включают переворот изображения тайла в ячейке по горизонтали и вертикали (в таком порядке) и вот оставшиеся верхние 4 бита и используются как верхние 4 бита индекса палитры в случае, если у тайловой карты включен 16-цветный режим. Если размеры текстовой карты составляет 32x32 или 32x64 тайлов, то общаться к конкретному тайлу через указатель u16* tiles можно как tiles[ x + 32 * y ], где x это координата по горизонтали (от 0 до 31), а y — координата по вертикали. Хотелось бы ожидать подобной формулы и от режимов 64x32 или 64x64, но на самом деле такие карты образованы пристыкованными вплотную областями 32x32 тайла, таким образом, что раскладка в памяти карты 64x64 представлена как u16 tiles[4][32][32].
Визуально же сами эти блоки на экране консоли отображаются в 2 строки и 2 столбца в следующем порядке:
+-+-+
|0|1|
+-+-+
|2|3|
+-+-+
На самом деле то же самое происходит и в блоке 32x64, но в нём раскладка в памяти просто совпадает с «естественной». Таким образом в текстовых тайловых картах размерностью 64x32 или 64x64 тайла требуется немного модифицировать адресацию ячейки (x, y), что будет показано ниже.
Скомпонуем теперь все накопившиеся знания о работе с видеоадаптером в ряд макросов с комментариями:
// *** РЕГИСТР КОНТРОЛЯ ВИДЕОРЕЖИМА
#define REG_DISPCNT (*((volatile u32 *) 0x4000000))
// Флаги регистра:
// Биты 0-2: номер видеорежима
// Режим 0: 240x160 - тайловый видеорежим. Доступны 4 скроллируемых задних плана (BG0-BG3).
#define MODE_0 0x0
// Режим 1: 240x160 - тайловый видеорежим. Доступны 2 скроллируемых задних плана (BG0-BG1)
// и один с афинными трансформациями (BG2).
#define MODE_1 0x1
// Режим 2: 240x160 - тайловый видеорежим. Доступны 2 задних плана с афинными
// трансформациями (BG2-BG3).
#define MODE_2 0x2
// Режим 3: 240x160x15. Начинается с адреса 0x06000000, каждый пиксель занимает 2 байта
// 15-битного RGB-цвета. Из-за большой величины видеобуфера не имеет второй страницы.
#define MODE_3 0x3
// Режим 4: 240x160x8. Начинается с адреса 0x06000000, имеет вторую страницу по адресу
// 0x0600A000. Переключение между видеостраницами контролируется битом MODE_PAGE_2, что
// позволяет реализовать буферизацию. Однобайтовые пиксели хранят индекс в 16-битной
// палитре из 256 ячеек по адресу BGR_PALETTE.
#define MODE_4 0x4
// Режим 5: 160x128x16. Начинается с адреса 0x06000000, из-за пониженного разрешения
// тоже имеет вторую страницу по адресу 0x0600A000.
#define MODE_5 0x5
// Бит 4: для пиксельных режимов переключает отображение на вторую страницу.
#define MODE_PAGE_2 0x0010
// Бит 5: усиливает ресурсы, затрачиваемые на отрисовку спрайтов, что позволяет
// иногда бороться с мерцаниями спрайтов, судя по всему ценой уменьшения промежутков HBLANK.
#define MODE_FORCE_SPRITES 0x0020
// Бит 6: Режим компоновки многотайловых спрайтов (больше чем 8x8 пикселей).
// В таких спрайтах опорный тайл (индекс) выступает в роли левого-верхнего, остальные
// компонуются к нему следующим образом:
// 1 - линейный режим, тайлы выбираются подряд для формирования строк и колонок
// многотайлового спрайта
// 0 - массив тайлов образуют двумерный массив с шириной строки в 32 тайла и
// спрайты компонуются по соседним ячейкам в этом массиве графически-естественно.
#define MODE_LINEAR_SPRITES 0x0040
// Бит 7: Выключить дисплей
#define MODE_DISPLAY_OFF 0x0080
// Биты 8-11: включить задник BG0-BG3.
// В пиксельных видеорежимах экран считается задником BG2.
#define BG0_ENABLE 0x0100
#define BG1_ENABLE 0x0200
#define BG2_ENABLE 0x0400
#define BG3_ENABLE 0x0800
// Бит 12: включить отображение спрайтов.
#define SPR_ENABLE 0x1000
// Биты 13-15: включить окна.
#define WIN0_ENABLE 0x2000
#define WIN1_ENABLE 0x4000
#define SPRWIN_ENABLE 0x8000
// *** РЕГИСТР ТЕКУЩЕЙ ВЫВОДИМОЙ НА ЭКРАН СТРОКИ ПИКСЕЛЕЙ
// Меняется от 0 до 159 во время отображения строк дисплея и после еще увеличивается 68 раз
// до повторения цикла обновления экрана.
#define REG_VCOUNT (*((volatile u16 *) 0x4000006))
// *** РЕГИСТРЫ УПРАВЛЕНИЯ ТАЙЛОВЫМИ ФОНАМИ
#define REG_BG0CNT (*((volatile u16 *) 0x4000008))
#define REG_BG1CNT (*((volatile u16 *) 0x400000A))
#define REG_BG2CNT (*((volatile u16 *) 0x400000C))
#define REG_BG3CNT (*((volatile u16 *) 0x400000E))
// Битовые флаги:
// Приоритет (от 0 до 3)
#define BG_PRIORITY(x) (x)
// Адрес начала данных тайлов (массивы пикселей TILE DATA)
#define BG_TILE_DATA_AT_0000 0x00
#define BG_TILE_DATA_AT_4000 0x04
#define BG_TILE_DATA_AT_8000 0x08
#define BG_TILE_DATA_AT_C000 0x0C
// Режим мозаики
#define BG_MOSAIC 0x40
// Пиксели в тайлах 8-битные (байт), иначе селектор тайла
// хранит индекс одной из 16 палитр, а пиксели в тайлах 4-битные.
#define BG_COLOR_256 0x80
// Логический адрес начала карты тайлов (от 0 до 31) - он записывается в регистр.
#define BG_TILE_MAP_AT(x) ((x)<<8)
// Получить адрес начала карты тайлов, увеличение логического номера
// увеличивает его на 2048 байт, начиная с начала VID_RAM.
#define BG_TILE_MAP_ADDR(x) (0x6000000 + (x) * 0x800)
// Режим повторения для вращающегося фона
#define BG_ROT_OVERLAP 0x2000
// Размеры текстовых фонов (TILE MAP)
#define BG_SIZE_256x256 0x0000
#define BG_SIZE_512x256 0x4000
#define BG_SIZE_256x512 0x8000
#define BG_SIZE_512x512 0xC000
// Размеры вращающихся фонов (TILE MAP)
#define BG_ROT_SIZE_128x128 0x0000
#define BG_ROT_SIZE_256x256 0x4000
#define BG_ROT_SIZE_512x512 0x8000
#define BG_ROT_SIZE_1024x1024 0xC000
// *** Регистры горизонтального и вертикального смещений текстовых фонов
// Они не работают для вращающихся фонов - для последних существуют свои регистры.
#define REG_BG0HOFS (*((volatile u16 *) 0x4000010))
#define REG_BG0VOFS (*((volatile u16 *) 0x4000012))
#define REG_BG1HOFS (*((volatile u16 *) 0x4000014))
#define REG_BG1VOFS (*((volatile u16 *) 0x4000016))
#define REG_BG2HOFS (*((volatile u16 *) 0x4000018))
#define REG_BG2VOFS (*((volatile u16 *) 0x400001A))
#define REG_BG3HOFS (*((volatile u16 *) 0x400001C))
#define REG_BG3VOFS (*((volatile u16 *) 0x400001E))
(добавим эти строки в наш gba_defs.h или воспользуемся уже готовым из исходников)
Обратите еще внимание на регистры смещения/прокрутки текстовых фонов — их 4 пары для каждого. Регистры с буквой H — это горизонтальные координаты пикселя фона, который будет отображаться в левом-верхнем углу экрана, а с буквой V — соответственно вертикальные. Важно отметить, что число может принимать отрицательные значения (фон при этом на экране будет уезжать вниз-вправо). При этом регистры имеют только 10 значащих (нижних) бит, но это не вызывает никаких проблем в силу природы целых отрицательных чисел на основной массе компьютеров, включая GBA — спокойно присваивайте этим регистрам любые и положительные и отрицательные значения — результат будет логичным.
В этом уроке я еще пока не описываю подробно спрайты и вращающиеся фоны, хотя и некоторые макросы ответственные за них в списке появились.
Не буду я так же в нём реализовывать зеркалирование тайлов или 16-цветные режимы — но в описании есть всё необходимое, чтобы вы могли сделать это самостоятельно.
Итак, создадим программу, выводящую на экран 2 текстовых фона — на одном будут по порядку 256 тайлов залитых однотонными цветами, а на другом, сверху, имитация падающего снега. DPad-ом можно бесконечно скроллить и задний и передний фоны с лёгким эффектом парралакса, а кнопка A возвращает координаты прокрутки в начальное значение.
05_simple_bg.cpp
#include <stdlib.h>
#include <string.h>
#include "gba_defs.h"
// Зададим флаг желаемого размера тайловых карт и
// (согласованно с ним) её ширину в тайлах (TMW - tile map width) и
// её высоту в тайлах (TMH = tile map height).
// Попробуйте выбирать другие значения (только согласованные).
#define BG_SIZE BG_SIZE_512x512
#define TMW 64
#define TMH 64
// Опорные указатели на нужные куски видеопамяти.
// Объяснение смотрите ниже в коде.
#define TILE_DATA ((u16*) (VID_RAM_START + 0x0000) )
#define TILE_MAP0 ((u16*) BG_TILE_MAP_ADDR( 16 ) )
#define TILE_MAP1 ((u16*) BG_TILE_MAP_ADDR( 20 ) )
#if TMW == 64
// Тайловые карты размеров 64x32 или 64x64 всё равно
// состоят из до четырёх последовательно идущих подмассивов
// размером 32x32 тайлов, которые в свою очередь выводятся
// на экран в следующем порядке:
// +---+---+
// | 0 | 1 |
// +---+---+
// | 2 | 3 |
// +---+---+
// Поэтому для получения индекса тайла (x,y) надо сперва
// поправить базовый указатель на начало карты в указатель
// на один из внутренних секторов размером 32x32.
// Функция возвращает ссылку на тайловый индекс в координатах
// (x,y) для тайловой карты, начинающейся по адресу base:
inline u16 &tile( u16 *base, int x, int y )
{
if ( x > 31 )
{
x -= 32;
base += 32 * 32;
};
if ( y > 31 )
{
y -= 32;
base += 2 * 32 * 32;
};
return base[ x + y * 32 ];
};
#else
// Для тайловых карт 32x32 и 32x64 всё проще:
inline u16 &tile( u16 *base, int x, int y )
{
return base[ x + y * 32 ];
};
#endif
int main(void)
{
// Инициализируем палитру так, чтобы 8-битный индекс цвета
// совпадал с цветовой маской BBGGGRRR
// BB GGG RRR
// 76 543 210
for ( unsigned int i = 0; i < 256; i++ )
{
BGR_PALETTE[ i ] = RGB( (i & 7) << 2, (i & 56) >> 1, (i & 192) >> 3 );
};
// Очистим первые 64Кб видеопамяти
memset( (void*) VID_RAM_START, 0, 65536 );
// Включаем MODE_0 и отображение BG0 и BG1
REG_DISPCNT = MODE_0 | BG0_ENABLE | BG1_ENABLE;
// Включаем задние фоны:
// Непрерывный массив из 512 256-цветных тайлов будет начинаться в начале
// видеопамяти и занимать 512*8*8=32768 байт.
// Сразу после него по адресу VID_START+32768 (16-ый логический индекс)
// будет находится тайловая карта BG0 максимальным размером 64x64x2=8192 байт.
REG_BG0CNT = BG_PRIORITY( 1 ) | BG_TILE_DATA_AT_0000 | BG_COLOR_256 |
BG_TILE_MAP_AT( 16 ) | BG_SIZE;
// Сразу после BG0 по адресу VID_START+40960 (20-ый логический индекс)
// будет находится тайловая карта BG1. Ей не будут нужны нижние 256 тайлов,
// поэтому она за базу тайловых данных возьмёт адрес VID_START+0x4000,
// таким образом для неё тайл с нулевым индексом будет тайлом
// с индексом 256 для тайловой карты BG0.
REG_BG1CNT = BG_PRIORITY( 0 ) | BG_TILE_DATA_AT_4000 | BG_COLOR_256 |
BG_TILE_MAP_AT( 20 ) | BG_SIZE;
// Инициализирум первые 256 тайлов всеми цветами палитры по порядку.
// Запись ведем 2-х-байтовыми u16, поэтому они спаренные.
for ( int i = 0; i < 256; i++ )
{
u16 *tile_base = TILE_DATA + i * 8 * 4;
for ( int j = 0; j < 8 * 4; j++ )
{
tile_base[ j ] = (i << 8) + i;
};
};
// Тайл 256 весь прозрачный с одним белым пикселем
// для создания эффекта "снега" вторым фоном.
TILE_DATA[ 8 * 4 * 256 + 6 ] = (0 << 8) + 255;
// Заполним тайловые карты построчно...
u16 val = 0;
for ( int y = 0; y < TMH; y++ )
{
for ( int x = 0; x < TMW; x++ )
{
// В BG0 все тайлы по порядку. 512 штук, вторая часть из
// которых преимущественно черная.
tile( TILE_MAP0, x, y ) = (val++) & 511;
// BG1 вся заполнена тайлом со "снегом". Заметьте, что в
// ней его индекс нулевой, т.к. у этого фона другая база для данных.
tile( TILE_MAP1, x, y ) = 0;
};
};
// Координаты скроллинга
int x = 0, y = 0;
// Координаты смещения "снега"
int sx = 0, sy = 0;
// Бесконечный цикл
while ( true )
{
// Дождёмся выхода в VBLANK
while ( REG_VCOUNT < 160 );
// Считаем текущее состояние кнопок и сразу
// инвертируем биты, чтобы нажатые стали единицами.
int keys = ~REG_KEYS;
// Реагируем на нажатые кнопки.
if ( keys & KEY_LEFT )
x -= 1;
if ( keys & KEY_RIGHT )
x += 1;
if ( keys & KEY_UP )
y -= 1;
if ( keys & KEY_DOWN )
y += 1;
if ( keys & KEY_A )
{
x = 0;
y = 0;
};
if ( keys & KEY_B )
{
};
// Обновим скроллинг первого фона
REG_BG0HOFS = x;
REG_BG0VOFS = y;
// Обновим "снежок"
sx += 1;
sy -= 3;
REG_BG1HOFS = x * 2 + sx / 32;
REG_BG1VOFS = y * 2 + sy / 32;
// Меняем индексы в двух краевых тайлах, чтобы они выделялись на общем фоне
u16 &t0 = tile( TILE_MAP0, 0, 0 );
t0++;
if ( t0 > 255 )
t0 = 0;
u16 &t1 = tile( TILE_MAP0, 0, TMH );
t1++;
if ( t1 > 255 )
t1 = 0;
// Поперебираем пиксель "снежинки" для
// эффекта мерцания
u16 &t2 = TILE_DATA[ 8 * 4 * 256 + 6 ];
if ( t2 )
t2 = 0;
else
t2 = 255;
// Доводим синхронизацию по VLANK до конца.
while ( REG_VCOUNT >= 160 );
};
return 0;
}
чтобы скомпилировать этот код делаем привычный батник build_05_simple.bat:
@SET MODULES=
@set PROGNAME=05_simple_bg
@call build_gba.bat
Итак, здесь мы схватили тайлы за хвост в самых простых — текстовых фонах.
Здесь же видно, что благодаря зацикливанию при прокрутке, немного помозговав, можно реализовать иллюзию скроллинга по сколь угодно большому полю. Причём с помощью арифметики с обрезкой по битам (чему способствует то, что размеры тайловых карт в пикселях всегда степени двойки) это можно реализовать очень экономно — дорисовыванием только одной полоски краевых тайлов один раз на каждые 8 продвинутых пикселей. Это очень небольшой объём работы для процессора и падения FPS из-за возни с графикой тут не предвидятся. Реализуем такой принцип «бесконечного» скроллинга в следующей программе. В ней мы создадим большую карту размером 256x256 клеток в основной памяти GBA, а в тайловой карте видеопамяти будем поддерживать только небольшой кусок её отображения 31x21 тайла (ровно на 1 тайл больше по каждой из осей, чем влезает на экран — именно столько может быть видно одновременно тайлов если есть небольшая прокрутка). При помощи регистров прокрутки и обрезки координат в тайловой карте по «модулю 32» мы сведем к минимуму число операций, которые нужно выполнять для плавного скроллинга.
По большому игровому полю можно двигаться D-pad-ом, кнопки A и B включают малое и большое ускорение прокрутки соответственно. А кнопками L и R можно закрашивать клетки поля в центре экрана черными и белыми тайлами. Зажав SELECT можно увидеть тайловую карту без прокруток, чтобы увидеть её наполнение «как оно есть». Если при этом двигаться, то легко заметить полоски обновления новых областей и оценить изящность трюка с другой точки зрения. Тайловая карта, являясь «окном» в огромную карту поля, «прорублена» тем не менее хитрым образом — координаты конкретной клетки поля в тайловой карте никогда не плавают, как можно было бы ожидать, а являются фиксированными через отбрасывание битов выше пятого, т.е. операциями x & 31 и y & 31. Плавают в тайловой карте границы отображения, а не клетки поля. Таким образом мы и добиваемся минимальных затрат на перерисовку тайловой карты в движении.
05_scroll_bg.cpp
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include "gba_defs.h"
// Опорные указатели на нужные куски видеопамяти.
// Объяснение смотрите ниже в коде.
#define TILE_DATA ((u16*) (VID_RAM_START + 0x0000) )
#define TILE_MAP0 ((u16*) BG_TILE_MAP_ADDR( 16 ) )
// Ссылка на клетку тайловой карты 32x32 или 32x64
inline u16 &tile( u16 *base, int x, int y )
{
return base[ x + y * 32 ];
};
const int wmap_w = 128, wmap_h = 128; // Размеры карты поля в клетках-тайлах
u8 *wmap = NULL; // Карта поля будет динамическим массивом из байт
int vx = 0, vy = 0; // координаты viewport
// Координаты отображаемого левого-верхнего на экране тайла в текущем кадре
int cur_tx = 0, cur_ty = 0;
// Координаты отображаемого левого-верхнего на экране тайла на предыдущем кадре
int old_tx = -1, old_ty = -1;
// В демонстрационно-отладочных целях показать содержимое тайловой карты без прокрутки
bool show_base_of_tile_map = false;
// Ссылка на клетку карты поля (world map)
inline u8 &wcell( int x, int y )
{
return wmap[ x + y * wmap_w ];
};
// "Загнать" координаты viewport в такие рамки, чтобы не выйти за границы
// поля и вычислить cur_tx/ty.
void check_bounds()
{
vx = std::max( 0, vx );
vy = std::max( 0, vy );
vx = std::min( wmap_w * 8 - 240, vx );
vy = std::min( wmap_h * 8 - 160, vy );
cur_tx = vx >> 3;
cur_ty = vy >> 3;
};
// Обновить содержимое тайловой карты актуальными данными из карты поля.
// Задаются координаты клеток прямоугольной области (tx0,ty0,tx1,ty1) поля,
// которая должна быть скопирована в соответствующие ей клетки тайловой карты.
// Работа с координатами тайловой карты ведется по модулю 32 (x & 31) чтобы
// замкнуть отображение в её ограниченный размер 32x32 и обновлять только
// необходимый минимум клеток при участии механизма прокрутки фона (см. ниже).
// Для тестов и отладки флагом white можно включить закраску белыми тайлами.
void refill_viewport_area( int tx0, int ty0, int tx1, int ty1, bool white = false )
{
// Total refill screen rectangle by data
for ( int y = ty0; y <= ty1; y++ )
{
for ( int x = tx0; x <= tx1; x++ )
{
tile( TILE_MAP0, x & 31, y & 31 ) = white ? 255 : wcell( x, y );
};
};
};
// Обновить с предыдущего кадра состояние экрана (viewport).
// Должны быть правильно рассчитаны cur_tx, cur_ty и vx, xy не должны
// выходить за границы поля - т.е. предварительно надо вызвать check_bounds().
// Функция обновляет содержимое регистров прокрутки и, если в тайловой карте
// стали видны устаревшие полоски тайлов - обновляет их содержимое из карты поля.
// Важнейшую роль играет механизм отображения тайловых карт видеокартой GBA и то
// как происходит зацикливание при их прокрутке - работая с координатами клеток в
// тайловой карте "по модулю 32" мы ликвидируем необходимость обновлять всю
// площадь тайловой карты при плавной прокрутке.
// Тем не менее параметром force_refill можно принудительно выполнить полное обновление.
void refresh_viewport( bool force_refill = false )
{
// В регистры прокрутки фона просто записываем смещения vx и vy.
// Важно понимать, что т.к. карта зациклена при прокрутке в область
// 256x256 пикселей, то обрезка верхних незначащих бит этих значений
// ведет себя ровно так, как нам надо.
if ( !show_base_of_tile_map )
{
REG_BG0HOFS = vx;
REG_BG0VOFS = vy;
}
else
{
// В отладочных целях посмотрим на тайловую карту без сдвигов...
REG_BG0HOFS = 0;
REG_BG0VOFS = 0;
};
// Если координаты клетки, отображаемой в левом-верхнем углу экрана
// "уплыли" со своего предыдущего значения - перерисовываем нужные
// полоски клеток в тайловой карте.
if ( (old_tx != cur_tx) || (old_ty != cur_ty) || force_refill )
{
// Вычисляем разницу координат между предыдущим и текущим положением.
int dtx = cur_tx - old_tx;
int dty = cur_ty - old_ty;
int dtxa = std::abs( dtx );
int dtya = std::abs( dty );
// Если "уплыли" по координатам слишком далеко или перерисовка полосками
// будет нерациональна ввиду большой площади пересечения - включим
// режим полной перерисовки.
if ( (dtxa > 30) || (dtya > 20) || (dtxa * dtya > 30 * 20 / 4) )
{
force_refill = true;
};
if ( force_refill )
{
// Полная перерисовка
refill_viewport_area( cur_tx, cur_ty, cur_tx + 30, cur_ty + 20 );
}
else
{
if ( dtx > 0 )
{
// Перекрашиваем полоску справа
refill_viewport_area( cur_tx + 30 - dtx + 1, cur_ty, cur_tx + 30, cur_ty + 20 );
}
else if ( dtx < 0 )
{
// Перекрашиваем полоску слева
refill_viewport_area( cur_tx, cur_ty, cur_tx - dtx - 1, cur_ty + 20 );
};
if ( dty > 0 )
{
// Перекрашиваем полоску снизу
refill_viewport_area( cur_tx, cur_ty + 20 - dty + 1, cur_tx + 30, cur_ty + 20 );
}
else if ( dty < 0 )
{
// Перекрашиваем полоску сверху
refill_viewport_area( cur_tx, cur_ty, cur_tx + 30, cur_ty - dty - 1 );
};
};
// Обновляем предыдущие координаты левой-верхней отображаемой клетки
old_tx = cur_tx;
old_ty = cur_ty;
};
};
// Закрасить в карте поля прямоугольную рамку цветом val.
void wmap_rectangle( int x0, int y0, int x1, int y1, int val )
{
for ( int y = y0; y <= y1; y++ )
{
wcell( x0, y ) = val;
wcell( x1, y ) = val;
};
for ( int x = x0; x < x1; x++ )
{
wcell( x, y0 ) = val;
wcell( x, y1 ) = val;
};
};
int main(void)
{
// Инициализируем палитру так, чтобы 8-битный индекс цвета
// совпадал с цветовой маской BBGGGRRR
// BB GGG RRR
// 76 543 210
for ( unsigned int i = 0; i < 256; i++ )
{
BGR_PALETTE[ i ] = RGB( (i & 7) << 2, (i & 56) >> 1, (i & 192) >> 3 );
};
// Очистим первые 64Кб видеопамяти
memset( (void*) VID_RAM_START, 0, 65536 );
// Включаем MODE_0 и отображение BG0 и BG1
REG_DISPCNT = MODE_0 | BG0_ENABLE; // | BG1_ENABLE;
// Включаем задние фоны:
// Непрерывный массив из 512 256-цветных тайлов будет начинаться в начале
// видеопамяти и занимать 512*8*8=32768 байт.
// Сразу после него по адресу VID_START+32768 (16-ый логический индекс)
// будет находится тайловая карта BG0 максимальным размером 64x64x2=8192 байт.
REG_BG0CNT = BG_PRIORITY( 1 ) | BG_TILE_DATA_AT_0000 | BG_COLOR_256 |
BG_TILE_MAP_AT( 16 ) | BG_SIZE_256x256;
// Инициализирум первые 256 тайлов всеми цветами палитры по порядку.
// Запись ведем 2-х-байтовыми u16, поэтому они спаренные.
for ( int i = 0; i < 256; i++ )
{
u16 *tile_base = TILE_DATA + i * 8 * 4;
for ( int j = 0; j < 8 * 4; j++ )
{
tile_base[ j ] = (i << 8) + i;
};
};
// Инициализируем карту случайными тайлами
srand( 100 );
wmap = new u8[ wmap_w * wmap_h ];
for ( int y = 0; y < wmap_h; y++ )
{
for ( int x = 0; x < wmap_w; x++ )
{
wcell( x, y ) = rand() & 255;
};
};
// Обводим черной рамкой границы
wmap_rectangle( 0, 0, wmap_w - 1, wmap_h - 1, 0 );
// Рисуем 20 случайных прямоугольных рамок белым цветом
for ( int i = 0; i < 20; i++ )
{
int x0 = rand() % wmap_w;
int y0 = rand() % wmap_h;
int x1 = rand() % wmap_w;
int y1 = rand() % wmap_h;
wmap_rectangle( std::min( x0, x1 ), std::min( y0, y1 ),
std::max( x0, x1 ), std::max( y0, y1 ), 255 );
};
// Инициализируем координаты вьюпорта
vx = 0;
vx = 0;
check_bounds();
// Принудительно обновляем viewport данными из карты поля
refresh_viewport( true );
// Бесконечный цикл
while ( true )
{
// Дождёмся выхода в VBLANK
while ( REG_VCOUNT < 160 );
// Считаем текущее состояние кнопок и сразу
// инвертируем биты, чтобы нажатые стали единицами.
int keys = ~REG_KEYS;
show_base_of_tile_map = false;
// Флаг: нужно ли в этом кадре делать принудительную перерисовку
// всей области вьюпорта
bool force = false;
int step = 1; // Размер "шага" при движении в пикселях
// При нажатии на A или B ускоряемся...
if ( keys & KEY_A )
step = 10;
if ( keys & KEY_B )
step = 30;
// Реагируем на нажатые кнопки.
if ( keys & KEY_LEFT )
vx -= step;
if ( keys & KEY_RIGHT )
vx += step;
if ( keys & KEY_UP )
vy -= step;
if ( keys & KEY_DOWN )
vy += step;
// По кнопкам L и R красим центр карты в черный или белый цвета
if ( keys & KEY_L )
{
wcell( (vx >> 3) + 15, (vy >> 3) + 10 ) = 0;
force = true;
};
if ( keys & KEY_R )
{
wcell( (vx >> 3) + 15, (vy >> 3) + 10 ) = 255;
force = true;
};
if ( keys & KEY_SELECT )
{
show_base_of_tile_map = true;
};
// Проверяем вход координат вьюпорта в границы
check_bounds();
// И обновляем вьюпорт
refresh_viewport( force );
// Доводим синхронизацию по VLANK до конца.
while ( REG_VCOUNT >= 160 );
};
return 0;
}
и батник build_05_scroll.bat:
@SET MODULES=
@set PROGNAME=05_scroll_bg
@call build_gba.bat
О вращающихся тайловых картах я расскажу в последних уроках. Но даже текстовых уже достаточно, чтобы на их основе сделать неплохой скроллер.
Следующий же урок будет посвящён второму краеугольному столпу тайловой графики — спрайтам.
Продолжение...
0 комментариев