Разработка игр под NES на C. Глава 24. Приложение 2 — работа с банками памяти

в 20:44, , рубрики: C, cc65, Nes, Nintendo Entertainment System, ненормальное программирование, разработка, разработка игр

Завершающая часть цикла. В этой главе рассмотрим работу с маппером MMC3 на примерах
<<< предыдущая
image
Источник

Раньше мы не использовали переключение банков памяти, но теперь настало время освоить маппер MMC3. Без маппера можно использовать 32 килобайта PRG ROM для кода и 8 килобайт CHR ROM для графики. Маппер позволяет обойти этот барьер.

Будем иметь в виду выпуск нашей игры на реальном картридже. Мануал http://kevtris.org/mappers/mmc3/ утверждает, что у нас есть такие варианты:

  • До 64K PRG, 64K CHR
  • До 512K PRG, 64K CHR
  • До 512K PRG, VRAM
  • До 512K PRG, 256K CHR
  • До 128K PRG, 64K CHR, 8K CHR RAM

Список неполный. Выбираем самый компактный формат, 64/64к. Надо указать это в заголовке образа картриджа, чтобы эмулятор знал об этом. Документация на формат образа доступна в вики: http://wiki.nesdev.com/w/index.php/INES

Заголовок iNES

.byte $4e,$45,$53,$1a
.byte $04  ; = 4 x 0х4000 байт PRG ROM
.byte $08  ; = 8 x 0х2000 байт CHR ROM
.byte $40  ; = маппер №4 - MMC3

Дальше надо прописать банки памяти в .cfg:

Фрагмент nes.cfg

#Адреса банков ROM:
#все по адресу $8000, потому что они будут подставляться туда маппером
PRG0: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG1: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG2: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG3: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG4: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG5: start = $a000, size = $2000, file = %O ,fill = yes, define = yes;
PRG6: start = $c000, size = $2000, file = %O ,fill = yes, define = yes;
PRG7: start = $e000, size = $1ffa, file = %O ,fill = yes, define = yes;

# Вектора прерываний в хвосте ROM
VECTORS: start = $fffa, size = $6, file = %O, fill = yes;

Все банки памяти будут подгружаться по одному и тому же адресу $8000. Исполняемый код будет в последнем неперегружаемом банке, и его можно разместить по любому адресу. Распределение памяти — самое сложное при работе с маппером, тут надо быть аккуратным.

Сегменты надо прописать в конфиге:

nes.cfg

SEGMENTS {
HEADER:   load = HEADER,         type = ro;
CODE0:    load = PRG0,           type = ro,  define = yes;
CODE1:    load = PRG1,           type = ro,  define = yes;
CODE2:    load = PRG2,           type = ro,  define = yes;
CODE3:    load = PRG3,           type = ro,  define = yes;
CODE4:    load = PRG4,           type = ro,  define = yes;
CODE5:    load = PRG5,           type = ro,  define = yes;
CODE6:    load = PRG6,           type = ro,  define = yes;
STARTUP:  load = PRG7,           type = ro,  define = yes;
CODE:     load = PRG7,           type = ro,  define = yes;
VECTORS:  load = VECTORS,        type = ro;
CHARS:    load = CHR,            type = rw;

BSS:      load = RAM,            type = bss, define = yes;
HEAP:     load = RAM,            type = bss, optional = yes;
ZEROPAGE: load = ZP,             type = zp;
#OAM:   load = OAM1,    type = bss, define = yes;
}

Сегмент OAM в этом примере не используется.

А теперь запишем что-нибудь заметное в каждый банк и посмотрим, как оно разместится в ROM-файле. Для примера возьмем слова Bank0, Bank1 и так далее. Эти слова будут выводиться и на экран, переключение банков кнопкой Старт.

Размещение переменной в нужном банке делается через директиву PRAGMA:

lesson19.c

#pragma rodata-name (“CODE0”)
#pragma code-name (“CODE0”)
const unsigned char TEXT1[]={
“Bank0”};

#pragma rodata-name (“CODE1”)
#pragma code-name (“CODE1”)
const unsigned char TEXT2[]={
“Bank1”};

#pragma rodata-name (“CODE2”)
#pragma code-name (“CODE2”)
const unsigned char TEXT3[]={
“Bank2”};

При нажатии Старт перключается банк памяти по адресам $8000-$9FFF, и первые 5 байт показываются на экране

Вывод текста из банка

void Draw_Bank_Num(void){ // функция вывода на экран
PPU_ADDRESS = 0x20;
PPU_ADDRESS = 0xa6;
for (index = 0;index < 5;++index){
PPU_DATA = TEXT1[index];
}
PPU_ADDRESS = 0;
PPU_ADDRESS = 0;
}

