Реактивное введение в программирование Game Boy Advance (часть 8 из 8)
Сохранения…
Часть 1: Инструментарий, основы, кнопки, таймеры
Часть 2: Пиксельные видеорежимы
Часть 3: Тайловые видеорежимы
Часть 4: Спрайты
Часть 5: Вращающиеся фоны и прозрачность
Часть 6: Прерывания, DMA
Часть 7: Звук (Direct Sound)
Часть 8: Сохранения
Достаточно сложная игра немыслима без сохранений и загрузок. В GBA нет какого то единого слота флешки для сохранений, как в Playstation 1/2. Вместо этого картридж с игрой должен нести в себе чип позволяющий сделать сохранение. Таковые чипы бывают разных типов (SRAM, EEPROM и Flash) да еще и разных производителей.
Чипы Flash и EEPROM обладают несколько замысловатым интерфейсом взаимодействия — блочным чтением/записью, необходимостью стирать блок памяти перед его записью и тому подобное. Поэтому для этого введения я выбрал самый простой в программировании тип постоянного ОЗУ — SRAM (static RAM, статическое ОЗУ). Это просто микросхема статического ОЗУ, подпитываемая батарейкой. Static RAM позволяет одной таблеточной батарейкой поддерживать сохранение игры долгие годы. Эмуляторы же просто будут создавать файл с содержимым этого SRAM рядом с образом игры.
Согласно документации no$gba SRAM имеет объём 32Кб (вообще самый большой размер чипа с Flash — 128Кб, но чаще всего он равен 64Кб) и просто маппится на адреса 0xE000000-0xE007FFF. Как запись так и чтение с этих адресов можно производить только байтами (одно это сразу же отметает попытки общения со SRAM посредством DMA). Кроме того документация no$gba настаивает, что чтение из этих адресов должно производится кодом располагающимся в WRAM консоли — чтобы считывание кодов инструкций не «дёргало» адресные линии картриджа. При этом на запись такого ограничения нет. Ограничение это довольно таки серьезное, но во всех примерах, что я видел, этого не делается, а в эмуляторах и на flash-картриджах с реальным железом всё работает и без выполнения этого условия (подтверждена работоспособность на EverDrive GBA X5, при этом перед запуском игры там необходимо в меню уточнить тип чипа сохранений — выбрать вариант «SRAM»). У меня есть подозрение, что оно случайно скопировано из инструкций для Flash-памяти, так как именно там важно то как «дёргаются» адресные линии, хотя это не факт.
Однако просто начать писать и читать байты в адреса 0xE000000-0xE007FFF недостаточно. Дело в том что все остальные виды перезаписываемой памяти в GBA мапятся тоже на эти же адреса, но с другим интерфейсом, а заголовок картриджа не содержит никаких отметок о том какой тип перезаписываемой памяти используется. Игра просто знает сама какой чип находится в картридже и записывает в него данные нужным способом. С настоящим картриджем естественно никаких проблем это не вызывает, а вот нам придётся предпринять еще один шаг.
Дело в том, что эта проблема актуальна и для эмуляторов GBA и их разработчики подметили, что картриджи созданные с использованием официального SDK от Nintendo как правило содержат небольшие строчки помогающие идентифицировать чип сохранений:
Эти строчки должны располагаться по адресам кратным 4, а их длина тоже должна быть кратна 4 байтам (с заливкой нулями остатка). Официальные утилиты Nintendo как правило вставляют вместо 'nnn' цифры версий, но нам лучше так и оставлять их как 'nnn'.
Поэтому всё что нам остаётся сделать — это вставить в программу static массив из char с текстом «SRAM_Vnnn» и убедиться, что оптимизатор не выкинет его из финального образа.
Итак, всё что нам нужно для реализации нашей программы это добавить к разметке карте памяти в gba_def.h следующие определения:
После чего мы готовы написать программу сохраняющую состояние в SRAM:
Она будет представлять из себя один экран из черных и белых квадратиков над которым мы можем (DPad-ом) водить курсором. Нажатие кнопки (A) окрашивает клетку под курсором в белый, а (B) — в чёрный цвет. Нажатие START окрашивает все клетки чёрным, нажатие же ® сохраняет карту клеток в SRAM, а (L) — загружает из неё.
11_sram.cpp:
Не очень хорошая идея пробрасывать данные напрямую между видеопамятью и постоянной памятью, особенно если последняя выполнена по технологии отличающейся от SRAM — если мы выйдем за временные пределы VBlank, то доступ ЦП к видеопамяти резко замедлится и это может плохо сказаться на таймингах, которые могут быть критичны. Так что по хорошему надо сохранять память только из/в EWRAM или IWRAM. Но в данном случае всё работает.
Ну вот и всё. Сохранение данных — последнее из этого «реактивного» введения, что надо знать для создания полноценной видеоигры для Game Boy Advance. Есть немало вещей, которые остались за кадром, но необходимый минимум здесь дан, а многое из того что осталось без внимания или редко используются или может и вообще не понадобится. В любом случае, если вы владеете английским языком, то можете уже более внимательно и вдумчиво почитать материалы по ссылкам в начале этого введения.
Обратите внимание, что созданный по мере написания этого цикла gba_defs.h как можно сильнее старался соотвествовать использованной литературе, которые в свою очередь пыталась соответствовать официальному SDK от Nintendo, но он не является на 100%-совместимым с теми файлами и библиотеками, которые можно найти в составе DevKitPro. Впрочем, так как он на 99% состоит из define-ов, то особой проблемы в том, чтобы скрещивать накопленные в нём знания с другими заголовками не предвидится.
В качестве сквозного примера для демонстрации разработки игры архив с исходным кодом всех уроков содержит папку CFA с проектом реализации клона игры Contra Force (спираченная версия известна под именем Super Contra 6).
В нём используется компиляция с помощью make, что многое упрощает и делает возможным билд проекта через настраиваемую среду разработки. Я, например, использовал IDE Code::Block и в папке CFA есть настроенный проект для него — для билда настроены запуски соответствующих батников типа build/clean прямо из самой среды.
Я пока этим проектом перестал заниматься, но его лицензия свободная типа BSD, за исключением использованных ресурсов. Картинки и звуки из оригинальной игры принадлежат Konami, автор музыки (кавера на оригинальные треки) — Darkman007 (кстати, рекомендую ознакомиться с его творчеством: darkman007.untergrund.net/ ), альтернативный тайлсет создал пользователь Warentino с сайта gamedev.ru. Всё остальное можно курочить как угодно с сохранением информации об авторстве и происхождении.
Кстати, если проект заинтересует, то больше информации о нём можно посмотреть тут: gamedev.ru/flame/forum/?id=227447
Оглавление
Часть 1: Инструментарий, основы, кнопки, таймеры
Часть 2: Пиксельные видеорежимы
Часть 3: Тайловые видеорежимы
Часть 4: Спрайты
Часть 5: Вращающиеся фоны и прозрачность
Часть 6: Прерывания, DMA
Часть 7: Звук (Direct Sound)
Часть 8: Сохранения
11. Сохранения
Достаточно сложная игра немыслима без сохранений и загрузок. В GBA нет какого то единого слота флешки для сохранений, как в Playstation 1/2. Вместо этого картридж с игрой должен нести в себе чип позволяющий сделать сохранение. Таковые чипы бывают разных типов (SRAM, EEPROM и Flash) да еще и разных производителей.
Чипы Flash и EEPROM обладают несколько замысловатым интерфейсом взаимодействия — блочным чтением/записью, необходимостью стирать блок памяти перед его записью и тому подобное. Поэтому для этого введения я выбрал самый простой в программировании тип постоянного ОЗУ — SRAM (static RAM, статическое ОЗУ). Это просто микросхема статического ОЗУ, подпитываемая батарейкой. Static RAM позволяет одной таблеточной батарейкой поддерживать сохранение игры долгие годы. Эмуляторы же просто будут создавать файл с содержимым этого SRAM рядом с образом игры.
Согласно документации no$gba SRAM имеет объём 32Кб (вообще самый большой размер чипа с Flash — 128Кб, но чаще всего он равен 64Кб) и просто маппится на адреса 0xE000000-0xE007FFF. Как запись так и чтение с этих адресов можно производить только байтами (одно это сразу же отметает попытки общения со SRAM посредством DMA). Кроме того документация no$gba настаивает, что чтение из этих адресов должно производится кодом располагающимся в WRAM консоли — чтобы считывание кодов инструкций не «дёргало» адресные линии картриджа. При этом на запись такого ограничения нет. Ограничение это довольно таки серьезное, но во всех примерах, что я видел, этого не делается, а в эмуляторах и на flash-картриджах с реальным железом всё работает и без выполнения этого условия (подтверждена работоспособность на EverDrive GBA X5, при этом перед запуском игры там необходимо в меню уточнить тип чипа сохранений — выбрать вариант «SRAM»). У меня есть подозрение, что оно случайно скопировано из инструкций для Flash-памяти, так как именно там важно то как «дёргаются» адресные линии, хотя это не факт.
Однако просто начать писать и читать байты в адреса 0xE000000-0xE007FFF недостаточно. Дело в том что все остальные виды перезаписываемой памяти в GBA мапятся тоже на эти же адреса, но с другим интерфейсом, а заголовок картриджа не содержит никаких отметок о том какой тип перезаписываемой памяти используется. Игра просто знает сама какой чип находится в картридже и записывает в него данные нужным способом. С настоящим картриджем естественно никаких проблем это не вызывает, а вот нам придётся предпринять еще один шаг.
Дело в том, что эта проблема актуальна и для эмуляторов GBA и их разработчики подметили, что картриджи созданные с использованием официального SDK от Nintendo как правило содержат небольшие строчки помогающие идентифицировать чип сохранений:
EEPROM_Vnnn для EEPROM 512 байт или 8 Кб
SRAM_Vnnn для SRAM 32 Кб
FLASH_Vnnn для FLASH 64 Кб
FLASH512_Vnnn для FLASH 64 Кб (в новых картриджах)
FLASH1M_Vnnn для FLASH 128 Кб
Эти строчки должны располагаться по адресам кратным 4, а их длина тоже должна быть кратна 4 байтам (с заливкой нулями остатка). Официальные утилиты Nintendo как правило вставляют вместо 'nnn' цифры версий, но нам лучше так и оставлять их как 'nnn'.
Поэтому всё что нам остаётся сделать — это вставить в программу static массив из char с текстом «SRAM_Vnnn» и убедиться, что оптимизатор не выкинет его из финального образа.
Итак, всё что нам нужно для реализации нашей программы это добавить к разметке карте памяти в gba_def.h следующие определения:
#define SRAM_START 0x0E000000
#define SRAM_SIZE 32768
#define SRAM_BUFFER ((volatile u8 *) SRAM_START)
После чего мы готовы написать программу сохраняющую состояние в SRAM:
Она будет представлять из себя один экран из черных и белых квадратиков над которым мы можем (DPad-ом) водить курсором. Нажатие кнопки (A) окрашивает клетку под курсором в белый, а (B) — в чёрный цвет. Нажатие START окрашивает все клетки чёрным, нажатие же ® сохраняет карту клеток в SRAM, а (L) — загружает из неё.
11_sram.cpp:
#include "string.h"
#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 ];
};
// Вспомогательные функции по работе с геймпадом
int prev_keys, cur_keys = 0;
void read_keys()
{
prev_keys = cur_keys;
cur_keys = ~REG_KEYS;
};
inline int key_is_down( int key )
{
return cur_keys & key;
};
inline int key_is_pressed( int key )
{
return (cur_keys & key) && !(prev_keys & key);
};
// Карта будущих пикселей для курсора.
// Для простоты - в виде строки.
static const u8 cursor_image[] = {
"00 00"
"010 010"
" 010010 "
" 0110 "
" 0110 "
" 010010 "
"010 010"
"00 00" };
// Символы этой строки превращаются в цвета
// (точнее палитровые индексы) этой функцией:
inline u8 cursor_color( u8 d )
{
if ( d == '0' )
return 167;
if ( d == '1' )
return 255;
return 0;
};
// Хинт для эмуляторов о том, что этот картридж имеет чип SRAM для сохранений!
static const char sram_label[] = { 'S', 'R', 'A', 'M', '_', 'V', 'n', 'n', 'n', 0, 0, 0 };
int cx = 0, cy = 0; // координаты курсора
int main(void)
{
// Делаем вид, что куда то копируем массив sram_label, чтобы оптимизирующий
// компилятор не выкинул его из образа картриджа
memcpy( (void*) ROM_START, sram_label, 12 );
// Инициализируем палитру так, чтобы 8-битный индекс цвета
// совпадал с цветовой маской BBGGGRRR
for ( unsigned int i = 0; i < 256; i++ )
{
BGR_PALETTE[ i ] = RGB( (i & 7) << 2, (i & 56) >> 1, (i & 192) >> 3 );
SPR_PALETTE[ i ] = BGR_PALETTE[ i ];
};
// Очистим видеопамять
memset( (void*) VID_RAM_START, 0, VID_RAM_SIZE );
// Включаем MODE_0 и BG0
REG_DISPCNT = MODE_0 | BG0_ENABLE | SPR_ENABLE;
// Параметры фона BG0
REG_BG0CNT = BG_PRIORITY( 2 ) | BG_TILE_DATA_AT_0000 | BG_COLOR_256 |
BG_TILE_MAP_AT( 16 ) | BG_SIZE_256x256;
// Закрашиваем второй тайл фона белым цветом (первый остаётся черным)
for ( int i = 0; i < 4 * 8; i++ )
{
TILE_DATA[ 32 + i ] = 65535; // все биты в 1 даст нужное
};
// Закрашиваем первый тайл спрайтов курсором
const u8 *data = cursor_image;
for ( int y = 0; y < 8; y++ )
{
for ( int x = 0; x < 4; x++ )
{
// извлекаем цвета двух смежных пикселей из строки cursor_image
u16 w = cursor_color( *data++ );
w = w + (cursor_color( *data++ ) << 8);
SPR_TILES[ x + y * 4 ] = w;
};
};
// Уберем все спрайты за экран, отключим их и придадим
// им минимальные размеры
for ( int i = 0; i < 128; i++ )
{
SPR_BUFFER[ i ].attr0 = ATTR0_Y( -16 ) | ATTR0_DISABLED | ATTR0_SHAPE_SQ;
SPR_BUFFER[ i ].attr1 = ATTR1_X( -16 ) | ATTR1_SQ_8x8;
SPR_BUFFER[ i ].attr2 = ATTR2_PRIORITY( 1 );
};
// Бесконечный цикл
while ( true )
{
// Дождёмся выхода в VBLANK
while ( REG_VCOUNT < 160 );
read_keys();
// Двигаем курсор
if ( key_is_down( KEY_LEFT ) )
{
cx--;
if ( cx < 0 ) cx = 0;
};
if ( key_is_down( KEY_RIGHT ) )
{
cx++;
if ( cx > 29 * 8 ) cx = 29 * 8;
};
if ( key_is_down( KEY_UP ) )
{
cy--;
if ( cy < 0 ) cy = 0;
};
if ( key_is_down( KEY_DOWN ) )
{
cy++;
if ( cy > 19 * 8 ) cy = 19 * 8;
};
// Закраска белым
if ( key_is_down( KEY_A ) )
{
tile( TILE_MAP0, (cx + 4) >> 3, (cy + 4) >> 3 ) = 1;
};
// Закраска черным
if ( key_is_down( KEY_B ) )
{
tile( TILE_MAP0, (cx + 4) >> 3, (cy + 4) >> 3 ) = 0;
};
// Сброс поля в черный цвет
if ( key_is_pressed( KEY_START ) )
{
for ( int y = 0; y < 20; y++ )
{
for ( int x = 0; x < 30; x++ )
{
tile( TILE_MAP0, x, y ) = 0;
};
};
};
// Сохранение - просто записываем информацию байт за байтом
if ( key_is_pressed( KEY_R ) )
{
for ( int y = 0; y < 20; y++ )
{
for ( int x = 0; x < 30; x++ )
{
SRAM_BUFFER[ x + 30 * y ] = tile( TILE_MAP0, x, y );
};
};
};
// Загрузка - просто считываем информацию байт за байтом
if ( key_is_pressed( KEY_L ) )
{
for ( int y = 0; y < 20; y++ )
{
for ( int x = 0; x < 30; x++ )
{
tile( TILE_MAP0, x, y ) = SRAM_BUFFER[ x + 30 * y ];
};
};
};
// Выставляем спрайт курсора
SPR_BUFFER[ 0 ].attr0 = ATTR0_Y( cy ) | ATTR0_MODE_NORMAL |
ATTR0_COLOR_256 | ATTR0_SHAPE_SQ;
SPR_BUFFER[ 0 ].attr1 = ATTR1_X( cx ) | ATTR1_SQ_8x8;
SPR_BUFFER[ 0 ].attr2 = ATTR2_TILE( 0 ) | ATTR2_PRIORITY( 0 );
// Доводим синхронизацию по VLANK до конца.
while ( REG_VCOUNT >= 160 );
};
return 0;
}
Не очень хорошая идея пробрасывать данные напрямую между видеопамятью и постоянной памятью, особенно если последняя выполнена по технологии отличающейся от SRAM — если мы выйдем за временные пределы VBlank, то доступ ЦП к видеопамяти резко замедлится и это может плохо сказаться на таймингах, которые могут быть критичны. Так что по хорошему надо сохранять память только из/в EWRAM или IWRAM. Но в данном случае всё работает.
Ну вот и всё. Сохранение данных — последнее из этого «реактивного» введения, что надо знать для создания полноценной видеоигры для Game Boy Advance. Есть немало вещей, которые остались за кадром, но необходимый минимум здесь дан, а многое из того что осталось без внимания или редко используются или может и вообще не понадобится. В любом случае, если вы владеете английским языком, то можете уже более внимательно и вдумчиво почитать материалы по ссылкам в начале этого введения.
Обратите внимание, что созданный по мере написания этого цикла gba_defs.h как можно сильнее старался соотвествовать использованной литературе, которые в свою очередь пыталась соответствовать официальному SDK от Nintendo, но он не является на 100%-совместимым с теми файлами и библиотеками, которые можно найти в составе DevKitPro. Впрочем, так как он на 99% состоит из define-ов, то особой проблемы в том, чтобы скрещивать накопленные в нём знания с другими заголовками не предвидится.
Contra Force Advance
В качестве сквозного примера для демонстрации разработки игры архив с исходным кодом всех уроков содержит папку CFA с проектом реализации клона игры Contra Force (спираченная версия известна под именем Super Contra 6).
В нём используется компиляция с помощью make, что многое упрощает и делает возможным билд проекта через настраиваемую среду разработки. Я, например, использовал IDE Code::Block и в папке CFA есть настроенный проект для него — для билда настроены запуски соответствующих батников типа build/clean прямо из самой среды.
Я пока этим проектом перестал заниматься, но его лицензия свободная типа BSD, за исключением использованных ресурсов. Картинки и звуки из оригинальной игры принадлежат Konami, автор музыки (кавера на оригинальные треки) — Darkman007 (кстати, рекомендую ознакомиться с его творчеством: darkman007.untergrund.net/ ), альтернативный тайлсет создал пользователь Warentino с сайта gamedev.ru. Всё остальное можно курочить как угодно с сохранением информации об авторстве и происхождении.
Кстати, если проект заинтересует, то больше информации о нём можно посмотреть тут: gamedev.ru/flame/forum/?id=227447
2 комментария
Спасибо!