Пришло время для того что бы сделать в игре статус бар, где можно было бы отображать жизни, удары, инвентарь. Для разработки игровых механик на подобие 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/ - флаги статусов процессора
Комментарии (7)
Alexey2005
10.08.2023 11:47Столько энтузиастов сейчас ковыряет NES, а где результат-то? Где качественные игры, сравнимые с жемчужинами 90-х?
Игру для NES сейчас сделать не в пример проще, чем 30 лет назад, однако этот самый новодел откровенно разочаровывает.VBKesha
10.08.2023 11:47-
Энтузиазим не всегда дает хоть какой то результат. Иногда(да на самом деле часто) нравится сам процесс. Сколько народу ковыряли например OpenGL и ничего не сделали в итоге. Я сам из числа таких, поковырять зачастую интересно а вот прям чтобы на выходе было что-то годное что показать не стыдно это другое.
-
Умение набирать текст на клавиатуре, не делает из человека писателя. Так и тут умение обрабатывать кнопки, и двигать спрайты не обозначает что все можно лепить шедевр. Текущая "простота" дает возможность воплотить идеи в жизнь. Но не дает сами идеи, из разряда по экрану ползают гигантские змеи вокруг шипов и по ним надо доползти до выхода....
-
vadimr
Циклы в ассемблере лучше крутить в обратном порядке, тогда не нужна команда сравнения:
ldx #8
loop ...
dex
bne loop
himysay Автор
Тут вынуждено, в вашем случае 0 внутри цикла необработается. То есть x=1 dex x=0 и уже зеро флаг будет установлен и алгоритм дальше пойдет. Если x не нужен так и кручу в обратном направлении
vadimr
Нет, к сожалению, полного кода. Что-то мешает массив mmc3Register расположить в обратном направлении и написать:
LDA mmc3Register-1, X
Возняк бы за 2 байта и 3 такта убил :)
himysay Автор
Позже попробую обязательно так сделать, тут мне важно было рабочий вариант сделать. Спасибо.
vadimr
Не за что :)
Есть переведённая на русский книга: У. Морер. Язык ассемблера для персонального компьютера Эпл. Там как раз ставится стиль программирования для 6502. Практически библия.