SGDK. Оптимизируем спрайты.

Предисловие

В SGDK, очень просто добавить спрайт на сцену, для этого нужно использовать функцию SPR_addSprite, которая копирует тайлсет спрайта в VRAM и использует его. Это очень удобная функция, однако, мы говорим о консоли 1988 года, мы не можем позволить себе роскоши каждому спрайту давать уникальный тайлсет. Поэтому, в этой статье, я расскажу как оптимизировать спрайты.

Способы проигрывания анимации на Sega Genesis.

Каждый спрайт использует тайлы из VRAM. Есть 2 способа получения тайлов, у каждого есть свои плюсы и минусы:

  • Перерисовать используемые тайлы в VRAM. Хуже производительность, меньше потребление VRAM.
  • Сменить индекс тайла VRAM на другой. Лучше производительность, больше потребление VRAM.

В данной статье мы рассмотрим оба способа.

Анимация сменой индекса тайла.

Скачайте проект с github. Далее рассмотрим код:

#include <genesis.h>
#include "resources.h"


#define NUM_ENEMY       100

u16** sprTileIndexes[NUM_ENEMY]; //Указатель под функцию SPR_loadAllFrames
Sprite* enemies[NUM_ENEMY]; //Массив спрайтов

// forward
static void frameChanged(Sprite* sprite);

int main()
{
	u16 vramIndex = TILE_USER_INDEX; //Индекс первого незанятого тайла
	u16 numTile; //Используется функцией SPR_loadAllFrames для хранения количество тайлов тайлсета спрайта
	SPR_init();
	PAL_setPalette(PAL0, img.palette->data, CPU);
	VDP_drawImageEx(BG_A, &img, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, vramIndex), 0, 0, FALSE, FALSE);
        //Картинка заняла тайлы, сдвигаем ближайший незанятый тайл на количество тайлов картинки
	vramIndex += img.tileset->numTile;
	PAL_setPalette(PAL3, spr_sonic.palette->data, CPU);

        //загружаем в VRAM, полный тайлсет спрайта
	sprTileIndexes[0] = SPR_loadAllFrames(&spr_sonic, vramIndex, &numTile);
	for(u16 i=0;i<7;i++){
		for(u16 j=0;j<4;j++){
			const u16 enemyIndex = (i*4)+j;
                        //Добавляем спрайт на сцену
			enemies[enemyIndex] = SPR_addSprite(&spr_sonic, i*40, j*40, TILE_ATTR(PAL3, 0, FALSE, FALSE));
                        //Если не удалось добавить спрайт, то завершаем цикл
			if(enemies[enemyIndex] == NULL){
				break;
			}
                        //Отключаем функцию автоматической загрузки тайлов спрайта в VRAM
			SPR_setAutoTileUpload(enemies[enemyIndex], FALSE);
                        //Записываем индекс спрайта в data (нужно для функции смены кадра спрайта)
			enemies[enemyIndex]->data = 0;
			
                        //Меняем спрайту функцию смены кадра на frameChanged
			SPR_setFrameChangeCallback(enemies[enemyIndex], &frameChanged);
			SPR_setAnim(enemies[enemyIndex], 2);
			SPR_update();
			SYS_doVBlankProcess();
		}
	}

	while(1){
		VDP_showFPS(FALSE);  
		SPR_update();
		SYS_doVBlankProcess();
	}
    return (0);
}

static void frameChanged(Sprite* sprite)
{
    // Ранее мы записали в sprite->data индекс спрайта
    u16 enemyIndex = sprite->data;
    // Используем индекс спрайта для получения индекса тайла в VRAM текузего кадра спрайта
    u16 tileIndex = sprTileIndexes[enemyIndex][sprite->animInd][sprite->frameInd];
    // Меняем кадр спрайта
    SPR_setVRAMTileIndex(sprite, tileIndex);
}

В этом коде есть новая функция SPR_loadAllFrames, которая загружает полный тайлсет спрайта (включая все анимации) в VRAM. У неё следующий синтаксис:

SPR_loadAllFrames(данные_спрайта, индекс_vram, &вернуть_количество_тайлов);
  • данные_спрайта — указатель на SpriteDefinition, откуда будут браться тайлы
  • индекс_vram — номер тайла в VRAM, куда будут копироваться тайлы спрайта.
  • &вернуть_количество_тайлов — указатель на переменную куда вернется количество скопированных тайлов

Данный способ — дороже в потреблении тайлов VRAM, но быстрее в производительности. Т.к. при анимации спрайта, мы не меняем тайлы в VRAM, мы меняем индекс тайла, что намного дешевле.

Остальное я объяснил в комментариях. В этом примере, все тайлы спрайта находятся в VRAM, и меняется только индекс тайла.

А теперь сравним автоматическую аллокацию с нашей ручной аллокацией.

Автоматическая аллокация VS ручная.

При ручной аллокации, мы быстро уперлись в лимит по спрайтам (80 спрайтов), при этом, все спрайты используют один и тот же тайлсет.

При автоматической аллокации, уперлись в лимит по VRAM, пришлось уменьшить количество спрайтов чтобы эмуль BlastEm не вылетел.

Также, ручная аллокация сильно увеличила производительность. В примере получили 60 FPS, скорость анимации спрайта — максимальная.

В анимации, я вызвал функцию получения FPS дважды из-за этого число FPS умножилось на 2.

Тогда как, при автоматической аллокации получили ~35 FPS, при этом, половина спрайтов, вообще не двигаются.

Можно увеличить FPS, за счет уменьшения сжатия спрайта

Но это не исправит сломанную анимацию половины спрайтов.

Также, мы можем убрать автоматическую аллокацию, и использовать одни и те же тайлсеты среди нескольких спрайтов.

