SGDK. Перемещаемся по карте.

Предисловие.

Перед разбором скроллинга карты, разберемся, каким образом карта рисуется в Sega Genesis.

Каким образом Sega Genesis рисует карту?

Откройте с помощью эмулятора Gens KMod любого соника. Зайдите на первый уровень.

Сейчас, мы узнаем, что происходит на слое BG_A, для этого, зайдите в CPU->Debug->Plane Explorer

Побегайте немного по карте.

И что мы видим:

  1. Размер слоя равен 64×32 тайла, что выше области видимости экрана.
  2. Тайлы карты достраиваются на лету, во время движения камеры.
  3. Тайлы достраиваются за пределами видимости камеры.
  4. Перемещение камеры, осуществляется за счет скролла плейна (слоя)

Когда Sega Genesis, скроллит карту она, по факту, рисует недостающие тайлы за границей экрана, и скроллит плейн (слой). И получается, что область видимости экрана, бегает по плейну.

Пример данного скролла, доступен в SGDK/sample/sonic.

Не волнуйтесь, нам не потребуется соотносить координаты игрока, с координатами тайловой сетки, строить недостающие тайлы, а затем, скроллить плейн. Все это сделал за нас Stephane-D (создатель SGDK), и вывел в функцию MAP_scrollTo.

Ладно, перейдем к проекту.

Подгружаем ресурсы.

Скачайте карту уровня с MEGA.

И перекиньте её в папку res, вашего проекта. Палитру данной картинки, я ограничил до 16-ти цветов. О том, как это сделать, я говорил здесь.

Фрагмент карты

Если хотите создать свое изображение карты уровня то читайте SGDK. Программы для рисования карты из тайлов

В resources.res, напишите следующие строки.

PALETTE palette_all "bg.png"
TILESET bga_tileset "bg.png" BEST ALL
MAP bga_map "bg.png" bga_tileset BEST 0

Разберем их.

PALETTE palette_all "bg.png"

Здесь, происходит импорт палитры из картинки bg.png, название палитры выбрали pallete_all.

TILESET bga_tileset "bg.png" BEST ALL

Создаем набор тайлов из картинки. Из этих тайлов будет состоять карта. Разберем синтаксис.

TILESET имя картинка сжатие оптимизация
  • имя — задает имя, по которому будем обращаться к тайлсету.
  • картинка — путь до изображения (png или bmp)
  • сжатие — режим сжатия (BEST/NONE/APLIB/FAST)
  • оптимизация — устанавливает уровень оптимизации (NONE/ALL/DUPLICATE)

Дальше идет карта.

MAP bga_map "bg.png" bga_tileset BEST 0

Которая, задаётся по следующему синтаксису.

MAP имя картинка тайл_сет сжатие mapbase
  • Тайл сет — это набор тайлов данной карты (тайлсет, можно использовать в нескольких картах)
  • mapbase — Define the base tilemap value, useful to set a default priority, palette and base tile index offset. Using a base tile index offset (static tile allocation) allow to use faster MAP decoding function internally. (Тут я не понял, поэтому оставил объяснение из recomp.txt)

Остальные аргументы, уже разбирали. Перейдем к написанию кода.

Пишем код.

Сначала, импортируем нужные библиотеки, и создадим переменные.

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

Map *bga; 

s16 camPosX = -1;
s16 camPosY = -1;

fix32 posX = 0;
fix32 posY = 0;

bool paused = FALSE;
  • В bga будем хранить карту.
  • camPosX и camPosY — хранят координаты камеры.
  • posX и posY — хранят координаты игрока.
  • Переменную paused создал на будущее. Она, будет отвечать за паузу.

У posX и posY, появился новый тип fix32.

Про фальшивые дробные числа.

Данный тип, хранит фальшивое число число с плавающей точкой, который, в отличии от реального числа (float), нужно преобразовывать через функции. Как в ACS (если вы изучаете ZDoom).

У вас может возникнуть вопрос, зачем использывать фальшивые дробные числа, когда можно использовать float? Ответ кроется, в оптимизации. Сеге не нужна такая точность, какую предлагает float. Слишком дорого тратить ресурсы, это вам не компьютер, с бесконечным уровнем абстракций, и абсурдно неоптимизированными технологиями (привет electron).

Вернемся к коду.

void setCameraPosition(s16 x, s16 y)
{
    if ((x != camPosX) || (y != camPosY))
    {
        camPosX = x;
        camPosY = y;
        MAP_scrollTo(bga, x, y);
    }
}

setCameraPosition — скроллит карту, но только, если координаты изменились. Что обеспечивает дополнительную оптимизацию.

За скролл карты, отвечает Map_scrollTo.

