Предисловие
В 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, за счет уменьшения сжатия спрайта
SPRITE spr_sonic "sonic.png" 6 6 NONE 1
Но это не исправит сломанную анимацию половины спрайтов.
Также, мы можем убрать автоматическую аллокацию, и использовать одни и те же тайлсеты среди нескольких спрайтов.
Используем 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 индексов позволяет значительно увеличить производительность.