Используем 1 тайлсет в нескольких спрайтах.

Начиная с SGDK 1.80, Stef переработал спрайтовый движок, сделав этот способ нерабочим. Однако, он оставил способ вернуть прошлый спрайтовый движок. Для этого нужно:

  • Открыть SGDK/inc/config.h, в папке куда устанавливали SGDK.
  • Сменить значение LEGACY_SPRITE_ENGINE  на 1, т.е. #define LEGACY_SPRITE_ENGINE    1
  • Пересобрать SGDK, запустив батник SGDK/build_lib.bat

Спасибо werton‘у за обнаружение способа возвращения спрайтового движка.

Перекопируйте второй пример.

#include <genesis.h>
#include "resources.h"


#define NUM_ENEMY       100

u16** sprTileIndexes[NUM_ENEMY]; //Указатель под функцию SPR_loadAllFrames
Sprite* enemies[NUM_ENEMY]; //Массив спрайтов

// forward
static void frameChanged(Sprite* sprite);

int main()
{
	u16 vramIndex = TILE_USER_INDEX; //Индекс первого незанятого тайла
	u16 numTile; //Используется функцией SPR_loadAllFrames для хранения количество тайлов тайлсета спрайта
	SPR_init();
	PAL_setPalette(PAL0, img.palette->data, CPU);
	VDP_drawImageEx(BG_A, &img, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, vramIndex), 0, 0, FALSE, FALSE);
        //Картинка заняла тайлы, сдвигаем ближайший незанятый тайл на количество тайлов картинки
	vramIndex += img.tileset->numTile;
	PAL_setPalette(PAL3, spr_sonic.palette->data, CPU);
        u16 curSpriteIndex = 1; //Индекс текущего спрайта (1-79, 0 - зарезервирован)
	enemies[0] = SPR_addSpriteEx(&spr_sonic, 100, 100, TILE_ATTR_FULL(PAL3, 0, FALSE, FALSE, vramIndex), curSpriteIndex, SPR_FLAG_AUTO_TILE_UPLOAD | SPR_FLAG_AUTO_SPRITE_ALLOC );
        //SPR_FLAG_AUTO_SPRITE_ALLOC - аллоцируем спрайт
        //SPR_FLAG_AUTO_TILE_UPLOAD - разрешаем спрайту писать в VRAM
	curSpriteIndex++;
	SPR_setAnim(enemies[0], 2);
	
	for(u16 i=0;i<5;i++){
		for(u16 j=0;j<6;j++){
			const u16 enemyIndex = (i*6)+j;
			enemies[enemyIndex] = SPR_addSpriteEx(&spr_sonic, i*30, j*30, TILE_ATTR_FULL(PAL3, 0, FALSE, FALSE, vramIndex), curSpriteIndex, SPR_FLAG_AUTO_SPRITE_ALLOC);
			if(enemies[enemyIndex] == NULL){
				break;
			}
			SPR_setAnim(enemies[enemyIndex], 2);
			curSpriteIndex++;
		}
	}
        while(1){
		VDP_showFPS(FALSE);  
		SPR_update();
		SYS_doVBlankProcess();
	}
    return (0);
}

У функции SPR_addSpriteEx следующий синтаксис:

SPR_addSpriteEx(spriteDefinition, x, y, аттрибут, индекс_спрайта, флаг)
  • spriteDefinition — указатель на SpriteDefinition
  • x,y — координаты спрайта
  • аттрибут — аттрибут спрайт, задается макросом TILE_ATTR или TILE_ATTR_FULL.
  • флаг — задает функции спрайта

Т.е. сначала, я аллоцировал спрайт и тайлы в VRAM под него:

enemies[0] = SPR_addSpriteEx(&spr_sonic, 100, 100, TILE_ATTR_FULL(PAL3, 0, FALSE, FALSE, vramIndex), curSpriteIndex, SPR_FLAG_AUTO_TILE_UPLOAD | SPR_FLAG_AUTO_SPRITE_ALLOC );

Аллоцировал тайлы с помощью флага SPR_FLAG_AUTO_TILE_UPLOAD и спрайт с помощью SPR_FLAG_AUTO_SPRITE_ALLOC, а также, в аттрибуте задал индекс в VRAM (vramIndex).

Далее, в цикле, добавил на сцену больше спрайтов, только без флага SPR_FLAG_AUTO_TILE_UPLOAD, т.к. я не хочу чтобы все спрайты писали в VRAM, я хочу чтобы только 1 это делал. Если этого не сделать, то получим сломанную анимацию.

enemies[enemyIndex] = SPR_addSpriteEx(&spr_sonic, i*30, j*30, TILE_ATTR_FULL(PAL3, 0, FALSE, FALSE, vramIndex), curSpriteIndex, SPR_FLAG_AUTO_SPRITE_ALLOC);

Остальное я объяснил в комментариях. В этом примере, спрайт пишет в VRAM изменяя тайлы анимации. В итоге получили следующее.

А есть ли вариант попроще?

Разумеется, в SGDK_PlatformerStudio уже реализована пред-загрузка тайлов спрайта в VRAM. Делается это добавлением вашего SpriteDefinition в меню SpriteOptimizations.

Также, там есть рычаг «If on level«, который, если включен, аллоцирует тайлы спрайта в VRAM, только если entity использующий этот SpriteDefinition есть на уровне.

Заключение

Как видите автоматический способ добавления спрайтов не очень подходит для ретро-консоли. Напротив, ручная аллокация и установка VRAM индексов позволяет значительно увеличить производительность.

Итоговый результат.

Пожалуйста отключи блокировщик рекламы, или внеси сайт в белый список!

Please disable your adblocker or whitelist this site!