Пришло время для того что бы сделать в игре статус бар, где можно было бы отображать жизни, удары, инвентарь. Для разработки игровых механик на подобие powerup'ов, а так же механик рпг, к примеру что бы пройти на следующую часть уровня надо разбить некое препятствие условным молотком. Есть два основных способа решения данной задачи:

  1. Sprite 0 hit - смысл алгоритма спрайт зеро хит, то что при появление первого непрозрачного пикселя спрайта который перекрывает не прозрачный пиксель фона, нес в статус флаге CPU устанавливает бит флага нулевого спрайта. За этот момент можно зацепится и к примеру до установки данного флага прокрутку устанавливать на 0 а после установки уже прокручивать фон. Способ довольно простой но для простых статус баров с верху. При этом есть объективные минусы в том что мы должны ждать наступления спрайта 0 по этому это должна быть небольшая область с верху. Типичный пример реализации данного метода это супер марио брос, у него в меню есть монетка нижняя часть которой перекрыта спрайтом.

  2. Генерировать событие 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 ; включаем прерывание

Немного распишу порты выше:

  1. E000 - запись любого значения в этот порт отключает прерывание

  2. E001 - запись любого значения включает прерывание

  3. C000 - счетчик обратного отсчета линий развертки, при прохождение каждой лини уменьшается на 1 и при достижение 0 будет сгенерировано прерывание IRQ

  4. С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 может сработать где то по середине выполнения программы, и привести к непредвиденным ошибкам выполнения программы.

В качестве заключения материалы по теме:

  1. документация по MMC3

  2. полезный документ о IRQ прерываниях

Предыдущие статьи

Комментарии (7)


  1. vadimr
    10.08.2023 11:47
    +1

    INX
    CPX #8
    BCC :- 

    Циклы в ассемблере лучше крутить в обратном порядке, тогда не нужна команда сравнения:

    ldx #8

    loop ...

    dex

    bne loop


    1. himysay Автор
      10.08.2023 11:47

      Тут вынуждено, в вашем случае 0 внутри цикла необработается. То есть x=1 dex x=0 и уже зеро флаг будет установлен и алгоритм дальше пойдет. Если x не нужен так и кручу в обратном направлении


      1. vadimr
        10.08.2023 11:47
        +1

        Нет, к сожалению, полного кода. Что-то мешает массив mmc3Register расположить в обратном направлении и написать:

        LDA mmc3Register-1, X

        Возняк бы за 2 байта и 3 такта убил :)


        1. himysay Автор
          10.08.2023 11:47
          +1

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


          1. vadimr
            10.08.2023 11:47
            +2

            Не за что :)

            Есть переведённая на русский книга: У. Морер. Язык ассемблера для персонального компьютера Эпл. Там как раз ставится стиль программирования для 6502. Практически библия.


  1. Alexey2005
    10.08.2023 11:47

    Столько энтузиастов сейчас ковыряет NES, а где результат-то? Где качественные игры, сравнимые с жемчужинами 90-х?
    Игру для NES сейчас сделать не в пример проще, чем 30 лет назад, однако этот самый новодел откровенно разочаровывает.


    1. VBKesha
      10.08.2023 11:47

      1. Энтузиазим не всегда дает хоть какой то результат. Иногда(да на самом деле часто) нравится сам процесс. Сколько народу ковыряли например OpenGL и ничего не сделали в итоге. Я сам из числа таких, поковырять зачастую интересно а вот прям чтобы на выходе было что-то годное что показать не стыдно это другое.


      2. Умение набирать текст на клавиатуре, не делает из человека писателя. Так и тут умение обрабатывать кнопки, и двигать спрайты не обозначает что все можно лепить шедевр. Текущая "простота" дает возможность воплотить идеи в жизнь. Но не дает сами идеи, из разряда по экрану ползают гигантские змеи вокруг шипов и по ним надо доползти до выхода....