TEXT1 определяется на этапе компиляции и при старте консоли указывает в нулевой банк. При смене банка этот адрес останется неизменным, и в любом случае будет отображен текст из адресов $8000-8004. Банки переключаются вот так:

Переключение банка

if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){
++PRGbank;
if (PRGbank > 7) PRGbank = 0;
*((unsigned char*)0x8000) = 6; // переключить банк PRG по адресу $8000
*((unsigned char*)0x8001) = PRGbank;
Draw_Bank_Num(); //вывод текста из нового банка

Адрес $8000 принадлежит ROM, но запись туда перехватывается маппером. Дальше указывается номер банка для подгрузки. Подробности как обычно в вики:
http://wiki.nesdev.com/w/index.php/MMC3

Немного путаницы вносит случайное равенство адресов начала банка и служебного регистра маппера. Мы можем перенести банк в адреса $A000-$BFFF:

*((unsigned char*)0x8000) = 7; // Адрес начала банка PRG - $A000
*((unsigned char*)0x8001) = which_PRG_bank;

Но регистры управления все равно остаются по адресам $8000 и $8001.

Я также добавил код инициализации в начало main(). Этот момент не документирован, но судя по всему, после RESET гарантирована правильная загрузка только последнего банка, по адресам $E000-$FFFF. Весь наш код инициализации должен располагаться только там.

Такая схема работы с банками памяти (когда их начало фиксировано по одному адресу) весьма неудобна. Обычно в начале каждого банка хранится массив с указателями на структуры данных и функции. Тогда можно переходить в них косвенными переходами, или более быстрым фокусом со стеком: http://wiki.nesdev.com/w/index.php/RTS_Trick. Там Ассемблер, но оно того стоит.

В любом случае, я хочу добавить прокрутку фона с параллаксом. Для этого надо каждые 4 кадра переключать банк CHR ROM в область памяти PPU — тайлы будут подхватываться оттуда. MMC3 разбивает CHR ROM на банки по 64 тайла, это 0x400 байт. Будем делать анимированный водопад, в каждом наборе тайлов они будут сдвинуты на 1 пиксель — при смене банков получится анимация.
image

Ссылка на исходный код, следующий кадр показывается по кнопке Старт:
Дропбокс
Гитхаб

Еще MMC3 умеет считать строки, выведенные на телевизор. Обычно это делается через нулевой спрайт, но он работает один раз за кадр — иногда нужно больше. Для имитации параллакса фона будем менять положение прокрутки каждые 20 строк. MMC3 будет вызывать прерывания в нужные моменты, и в его обработчике будет устанавливаться прокрутка в нужное положение. Обработчик написан на ассемблере, потому что при работе с С можно случайно повредить стек при вызове функции.
http://www.cc65.org/faq.php#IntHandlers

При старте приставки прерывания выключены, их надо включить в main().

asm (“cli”); // Включить прерывания

Указатели в векторе прерываний в конце файла reset.s должны показывать на правильные обработчики. Теперь можно настроить подсчет строк:

*((unsigned char*)0xe000) = 1; // Выключить MMC3 IRQ
*((unsigned char*)0xc000) = 20; // Вызвать прерывание через 20 строк
*((unsigned char*)0xc001) = 20;
*((unsigned char*)0xe001) = 1; // Снова включить MMC3 IRQ

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

Еще очень желательно дергать горизонтальную прокрутку во время очень короткого периода H-blank — время хода луча на начало строки. Если это не учитывать, будет небольшое искажение изображения. Если знать куда смотреть, оно заметно во многих играх.

Прерывание MMC3 срабатывает ровно в H-blank, но его длительности не хватает на переход в обработчик. Так что я поставил там простой цикл, который ждет примерно 100 тактов до следующего H-blank. Этот момент может неточно обрабатываться некоторыми эмуляторами. Реальные игры не ждут следующей строки и делают сдвиг прокрутки в области с однотонной заливкой. После сдвига прокрутки ждем следующие 20 строк, и повторяем все снова.

Если хотите увидеть это своими глазами, исправьте ограничение цикла в обработчике. Сдвиг буквально 1 повторение будет видно — H-blank действительно настолько короткий.

image
Старт все еще переключает банки, но тут это не заметно.

Дропбокс
Гитхаб

Если лень возиться с перекомпиляцией, то вот гифка:
image
Цикл тайминга укорочен на 1 оборот — прокрутка меняется за несколько пикселей до конца строки. Искажение видно в правом конце нижней строки каждого горизонтального слоя. Оно меняется каждый кадр, так что на экране все пляшет. Если же прерывание сработает посреди строки, то будет совсем плохо.

Такая работа с прокруткой позволяет реализовать эффект параллакса. Запрос 'NES parallax scrolling' на Ютубе даст наглядные примеры. Опять-таки, обратите внимание, что в большинстве игр слои фона разделены однотонной заливкой.

Автор: Вадим Марков

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js