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

Сохранения…



Оглавление


Часть 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 комментария

avatar
Крутой цикл, полный.
Спасибо!
  • VBI
  • 0
avatar
Почти под ts-conf )
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.