Реактивное введение в программирование Game Boy Advance (часть 3 из 8)

Тайловые видеорежимы…

Оглавление


Часть 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)
Любая тайловая карта (и текстовая и вращающаяся) обладает свойствами, которые мы настраиваем через регистры управления REG_BGxCNT, где x меняется от 0 до 3:
  • Приоритет отрисовки — число от 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 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.