Пришло время для того что бы сделать в игре статус бар, где можно было бы отображать жизни, удары, инвентарь. Для разработки игровых механик на подобие powerup'ов, а так же механик рпг, к примеру что бы пройти на следующую часть уровня надо разбить некое препятствие условным молотком. Есть два основных способа решения данной задачи:
Sprite 0 hit - смысл алгоритма спрайт зеро хит, то что при появление первого непрозрачного пикселя спрайта который перекрывает не прозрачный пиксель фона, нес в статус флаге CPU устанавливает бит флага нулевого спрайта. За этот момент можно зацепится и к примеру до установки данного флага прокрутку устанавливать на 0 а после установки уже прокручивать фон. Способ довольно простой но для простых статус баров с верху. При этом есть объективные минусы в том что мы должны ждать наступления спрайта 0 по этому это должна быть небольшая область с верху. Типичный пример реализации данного метода это супер марио брос, у него в меню есть монетка нижняя часть которой перекрыта спрайтом.
Генерировать событие IRQ на определенной линии отрисовки экрана - такое прерывание может генерировать чуть ли не самый используемый маппер MMC3, данный способ более прямой чем спрайт зеро хит по этому для меня он стал более приемлемым разберем подробнее чуть ниже его.
Отличия MMC1 от MMC3:
MMC1 - переключает страницы памяти PRG полностью и CHR страницы полностью по 4 кб.
MMC3 - переключает банки и страницы PRG а CHR условно говоря собирает из нескольких частей, что более оптимально потому как можно использовать разные части графики в разных готовый наборах pattern 0 и pattern 1
MMC1 - управляется последовательным портом надо по сути сделать 8 операций записи битов 0 или 1 что бы управлять последним
MMC3 - управляется записью в два регистра $8000 и $8001 байта указывающего на действие нужное нам
MMC1 - не генерирует IRQ прерывания
MMC3 - генерирует IRQ прерывание на событие hBlank
MMC1 и MMC3 имеют различные маппинги памяти.
Инициализация MMC3 и работа с ним
В первую очередь нам необходимо переделать нашу конфигурацию линкера для того что бы ром собрался корректна. Пока конфигурацию для MMC3 выбрал ту же самую что и для MMC1 - 128кб PRG памяти и 128кб CHR. У меня получилась следующая конфигурация:
Пример конфигурации
MEMORY { HEADER: start=$00, size=$10, fill=yes, fillval=$00; ZEROPAGE: start=$10, size=$ff; STACK: start=$0100, size=$0100; OAMBUFFER: start=$0200, size=$0100; RAM: start=$0300, size=$0500; ROM_0: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D0; ROM_1: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D1; ROM_2: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D2; ROM_3: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D3; ROM_4: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D4; ROM_5: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_6: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_7: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_8: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_9: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_10: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_11: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_12: start = $8000, size = $2000, type = ro, file = %O, fill=yes, fillval = $D5; ROM_H: start = $A000, size = $4000, type = ro, file = %O, fill=yes, fillval = $CC; PRG_FIXED: start = $E000, size = $2000, type = ro, file = %O, fill = yes, fillval = $FF; CHR: start=$1000, size=$20000; } SEGMENTS { HEADER: load=HEADER, type=ro, align=$10; ZEROPAGE: load=ZEROPAGE, type=zp; STACK: load=STACK, type=bss, optional=yes; OAM: load=OAMBUFFER, type=bss, optional=yes; BSS: load=RAM, type=bss, optional=yes; DMC: load=ROM_H, type=ro, align=64, optional=yes; CODE_1: load=ROM_0, type=ro, align=$0100; CODE_2: load=ROM_1, type=ro, align=$0100; CODE_3: load=ROM_2, type=ro, align=$0100; CODE_4: load=ROM_3, type=ro, align=$0100; CODE_5: load=ROM_4, type=ro, align=$0100; CODE_6: load=ROM_5, type=ro, align=$0100; CODE_7: load=ROM_6, type=ro, align=$0100; CODE_8: load=ROM_7, type=ro, align=$0100; CODE_9: load=ROM_8, type=ro, align=$0100; CODE_10: load=ROM_9, type=ro, align=$0100; CODE_11: load=ROM_10, type=ro, align=$0100; CODE_12: load=ROM_11, type=ro, align=$0100; CODE_13: load=ROM_12, type=ro, align=$0100; CODE: load=PRG_FIXED, type=ro, align=$0100; RODATA: load=ROM_12, type=ro, align=$0100; VECTORS: load=PRG_FIXED, type=ro, start=$FFFA; CHR: load=CHR, type=ro, align=16, optional=yes; }
Самое важное в конфигурации то что код инициализации маппера а так же векторы прерывания должны располагаться в фиксированной области памяти. Фиксированный банк памяти находиться по адресам $E000 - $EFFF. CHR просто выделил полностью память под график в одной секции куда в будущем сможем добавить все нужные нам файлы без деления на 2кб, 2кб, 1кб, 1кб и так далее особенность хранения chr в маппере MMC3. Для начала я просто использовал старые CHR еще не дорабатывал аспект разбиения и составления данных таблицы паттернов.
Для инициализации маппера, необходимо инициализировать регистры и загрузить банки памяти, на самом деле способ подсмотрел в интернете, и данные процедуры были copy-past'ом перенесены в проект привожу пример кода:
Код инициализации MMC3
.proc loadBanks LDX #$08 ; Start of Page to load Add 8 Hex per CHR .. CHR 3 = $10 or 16 LDA #$80 ; Starting Address for $8000 0,1,2,3,4,5,6 LDY #$00 ; Loop Counter LoadPPU2k: ; load two sets 2x2k to make first 4k (BACKGROUND) STA $8000 ; Bank Selection with Inversion STX $8001 ; Selection of Bank INY INX ; Increase X x 2 as 2k INX CLC ADC #$01 CPY #$02 ; For loop BNE LoadPPU2k LoadPPU1k: ; load 4 * 1k sets to make FORGROUND 4k STA $8000 ; Bank Selection with Inversion STX $8001 ; Selection of Bank INY INX ; Increase X * x as 1k CLC ADC #$01 CPY #$06 ; For loop BNE LoadPPU1k RTS .endproc .proc initMMC3 LDX #$00 LDA #$00 STA $E000 ; IRQ disable STA $A000 ; mirroring 0 Vertical; 1 Horizontal : STX $8000 ; select register LDA mmc3Register, X STA $8001 ; initialize register INX CPX #8 ; Compare 8 to X BCC :- ; Branch not Equal ;PRG ROM Selections LDY #02 ; Starting Banks Change This from 0 2 4 6 ETC to change Starting Color Startup $00 LDA #6 ; $8000 Selection Bank = 6 (NOTE: Not HEX) STA $8000 STY $8001 ; Select Bank LOW LDA #7 ; $A000 Selection Bank = 6 (NOTE: Not HEX) STA $8000 INY STY $8001 ; Select Bank HIGH rts .endproc
Просто вызываем этот код в процедуре reset и мы готовы работать с маппером.
Модифицируем процедуры переключения банков программной памяти:
.proc setPrgBank LDA #%00000110 STA $8000 STX $8001 RTS .endproc
Для того что бы переключить банк памяти, необходимо просто загрузить в X номер банка и после вызвать процедуру, к примеру так:
LDX #$02 JSR setPrgBank
Процедуру переключения графических банков и страниц модифицируем следующим образом
.proc switchChr STA $8000 STX $8001 RTS .endproc
Для переключения CHR необходимо загрузить в A номер банка памяти а в X номер страницы. Об этом поговорим в следующих статьях.
Так же меняем методы зеркалирования, в MMC3 это делается довольно просто записью в порт $A000: либо #$00 - Вертикальное зеркалирование, либо #$01 - Горизонтальное зеркалированние.
Код смены зеркалириования
.proc setVerticalMirror LDA #$00 STA $A000 RTS .endproc .proc setHorizontalMirror LDA $01 STA $A000 RTS .endproc
После всех манипуляций наш старый код должен запуститься, в теории конечно, у меня далеко не с первого раза удалось корректно пере-собрать мой проект. Были проблемы из за банальной не внимательности не туда загружалась номер страницы, код был в другой совсем области, где то RODATA не подгружалась, а где то неправильно вызывалась функция. Очень сильно смущало что код работает но графики нет, решилось все банально корректным переключением банка памяти.
Прерывание IRQ
В первую очередь в процедуре RESET, нам необходимо отключить стандартное прерывание IRQ которое генерируются самой платформой NES дабы не обработка алгоритма не происходила повторно. Для этого надо записать #$40 в порт $4017
LDA #$40 STA $4017 CLI
С помощью CLI мы включаем прерывания IRQ.
Далее, в начале вектора RESET, я расположил код инициализации точки срабатывания прерывания, опять же инициализация это образно сказано с моей стороны для простого понимания материала. Приведу код данной "инициализации":
LDA #$C0 ; 192 линия STA $E000 ; отключаем прерывание STA $C000 ; записываем счетчик строк STA $C001 ; еще один счетчик STA $E000 ; еще раз отключаем прерывание что бы зафиксировать значение счетчика STA $E001 ; включаем прерывание
Немного распишу порты выше:
E000 - запись любого значения в этот порт отключает прерывание
E001 - запись любого значения включает прерывание
C000 - счетчик обратного отсчета линий развертки, при прохождение каждой лини уменьшается на 1 и при достижение 0 будет сгенерировано прерывание IRQ
С001 - фиксирует значение счетчика (на самом деле на многих сайтах по английски звучит как IRQ counter latch что я перевожу как некое запирание счетчика)
Переходим в вектор IRQ
.proc irq_isr PHA ; а в стэк TXA ; x -> a перенос PHA TYA ; y -> a перенос PHA LDA #$00 STA $E000 ; отключаем прерывание IRQ STA $2005 ; фиксируем прокрутку на 0 по X и Y STA $2005 PLA ; загрузить а из стэка TAY ; a -> y перенос PLA TAX ; a -> x перенос PLA ; загрузить а RTI .endproc
Тут есть небольшой но довольно важный момент последовательность пуша аккумулятора в стек и пула для того что бы сохранить значения так как IRQ может сработать где то по середине выполнения программы, и привести к непредвиденным ошибкам выполнения программы.
В качестве заключения материалы по теме:
Предыдущие статьи
https://www.youtube.com/channel/UCzgRrIXX4QDiaWkISx6DKFw - канал о программирование на nes
Программирование assembler 6502 nes/famicom/dendy векторы прерывания, процедуры и их вызов
https://habr.com/ru/post/719636/ - вывод спрайтов и анимация
https://habr.com/ru/publication/edit/721168/ - флаги статусов процессора