MAP_scrollTo(плейн, x, y);

Далее, реализуем перемещение камеры (скролл карты).

void handleInput()
{
    u16 value = JOY_readJoypad(JOY_1);

    if (!paused)
    {
        if (value & BUTTON_RIGHT)
        {
            posX += FIX32(5);
	    setCameraPosition(fix32ToInt(posX),fix32ToInt(posY)-140);
        }
        else if (value & BUTTON_LEFT)
        {
            posX -= FIX32(5);
	    setCameraPosition(fix32ToInt(posX),fix32ToInt(posY)-140);
        }

        if (value & BUTTON_UP)
        {
            posY -= FIX32(5);
	    setCameraPosition(fix32ToInt(posX),fix32ToInt(posY)-140);
        }
        else if (value & BUTTON_DOWN)
        {
            posY += FIX32(5);
	    setCameraPosition(fix32ToInt(posX),fix32ToInt(posY)-140);
        }
    }
}

Здесь, мы меняем координаты камеры в зависимости от нажатой кнопки.

  • FIX32 — преобразует целое число в фальшивое 32-х битное дробное число.
  • fix32ToInt — переводит фальшивое 32-х битное дробное число в целое число.
int main()
{
    u16 ind;
    ind = TILE_USERINDEX;
	
    VDP_loadTileSet(&bga_tileset, ind, DMA);
    bga = MAP_create(&bga_map, BG_A, TILE_ATTR_FULL(0, FALSE, FALSE, FALSE, TILE_USERINDEX));
	
    setCameraPosition(fix32ToInt(posX),fix32ToInt(posY)-140);
    SYS_doVBlankProcess();
	
    VDP_setPaletteColors(0, palette_all.data, 16*2);
	
    while(1)
    {
	handleInput();
        SYS_doVBlankProcess();
    }
	
    return (0);
}

Функцию main, разберем подробнее.

u16 ind;
ind = TILE_USERINDEX;

TILE_USERINDEX — задает индекс начального тайла (понадобится при записи тайлсета в VRAM). Данный индекс записал в переменную ind.

VDP_loadTileSet(&bga_tileset, ind, DMA);

VDP_loadTileSet — записывает тайлсет в VRAM. Принимает:

  • тайлсет — указатель на тайлсет.
  • тайл_индекс — индекс, начиная с которого, будем писать тайлы в VRAM.
  • способ передачи — доступны значения (CPU, DMA, DMA_QUEUE, DMA_QUEUE_COPY)
bga = MAP_create(&bga_map, BG_A, TILE_ATTR_FULL(0, FALSE, FALSE, FALSE, TILE_USERINDEX));

MAP_create — создает карту &bga_map на нужном плейне BG_A, используя тайлы TILE_ATTR_FULL, по следующему шаблону.

TILE_ATTR_FULL(палитра, приоритет, перевернуть_по_вертикали, перевернуть_по_горизонтали, индекс_тайла)
  • Палитра — Указываем палитру которую будет использовать тайлы (в нашем случае спрайт)
  • Приоритет — задает приоритет спрайта (тайла), т.е. спрайт с меньшим числом, будет перекрывать спрайт с большим.
  • индекс_тайла — задает индекс начального тайла в VRAM.
  • остальное, понятно из названия.

Кстати, почти то-же самое мы обсуждали в SGDK. Двигаем спрайт по экрану.

setCameraPosition(fix32ToInt(posX),fix32ToInt(posY)-140);
SYS_doVBlankProcess();

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

VDP_setPaletteColors(0, palette_all.data, 16*2);

Заполнили CRAM, нашей палитрой, взятой из bg.png. Здесь используется VDP_setPalleteColors, но он ничем не отличается от PAL_setPalleteColors, который я уже разбирал в SGDK. Переходы Fade-in и Fade-out.

Палитру изображения можно посмотреть в infranView. Для этого, откройте изображение и выберите Image->Palette->Edit Palette…

Палитра карты
 while(1)
{
handleInput();
SYS_doVBlankProcess();
}

Ну и, запускаем бесконечный цикл, проверяющий нажатие кнопок.

В итоге, получилось реализовать движение по карте.

Дополнение от 29.06.2021.

Как выяснилось, MAP_scrollTo начинает артифачить (рисовать мусорные тайлы), на высоких координатах x или y.

Что-бы пофиксить данную проблему, необходимо скачать вверсию SGDK выше чем 1.62. Но если такая вверсия не вышла (а она не вышла на момент написания статьи), то скачивайте zip файл SGDK, из официального репозитория.

Ответ Stephane-D по данному вопросу

И спасибо Stephane-D за решение проблемы.

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