В этой статье рассмотрим работу с тайловой графикой, прерываниями, сенсорным экраном и клавиатурой. На основе этого напишем всем с детства известную игру — «пятнашка».
Для начала поподробнее разберём работу с видеоконтроллером DS.
Практически все видеорежимы используют «многослойную» структуру организации вывода на экран, то есть одновременно мы можем отображать до 4-х планов (background). Не знаю хорошенько какой термин лучше использовать, пусть будет «план» — «задний план».
Всего имеется 6 типов задних планов:
Как уже упоминалось оба графических ядра Nintendo DS используют общую видеопамять (656КБ). Она разделена на 9 банков различного размера и назначения, проименованных латинскими буквами от A до I. Здесь полный список. Чтобы видеоконроллер мог использовать эти банки мы должны отбразить («замапить») их в специальную область адресного пространства, начинающуюся с 0x06000000.
Более развёрнуто про организацию видеопамяти и назначение различных банков можно почитать тут.
В игре будем использовать нулевой графический режим видеоконтроллера (MODE_0_2D), в котором имеется 4 тайловых плана. На нижнем экране (по умолчанию дополнительное ядро) в одном из них будет собственно происходить игра (перемещение фишек), а другой применим для отображения заставочной картинки. Верхний же экран (основное ядро) используем просто для вывода текстовой информации.
Теперь немного углубимся в организацию видеопамяти в режиме тайловых планов. Картинка на экране в этом режиме формируется на основе так называемой тайловой карты, в которой прописаны номера тайлов, которые должны быть отображены в квадратах 8х8 пикселей на изображении. Сами же тайлы хранятся в отдельной области памяти. Из каких адресов видеоконтроллер будет отображать карту и тайлы определяется регистром управления (Control Register — CR). Для каждого из 8 планов (4 на основном ядре и 4 на дополнительном) имеется свой регистр. В нашем случае нужно инициализировать 3 из них: SUB_BG0_CR, SUB_BG1_CR и BG0_CR — по одному на каждый из используемых планов.
Тут есть небольшая хитрость. Дело в том, что регистры управления 16 битные, а в них нужно хранить и адрес карты и адрес тайлов и прочие параметры. Всвязи с этим под адреса выделено по 5 бит. Таким образом тайлы могут храниться по 32-м базовым адресам со смещением в 16КБ, а карты по 32-м адресам со мещением в 2КБ.
При том, что они хранятся в одном банке памяти имеем такую картину:
Нам потребуется 2 тайловых карты для нижнего экрана. Они будут располагаться по базовым адресам 0 и 1. Также нам потребуются 2 набора самих тайлов. Нулевой базовый адрес тайлов пересекается с используемой памятью карт, так что мы не будем его использовать. С базового адреса №1 расположим тайлы фишек. Они занимают 36 КБ, поэтому базовые адреса 2, 3 и 4 мы тоже не будем использовать. Далее с адреса 5 разместим набор тайлов для стартовой заставки.
Для верхнего же экрана используем тайловую карту с базового адреса 0, а сами тайлы, в которых будет помещаться русский шрифт, разместим с адреса 1. Регистр управления для текста установит libNDS при инициализации консоли.
Теперь инициализируем наши регистры управления для использования 16 цветных тайлов (BG_COLOR16):
На самом деле здесь можно было сэкономить изрядное количество видеопамяти (32КБ), если разместить тайлы и карты по другим базовым адресам. Однако, в нашем случае такая оптимизация не требуется, т.к. свободной памяти более чем достаточно.
Теперь преобразуем номера базовых адресов в абсолютные адреса видеопамяти для того, чтобы можно было с ней работать напрямую:
Затем скопируем данные наших тайлов в видеопамять:
И сразу заполним нулевой план нижнего экрана тайлами заставки:
Каждое слово в тайловой карте кроме номера тайла содержит в себе информацию о палитре и отражениях по осям. Для того, чтобы использовать первую палитру мы выставили 12 бит в 1 у каждого элемента карты.
Если побитово расписать элемент тайловой карты, увидим следующее:
Инициализируем консоль libNDS:
В программе используем шрифт созданный ClusterM для PAlib в кодировке CP1251. К сожалению в текущей версии библиотеки при попытке перехода на юникод поломали поддержку вывода символов верхней половины ASCII, поэтому придётся обойтись без русского текста. Хотя конечно его можно выводить напрямую записывая в тайловую карту коды символов.
Все тайлы создаются путём преобразования из BMP при помощи программы grit.
Для считывания состояния клавиш без использования libNDS нам пришлось бы использовать не только регистры процессора ARM9, но и ARM7. К счастью создатели библиотеки позаволяют абстрагироваться от этого факта. Мы просто используем функцию scanKeys() для обновления состояния информации о нажатиях. А keysHeld() для определения того, какая клавиша нажата, или действует нажатие на сенсорный экран. Что именно нажато определяем в соотвтствии с битами возвращённого функцией значения:
Так что мы просто в цикле выполняем:
И затем в зависимости от того, что в переменной held выполняем нужные действия.
Если выставлен бит KEY_TOUCH, значит обнаружено нажатие на тачскрин и мы можем считать координаты стилуса посредством функции touchRead. Она возвращает структуру touchPosition, в которой нас интересуют поля px и py, содержащие координаты пикселя на который указывает стилус:
Для нормальной работы большинства программ взаимодействующих с пользователем (наша программа не исключение), требуется контроль временных интервалов, который обычно обеспечивается прерываниями от таймеров. Существует три регистра для работы с прерываниями:
Главный регистр включения прерываний (Interrupt Master Enable Register) предоставляет возможность включать и выключать обработчики всех прерываний.
Регистр включения прерываний (Interrupt Enable Register) позволяет включать и выключать отдельные прерывания. Каждый бит регистра отвечает за определённое прерывание:
Регистр флагов прерываниий (Interrupt Flags Register) устанавливается аппаратно когда возникает прерывание. Он содержит битовую маску прерываний.
Мы не будем работать с прерыванииями напрямую, а как обычно, воспользуемся услугами libnds.
Сначала установим обработчик прерывания по «обратному ходу луча по вертикали». Это прерывание будет вызываться когда закончена отрисовка экрана. В обработчика этого прерывания будем выводить картинку, чтобы избежать мерцания и разрывов изображения:
Далее настроим один из таймеров на нужную частоту и установим на него обработчик прерывания. Библиотека libNDS для этих целей предоставляет весьма удобную функцию timerStart. Нам достаточно вызвать эту функцию с нужным делителем, частотой и указателем на обработчик прерывания, для того, чтобы провести полную инициализацию таймера и прерывания.
Напоследок рассмотрим ещё одну функцию предоставлямую библиотекой libnds — swiWaitForVBlank. Она останавливает процессор ARM9 до прихода прерывания обратного хода луча по вертикали.
Используя всё рассмотренное выше можно уже написать простенькую игру. Здесь можно взять исходный код игры «пятнашка», а здесь исполняемый файл.
Скриншот:
Для начала поподробнее разберём работу с видеоконтроллером DS.
Инициализация видеоконтроллера
Практически все видеорежимы используют «многослойную» структуру организации вывода на экран, то есть одновременно мы можем отображать до 4-х планов (background). Не знаю хорошенько какой термин лучше использовать, пусть будет «план» — «задний план».
Всего имеется 6 типов задних планов:
- framebuffer — Самый простой тип заднего плана. Каждое слово (16бит) в видеопамяти отображается в виде пикселя на экране. (Использовался в прошлом примере);
- 3D — Картинка на экране формируется OpenGL-подобными командами;
- text — Текстовый задний план (он же тайловый) разделён на блоки 8х8 пикселей, в каждом из которых отображается один из тайлов;
- rotation — Тайловый план с возможнотью вращения и масштабирования;
- extended rotation — Тоже что и фреймбуфер, но ещё позволяет отображать глубину цвета 8 бит на пиксель, а также поддерживает скроллинг, масштабирование и вращение, кроме того может использовать альфа-бит;
- large bitmap — Большие 512х1024 или 1024х512 изображения с 8 битами на пиксель.
Как уже упоминалось оба графических ядра Nintendo DS используют общую видеопамять (656КБ). Она разделена на 9 банков различного размера и назначения, проименованных латинскими буквами от A до I. Здесь полный список. Чтобы видеоконроллер мог использовать эти банки мы должны отбразить («замапить») их в специальную область адресного пространства, начинающуюся с 0x06000000.
Более развёрнуто про организацию видеопамяти и назначение различных банков можно почитать тут.
В игре будем использовать нулевой графический режим видеоконтроллера (MODE_0_2D), в котором имеется 4 тайловых плана. На нижнем экране (по умолчанию дополнительное ядро) в одном из них будет собственно происходить игра (перемещение фишек), а другой применим для отображения заставочной картинки. Верхний же экран (основное ядро) используем просто для вывода текстовой информации.
videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE); //Видеорежим основного ядра
videoSetModeSub(MODE_0_2D | DISPLAY_BG1_ACTIVE | DISPLAY_BG0_ACTIVE); //Видеорежим дополнительного ядра
* This source code was highlighted with Source Code Highlighter.
Теперь немного углубимся в организацию видеопамяти в режиме тайловых планов. Картинка на экране в этом режиме формируется на основе так называемой тайловой карты, в которой прописаны номера тайлов, которые должны быть отображены в квадратах 8х8 пикселей на изображении. Сами же тайлы хранятся в отдельной области памяти. Из каких адресов видеоконтроллер будет отображать карту и тайлы определяется регистром управления (Control Register — CR). Для каждого из 8 планов (4 на основном ядре и 4 на дополнительном) имеется свой регистр. В нашем случае нужно инициализировать 3 из них: SUB_BG0_CR, SUB_BG1_CR и BG0_CR — по одному на каждый из используемых планов.
Тут есть небольшая хитрость. Дело в том, что регистры управления 16 битные, а в них нужно хранить и адрес карты и адрес тайлов и прочие параметры. Всвязи с этим под адреса выделено по 5 бит. Таким образом тайлы могут храниться по 32-м базовым адресам со смещением в 16КБ, а карты по 32-м адресам со мещением в 2КБ.
При том, что они хранятся в одном банке памяти имеем такую картину:
Нам потребуется 2 тайловых карты для нижнего экрана. Они будут располагаться по базовым адресам 0 и 1. Также нам потребуются 2 набора самих тайлов. Нулевой базовый адрес тайлов пересекается с используемой памятью карт, так что мы не будем его использовать. С базового адреса №1 расположим тайлы фишек. Они занимают 36 КБ, поэтому базовые адреса 2, 3 и 4 мы тоже не будем использовать. Далее с адреса 5 разместим набор тайлов для стартовой заставки.
Для верхнего же экрана используем тайловую карту с базового адреса 0, а сами тайлы, в которых будет помещаться русский шрифт, разместим с адреса 1. Регистр управления для текста установит libNDS при инициализации консоли.
Теперь инициализируем наши регистры управления для использования 16 цветных тайлов (BG_COLOR16):
int tile_base = 1;
int map_base = 0;
int tile_base_s = 5;
int map_base_s = 1;
int char_base = 1;
int scr_base = 0;
REG_BG0CNT_SUB = BG_COLOR_16 | BG_TILE_BASE(tile_base_s) | BG_MAP_BASE(map_base_s); //Заставка
REG_BG1CNT_SUB = BG_COLOR_16 | BG_TILE_BASE(tile_base) | BG_MAP_BASE(map_base); //Игровые фишки
* This source code was highlighted with Source Code Highlighter.
На самом деле здесь можно было сэкономить изрядное количество видеопамяти (32КБ), если разместить тайлы и карты по другим базовым адресам. Однако, в нашем случае такая оптимизация не требуется, т.к. свободной памяти более чем достаточно.
Теперь преобразуем номера базовых адресов в абсолютные адреса видеопамяти для того, чтобы можно было с ней работать напрямую:
u16* sub_tile = (u16*)BG_TILE_RAM_SUB(tile_base);
u16* sub_map = (u16*)BG_MAP_RAM_SUB(map_base);
u16* sub_tile0 = (u16*)BG_TILE_RAM_SUB(tile_base_s);
u16* sub_map0 = (u16*)BG_MAP_RAM_SUB(map_base_s);
u16* tile_char = (u16*)BG_TILE_RAM(char_base);
u16* map_char = (u16*)BG_MAP_RAM(scr_base);
* This source code was highlighted with Source Code Highlighter.
Затем скопируем данные наших тайлов в видеопамять:
memcpy((void*)sub_tile, (u8*)tilesTiles, 192*192/2); //Тайлы фишек
for (i=0; i < 16; ++i)
BG_PALETTE_SUB[i] = tilesPal[i]; //Палитра
memcpy((void*)sub_tile0, (u8*)startTiles, 256*192/2);//Тайлы заставки
for (i=0; i < 16; ++i)
BG_PALETTE_SUB[i+16] = startPal[i]; //Палитра
* This source code was highlighted with Source Code Highlighter.
И сразу заполним нулевой план нижнего экрана тайлами заставки:
for (i=0; i< 24*32; i++) //Выводим заставку
sub_map0[i] = (u16)(i)|0x1000;
* This source code was highlighted with Source Code Highlighter.
Каждое слово в тайловой карте кроме номера тайла содержит в себе информацию о палитре и отражениях по осям. Для того, чтобы использовать первую палитру мы выставили 12 бит в 1 у каждого элемента карты.
Если побитово расписать элемент тайловой карты, увидим следующее:
Биты | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Назначение | Палитра | Ветрик.отр. | Гориз.отр. | Номер тайла |
Инициализируем консоль libNDS:
PrintConsole *console = consoleInit(NULL, 0, BgType_Text4bpp, BgSize_T_256x256, scr_base, char_base, true, true);
ConsoleFont font;
font.gfx = (u16*)pa_text2Tiles; //Наш шрифт
font.pal = (u16*)pa_text2Pal; //Палитра
font.numChars = 256;//Количество символов
font.numColors = pa_text2PalLen/2;
font.bpp = 4;
font.asciiOffset = 0;
font.convertSingleColor = false;
consoleSetFont(console, &font);
* This source code was highlighted with Source Code Highlighter.
В программе используем шрифт созданный ClusterM для PAlib в кодировке CP1251. К сожалению в текущей версии библиотеки при попытке перехода на юникод поломали поддержку вывода символов верхней половины ASCII, поэтому придётся обойтись без русского текста. Хотя конечно его можно выводить напрямую записывая в тайловую карту коды символов.
Все тайлы создаются путём преобразования из BMP при помощи программы grit.
Клавиатура и тачскрин
Для считывания состояния клавиш без использования libNDS нам пришлось бы использовать не только регистры процессора ARM9, но и ARM7. К счастью создатели библиотеки позаволяют абстрагироваться от этого факта. Мы просто используем функцию scanKeys() для обновления состояния информации о нажатиях. А keysHeld() для определения того, какая клавиша нажата, или действует нажатие на сенсорный экран. Что именно нажато определяем в соотвтствии с битами возвращённого функцией значения:
Key Define | Mask Bit |
Associated Input |
---|---|---|
KEY_A | 1 << 0 | Кнопка A |
KEY_B | 1 << 1 | Кнопка B |
KEY_SELECT | 1 << 2 | Кнопка Select |
KEY_START | 1 << 3 | Кнопка Start |
KEY_RIGHT | 1 << 4 | Кнопка вправо |
KEY_LEFT | 1 << 5 | Кнопка влево |
KEY_UP | 1 << 6 | Кнопка вверх |
KEY_DOWN | 1 << 7 | Кнопка вниз |
KEY_R | 1 << 8 | Кнопка R |
KEY_L | 1 << 9 | Кнопка L |
KEY_X | 1 << 10 | Кнопка X |
KEY_Y | 1 << 11 | Кнопка Y |
KEY_TOUCH | 1 << 12 | Прикосновение к экрану |
KEY_LID | 1 << 13 | Крышка закрыта |
Так что мы просто в цикле выполняем:
scanKeys();
held = keysHeld();
* This source code was highlighted with Source Code Highlighter.
И затем в зависимости от того, что в переменной held выполняем нужные действия.
Если выставлен бит KEY_TOUCH, значит обнаружено нажатие на тачскрин и мы можем считать координаты стилуса посредством функции touchRead. Она возвращает структуру touchPosition, в которой нас интересуют поля px и py, содержащие координаты пикселя на который указывает стилус:
if (held&KEY_TOUCH){ //Нажатие на тачскрин
touchRead(&touchXY);
...
}
* This source code was highlighted with Source Code Highlighter.
Прерывания
Для нормальной работы большинства программ взаимодействующих с пользователем (наша программа не исключение), требуется контроль временных интервалов, который обычно обеспечивается прерываниями от таймеров. Существует три регистра для работы с прерываниями:
Имя | Адрес | Размер | Описание |
---|---|---|---|
REG_IME | 0x04000208 | 16 bits | Главный регистр включения прерываний |
REG_IE | 0x04000210 | 32 bits | Регистр включения прерываний |
REG_IF | 0x04000214 | 32 bits | Регистр флагов прерываний |
Главный регистр включения прерываний (Interrupt Master Enable Register) предоставляет возможность включать и выключать обработчики всех прерываний.
Регистр включения прерываний (Interrupt Enable Register) позволяет включать и выключать отдельные прерывания. Каждый бит регистра отвечает за определённое прерывание:
Bit | Имена в libnds | Описание |
---|---|---|
0 | IRQ_VBLANK | обратный ход луча по вертикали |
1 | IRQ_HBLANK | обратный ход луча по горизонтали |
2 | IRQ_YTRIGGER | сканируется линия REG_VCOUNT |
3 | IRQ_TIMER0 | Сработал таймер 0 |
4 | IRQ_TIMER1 | Сработал таймер 1 |
5 | IRQ_TIMER2 | Сработал таймер 2 |
6 | IRQ_TIMER3 | Сработал таймер 3 |
7 | IRQ_NETWORK | ? |
8 | IRQ_DMA0 | DMA 0 |
9 | IRQ_DMA1 | DMA 1 |
10 | IRQ_DMA2 | DMA 2 |
11 | IRQ_DMA3 | DMA 3 |
12 | IRQ_KEYS | Нажата клавиша |
13 | IRQ_CART | Извлечён картридж GBA |
16 | У ARM7 сработало прерывание IPC | |
17 | Входная FIFO не пуста | |
18 | Выходная FIFO не пуста | |
19 | IRQ_CARD | DS card data completed |
20 | IRQ_CARD_LINE | DS card interrupt 3 |
21 | GFX FIFO interrupt |
Регистр флагов прерываниий (Interrupt Flags Register) устанавливается аппаратно когда возникает прерывание. Он содержит битовую маску прерываний.
Мы не будем работать с прерыванииями напрямую, а как обычно, воспользуемся услугами libnds.
Сначала установим обработчик прерывания по «обратному ходу луча по вертикали». Это прерывание будет вызываться когда закончена отрисовка экрана. В обработчика этого прерывания будем выводить картинку, чтобы избежать мерцания и разрывов изображения:
void IRQ_vblank(void){ //обработчик прерывания по обратному ходу луча
...Здесь выводим картинку...
}
...
irqSet(IRQ_VBLANK, IRQ_vblank); //Устанавливаем обработчик прерывания по обратному ходу луча.
* This source code was highlighted with Source Code Highlighter.
Далее настроим один из таймеров на нужную частоту и установим на него обработчик прерывания. Библиотека libNDS для этих целей предоставляет весьма удобную функцию timerStart. Нам достаточно вызвать эту функцию с нужным делителем, частотой и указателем на обработчик прерывания, для того, чтобы провести полную инициализацию таймера и прерывания.
void timer0_function(void){
...Здесь отсчитываем время игры...
}
...
timerStart(0, ClockDivider_256, TIMER_FREQ_256(1000), timer0_function); //Таймер с делителем 256 на 1000Гц
* This source code was highlighted with Source Code Highlighter.
Напоследок рассмотрим ещё одну функцию предоставлямую библиотекой libnds — swiWaitForVBlank. Она останавливает процессор ARM9 до прихода прерывания обратного хода луча по вертикали.
Используя всё рассмотренное выше можно уже написать простенькую игру. Здесь можно взять исходный код игры «пятнашка», а здесь исполняемый файл.
Скриншот: