Pull to refresh

Программирование для Nintendo DS. Простейшая игра

Reading time7 min
Views4.4K
В этой статье рассмотрим работу с тайловой графикой, прерываниями, сенсорным экраном и клавиатурой. На основе этого напишем всем с детства известную игру — «пятнашка».
Для начала поподробнее разберём работу с видеоконтроллером 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 до прихода прерывания обратного хода луча по вертикали.

Используя всё рассмотренное выше можно уже написать простенькую игру. Здесь можно взять исходный код игры «пятнашка», а здесь исполняемый файл.
Скриншот:


Tags:
Hubs:
+45
Comments11

Articles