В этом цикле статей мы реализуем тайловую графическую систему.
Начнем с того, что такое тайл?
Тайлы — небольшие изображения одинаковых размеров, которые и служат фрагментами большой картины.
Что же нам нужно для реализации этой графической системы?
- Набор тайлов
- «Карта» (массив) в котором будут храниться номера тайлов
- Программа, которая согласно «карте» будет выводить их на экран.
- Если карта большая — реализация вертикального/горизонтальное скроллинга (прокрутки)
В статьях будут примеры кода на C++/Wiring C++. В коде не будут применяться классы и прочее из соображений скорости, также не будет использоваться никаких сторонних библиотек. Код может быть не оптимизирован, я хочу донести лишь только идею.
Набор тайлов
Итак, нужно определиться с размером самого тайла. Пусть это будет 16×16. Создаем набор тайлов. Формат: 16x(кол-во тайлов*16), 256цветов. Также можно(нужно) определить цвет прозрачности, если ваша система не поддерживает палитру цветов с альфа-каналом.
Здесь Альфа-канал — ядовито-розовый цвет или 100%Красного + 100%Синего. Набор тайлов лучше хранить одной «вертикальной» картинкой, тогда доступ к ним будет осуществляться быстрее. Количество тайлов — 4. У каждого тайла есть свой номер, если отсчет начать с 1, то у последнего тайла будет номер 4. Чтобы добраться до последнего тайла — нам нужно пропустить (номер-1)*16 пикселей. Все элементарно!
Пример кода на C++:
1 2 3 4 5 6 7 | const uint8_t TexturePack[] = { 0x00, 0xE3, 0x56, 0xC6, 0x7D, .... }; |
Массив элементов (Карта)
Теперь представим, что нам нужно сделать большую «картину» или игровую карту из этих тайлов. Что же мы имеем? — Набор тайлов и их номера. Теперь нужно создать двумерный массив, каждая ячейка которого будет хранить в себе номер нужно тайла.
К примеру, у нас в распоряжении экран/видеопамять 256×208 (не говорите, что это очень мало :)). 1 тайл — 16х16. Соответственно, массив должен быть размером 16×13.
Давайте сопоставим значения с номерами тайлов из набора.
Напишем кусочек кода для этой матрицы на C++:
1 2 3 | struct Map{ unsigned char tile1[13][16]; }; |
А если мы хотим больше, чем один слой? Сделать задный план, передний план… Чтобы наши тайлы были могли накладываться друг на друга. Добавим соответствующие правки.
1 2 3 4 5 | struct Map{ unsigned char tile1[13][16]; unsigned char tile2[13][16]; unsigned char tile3[13][16]; }; |
Теперь у нас аж 3 массива.
Вывод тайла
Когда мы уже сумели представить наши тайлы в массиве, теперь нужно научиться выводить их на экран.
Для начала разберемся, как вывести вообще что-нибудь. Вывод будет происходить напрямую в видео-память. Ее мы представим в виде массива (одномерного): 256×208 (8bit). 1 пиксел — 1байт, тк 256 цветов. :-> VGA.buf — вот наш видео-буфер. (Не обращайте внимание на класс VGA) Обратиться к пикселю по x,y можно так: VGA.buf[y*256+x]. Создадим простую подпрограмму:
1 2 3 4 5 6 7 8 9 | void drawBitmap(unsigned char num, const uint8_t *tex, int16_t x, int16_t y) { const uint8_t p = tex+(num*16*16); for (uint8_t y=0; y<16; ++y) { for (uint8_t x=0; x<16; ++x) { VGA.buf[y*256+x] = *p; ++p; } } } |
Эта функция выведет указанный [номером-1] тайл из набора тайлов на координаты x/y в видео-память. Но если учитывать, что тайл может не поместиться в видео-памяти: один его кусочек будет заходить за грацицы. Нужна другая функция, чтобы выводить тайл с учитыванием этих нюансов. Один хороший человек, Кузин Андрей, уже проработал этот вопрос и подарил нам замечательную функцию, выводящую изображение, которую я немного переделал под тайловую систему.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void drawBitmap(unsigned char tex, const uint8_t* b, int16_t posx, int16_t posy) { uint16_t texx = (tex*16*16); int16_t lsx; if ((posx>256) || (posy>208) || (posx+16<=0) || (posy+16<=0)) return; int16_t x1,y1,x2,y2; x1=y1=0; x2=16; y2=16; if (posx<0) x1=-posx; if (posy<0) y1=-posy; if (posx+16>256) x2=256-posx; if (posy+16>208) y2=208-posy; lsx=x2-x1; const uint8_t *p1=b+texx+y1*16+x1; int16_t i; long Addr=(posy+y1)*256+posx+x1; for (i=y1; i<y2; ++i) { MoveLineOfImage(p1,VGA.buf+Addr,lsx); p1+=16; Addr+=VGA_W; } } inline void MoveLineOfImage(const uint8_t *Source, uint8_t *Dest, uint8_t Count) { for (uint8_t i=0; i<Count; ++i){ if ((*Source)!=0xE3) { *Dest=*Source; } ++Source; ++Dest; }; } |
Как пишет автор, он потратил более 3-х месяцев на создание и оптимизацию этой функции.
Пользоваться можно так же как и предыдущей.