Программирование для Famicom/NES/Денди в Nesicide+ca65: ушибленный спрайт (6)
Научившись выводить и задний фон и спрайты мы готовы реализовать технику известную как zero sprite hit. Она применяется главным образом чтобы создать панель статистики в играх со скроллингом. По крайней мере в играх «первой волны».
Панель со статистикой (status bar) «прилеплена» или к верху или к низу экрана и не скроллится вместе с остальным задним фоном.
Однако задний фон у нас только один и создать такой эффект можно только подменив параметры скроллинга в регистре PPU_SCROLL прямо во время того как кадр выводится на экран. Чтобы поймать нужный момент в оригинальном Famicom «из коробки» существует специальный флаг в регистре PPU_STATUS — PPU_STAT_SPR0_HIT. PPU очищает его перед первым сканлайном изображения, а далее взводит в момент когда нарисуется первый непрозрачный пиксель нулевого спрайта, причём не просто нарисуется, а наложится на непрозрачный пиксель заднего фона.
Таким образом программа может опрашивая PPU_STATUS зацепиться за момент когда PPU_STAT_SPR0_HIT меняется с 0 в 1 и в этот момент подменить параметры скроллинга. Остаётся только правильно разместить нулевой спрайт на экране.
Создадим новый проект структуру которого вместе с исходными кодами возьмём из прошлого урока или сразу возьмём готовый проект Example03 отсюда: yadi.sk/d/_THxg1gxuCCVNw
У меня папки этих проектов лежат по пути c:\devel\nes, если вы их разместите по такому же пути, то проблем быть не должно.
По сравнению с предыдущим уроком в файле neslib.inc добавилось много новых вещей.
В конце секции «PPU (ВИДЕО)»:
Назначим имена адресам где у нас в VRAM лежат экранные области, их цветовые атрибуты и палитры. Закрепим здесь же адрес начала таблицы спрайтов в RAM — SPR_TBL. Создадим несколько однострочных (определяются через директиву .define) макросов вычисляющих адреса полей спрайтов в этой области по номеру спрайта.
В конце секции «Полезные макросы»:
Два новых макроса проверяющих то были ли нажаты кнопки на текущем кадре (а не просто зажаты ли они сейчас).
И далее ряд процедур упрощающих заполнение экранных областей и таблицы спрайтов. Обратите внимание на директиву .local — позволяет использовать метку внутри макроса не порождая ошибок дублирования имени меток несколькими такими макросами если бы этой директивы не было. Ибо иначе макрос просто подставлял одну и ту же метку mloop каждый раз при своём использовании и возникла бы такая ошибка. С директивой .local имя метки подвергается трансформации так что в каждом применении макроса она становится уникальной.
До сих пор мы использовали ресурс gamegfx.png из тестового примера среды Nesicide за авторством Damian Yerrick без изменений.
Но в данном уроке пришло время раскрасить его новыми тайлами.
Я делал это в GIMP, но можно сделать в любом графическом редакторе. Главное пользоваться только четырьмя цветами которые уже присутствуют в файле и не добавлять новых. Как я выше говорил их даже не следует воспринимать как цвета ибо это индексы в палитрах — от 0 до 3 от чёрного до белого цвета в самом файле.
Вот так будет выглядеть файл который нам нужен:
Если вы создаёте проект пошагово по инструкции то можете просто сохранить его прямо из этой картинки.
Обратите внимание, что Nesicide кеширует внутри себя графические файлы, поэтому не увидит изменений в этом файле если вы его просто перепишете поверх пока вы не перезапустите среду.
Ну и как всегда самое интересное только начинается — начинаем заполнять с нуля файл main.s:
main.s — начало
Почти весь код в начале мы уже видели в предыдущих уроках. Обращу внимание только на то, что единственная нужная именно этому примеру переменная — это scroll_x в zero page.
Идём дальше:
main.s — вторая часть
Эта процедура переводит байт в параметре arg0b в шестнадцатиричное представление и прописывает двум подряд идущим спрайтам такие номера тайлов чтобы они совпали с ASCII-представлением этого числа. Проще говоря мы в двух подряд идущих спрайтах как бы выводим два разряда шестндацитиричного представления байта. Шестнадцатиричное представление выбрано в связи с исключительной простотой его получения. Как видно процедура действительно небольшая. Регистр X перед вызовом процедуры уже должен быть настроен на поле SPR_TILE первого спрайта. С помощью этой процедуры и вспомогательных спрайтов висящих над полем которое будет скроллится мы будем выводить на экран координаты нулевого спрайта.
Идём дальше:
main.s — третья часть
Инициализационный код настраивает сразу и спрайты и задний фон.
После стандартного «разогрева» мы сперва прячем спрайты под нижним краем экрана прописывая им координату Y в 255 ($FF).
Дальше мы инициализируем с помощью нового макроса set_sprite 9 спрайтов — нулевой в котором будет выводится символ вертикальной черты, четыре цифры для вывода координат нулевого спрайта в двух парах разделённых пустым местом и четыре полностью закрашенных белым спрайта-подкладки для этих цифр, чтобы серый фон не сливался с их цветом. Я тут замечу, что было бы оптимальнее и по размеру и по скорости подготовить в ПЗУ директивами .byte область залитую нужными значениями и скопировать её в область спрайтов в одном цикле, но для теста и такое сойдёт.
Далее мы начинаем «раскрашивать» экранные области так чтобы у них получилась некая «шапка» и поле из кружков обведённое рамочкой. Цветовые атрибуты экранных обалстей заливаются нулевой палитрой, т.е. чёрно-белой с двумя градациями серого.
main.s — четвёртая часть
После старта и происходит самое интересное — дождавшись VBlank и залив в OAM таблицу спрайтов через DMA мы выставляем параметры скроллинга в (0, 0) и начинаем ждать zero sprite hit.
Тут следует пояснить вот какую важную вещь: сразу в VBlank ждать когда он станет 1 нельзя — он здесь уже будет равен 1 потому что zero sprite hit случился на предыдущем кадре. Сами мы не можем обнулить этот флаг — обнуляет его только PPU и делает это сразу перед тем как начнёт рисовать первый сканлайн видеоизображения. Из-за этого нам сперва надо дождаться когда флаг PPU_STAT_SPR0_HIT в PPU_STATUS станет равен 0 — значит VBlank закончился и начался новый кадр. И вот тут уже мы начинаем ждать zero sprite hit — пока флаг не станет равен 1.
Дождавшись мы выдерживаем небольшую настраиваемую паузу холостым циклом по X и записываем в PPU_SCROLL новые параметры прокрутки согласно значению в переменной scroll_x.
Основную задачу мы выполнили. Остаётся только среагировать на нажатия кнопок чтобы картинка ожила и задвигалась:
main.s — конец
Сперва мы выводим в спрайты-цифры координаты нулевого спрайта и далее смотрим какие кнопки нажаты.
Кнопка A скроллит экран вправо, а кнопка B — влево. Т.е. меняет scroll_x.
А вот кнопки направлений двигают нулевой спрайт в соответствующие стороны. Причём если зажата кнопка START, то они это делают шажками — т.е. чтобы сдвинуть спрайт на 1 пиксель надо нажать направление, а чтобы сдвинуть еще на 1 пиксель надо сперва отпустить и снова нажать. Это сделано для лёгкости точной настройки.
Смотрим что же получилось:
Действительно разрыв по горизонтали создаётся, действительно сверху создаётся неподвижное поле пригодное для панели информации и при данном холостом цикле по X нулевой спрайт надо разместить близко к середине экрана чтобы не появлялись перемигивающиеся артефакты.
Минусом данного подхода является то, что в одном сканлайне изображение портится и начинает выводится неправильно — это последствия того как устроен регистр PPU_SCROLL и PPU_ADDR. Вот теперь, если вы не сделали этого раньше, вы можете пройти в другую статью: Подводные камни скроллинга на Famicom/NES/Денди и изучить досконально как устроены эти регистры в Famicom/NES/Dendy и какие подводные камни мы сейчас нащупываем своими стопами.
Традиционным методом избавится от этих артефактов при данном подходе является размещение «косячного» сканлайна в линии с однотонным фоном что в видео тоже демонстрируется.
Остаётся еще заметить, что в механизме zero sprite hit в железе особенно первых ревизий денди были косяки — не работает он, например, на координате X=255. Всё это подробно изложено на nesdev.com, но я тут пересказывать не буду, т.к. практические цели выглядят так как на видео и с этими проблемами не сталкиваются.
Замечу еще только, что если вы выведете нулевой спрайт за пределы видимости, то программа зависнет, т.к. вечно будет ждать появления единичного бита в PPU_STATUS и конечно же никогда его не дождётся.
Ну вроде вот и всё что можно сказать об «ушибленном спрайте».
С накопленным на данный момент багажом уже можно было бы написать игру класса Super Mario, если бы не последняя тема которую надо рассмотреть — вывод звука. Этим мы и займёмся в следующем уроке.
В первую часть (оглавление)...
Панель со статистикой (status bar) «прилеплена» или к верху или к низу экрана и не скроллится вместе с остальным задним фоном.
Однако задний фон у нас только один и создать такой эффект можно только подменив параметры скроллинга в регистре PPU_SCROLL прямо во время того как кадр выводится на экран. Чтобы поймать нужный момент в оригинальном Famicom «из коробки» существует специальный флаг в регистре PPU_STATUS — PPU_STAT_SPR0_HIT. PPU очищает его перед первым сканлайном изображения, а далее взводит в момент когда нарисуется первый непрозрачный пиксель нулевого спрайта, причём не просто нарисуется, а наложится на непрозрачный пиксель заднего фона.
Таким образом программа может опрашивая PPU_STATUS зацепиться за момент когда PPU_STAT_SPR0_HIT меняется с 0 в 1 и в этот момент подменить параметры скроллинга. Остаётся только правильно разместить нулевой спрайт на экране.
Создадим новый проект структуру которого вместе с исходными кодами возьмём из прошлого урока или сразу возьмём готовый проект Example03 отсюда: yadi.sk/d/_THxg1gxuCCVNw
У меня папки этих проектов лежат по пути c:\devel\nes, если вы их разместите по такому же пути, то проблем быть не должно.
По сравнению с предыдущим уроком в файле neslib.inc добавилось много новых вещей.
В конце секции «PPU (ВИДЕО)»:
; *** Некоторые интересные адреса в VRAM
PPU_SCR0 = $2000 ; Адрес начала первой экранной области в VRAM
PPU_SCR0_ATTRS = $23C0 ; Начало атрибутов первой экранной области в VRAM
PPU_SCR1 = $2400 ; Адрес начала второй экранной области в VRAM
PPU_SCR1_ATTRS = $27C0 ; Начало атрибутов второй экранной области в VRAM
PPU_PALETTES = $3F00 ; Начало всех палитр в VRAM
PPU_BGR_PALETTES = $3F00 ; Начало палитр заднего фона в VRAM
PPU_SPR_PALETTES = $3F10 ; Начало палитр спрайтов в VRAM
SPR_TBL = $0200 ; Начало таблицы спрайтов в RAM
; Макросы SPR_FLD_* получают адрес поля спрайта в RAM по его номеру
.define SPR_FLD_X( i ) SPR_TBL + (4 * i) + SPR_X
.define SPR_FLD_Y( i ) SPR_TBL + (4 * i) + SPR_Y
.define SPR_FLD_TILE( i ) SPR_TBL + (4 * i) + SPR_TILE
.define SPR_FLD_ATTR( i ) SPR_TBL + (4 * i) + SPR_ATTR
Назначим имена адресам где у нас в VRAM лежат экранные области, их цветовые атрибуты и палитры. Закрепим здесь же адрес начала таблицы спрайтов в RAM — SPR_TBL. Создадим несколько однострочных (определяются через директиву .define) макросов вычисляющих адреса полей спрайтов в этой области по номеру спрайта.
В конце секции «Полезные макросы»:
; jump_if_keys1_was_not_pressed - перейти на метку label если в keys1_was_pressed
; не зажжён хотя бы один бит в переданном сканкоде key_code.
.macro jump_if_keys1_was_not_pressed key_code, label
lda keys1_was_pressed
and # key_code
beq label
.endmacro
; jump_if_keys2_was_not_pressed - перейти на метку label если в keys2_was_pressed
; не зажжён хотя бы один бит в переданном сканкоде key_code.
.macro jump_if_keys2_was_not_pressed key_code, label
lda keys2_was_pressed
and # key_code
beq label
.endmacro
; poke_vpage - записать байт value по координатам (cx, cy)
; в указанной странице page:
; page - PPU_SCR0 или PPU_SCR1
; cx - от 0 до 31
; cy - от 0 до 29
; value - записываемый байт
; портит аккумулятор!
; Заметьте, что параметры заключаются в скобки потому что иначе при
; подстановке сложных выражений могли бы неправильно развернуться.
.macro poke_vpage page, cx, cy, value
fill_ppu_addr (page) + (cx) + ((cy) * 32)
store PPU_DATA, value
.endmacro
; fill_vpage_line - записать байт value times раз начиная с
; координат (cx, cy) в указанной странице page.
; times не может быть больше 255!
; портит: A, X
.macro fill_vpage_line page, cx, cy, times, value
.local mloop
fill_ppu_addr (page) + (cx) + ((cy) * 32)
lda value
ldx times
mloop: sta PPU_DATA
dex
bne mloop
.endmacro
; fill_page_by - заливает все байты тайлов страницы page байтом value
; не затрагивает область атрибутов.
; портит: A, X
.macro fill_page_by page, value
.local mloop
fill_ppu_addr page
ldx # 32 * 30 / 4
lda value
mloop: sta PPU_DATA
sta PPU_DATA
sta PPU_DATA
sta PPU_DATA
dex
bne mloop
.endmacro
; set_sprite - установить все поля спрайта
.macro set_sprite num, cx, cy, tile, attr
store SPR_FLD_X( num ), cx
store SPR_FLD_Y( num ), cy
store SPR_FLD_TILE( num ), tile
store SPR_FLD_ATTR( num ), attr
.endmacro
Два новых макроса проверяющих то были ли нажаты кнопки на текущем кадре (а не просто зажаты ли они сейчас).
И далее ряд процедур упрощающих заполнение экранных областей и таблицы спрайтов. Обратите внимание на директиву .local — позволяет использовать метку внутри макроса не порождая ошибок дублирования имени меток несколькими такими макросами если бы этой директивы не было. Ибо иначе макрос просто подставлял одну и ту же метку mloop каждый раз при своём использовании и возникла бы такая ошибка. С директивой .local имя метки подвергается трансформации так что в каждом применении макроса она становится уникальной.
До сих пор мы использовали ресурс gamegfx.png из тестового примера среды Nesicide за авторством Damian Yerrick без изменений.
Но в данном уроке пришло время раскрасить его новыми тайлами.
Я делал это в GIMP, но можно сделать в любом графическом редакторе. Главное пользоваться только четырьмя цветами которые уже присутствуют в файле и не добавлять новых. Как я выше говорил их даже не следует воспринимать как цвета ибо это индексы в палитрах — от 0 до 3 от чёрного до белого цвета в самом файле.
Вот так будет выглядеть файл который нам нужен:
Если вы создаёте проект пошагово по инструкции то можете просто сохранить его прямо из этой картинки.
Обратите внимание, что Nesicide кеширует внутри себя графические файлы, поэтому не увидит изменений в этом файле если вы его просто перепишете поверх пока вы не перезапустите среду.
Ну и как всегда самое интересное только начинается — начинаем заполнять с нуля файл main.s:
main.s — начало
; Подключаем заголовок библиотеки Famicom/NES/Денди
.include "src/neslib.inc"
; Сегмент векторов прерываний и сброса/включения - находится в самых
; последних шести байтах адресного пространства процессора ($FFFA-FFFF)
; и содержит адреса по которым процессор переходит при наступлении события
.segment "VECTORS"
.addr nmi ; Вектор прерывания NMI (процедура nmi ниже)
.addr reset ; Вектор сброса/включения (процедура reset ниже)
.addr irq ; Вектор прерывания IRQ (процедура irq ниже)
.segment "ZPAGE": zp ; Сегмент zero page, это надо пометить через ": zp"
vblank_counter: .byte 0 ; Счётчик прерываний VBlank
cur_sprite: .byte 0 ; Нижний байт адреса текущего спрайта в таблцие спрайтов
scroll_x: .byte 0 ; Прокрутка по X
.segment "RAM" ; Сегмент неинициализированных данных в RAM
.segment "ROM_L" ; Сегмент данных в ПЗУ картриджа (страницы $C000-$FFFF)
palettes: ; Подготовленные наборы палитр (для фона и для спрайтов)
; Повторяем наборы 2 раза - первый для фона и второй для спрайтов
.repeat 2
.byte $0F, $00, $10, $20 ; Черный, серый, светло-серый, белый
.byte $0F, $16, $1A, $11 ; -, красный, зеленый, синий
.byte $0F, $1A, $11, $16 ; -, зеленый, синий, красный
.byte $0F, $11, $16, $1A ; -, синий, красный, зеленый
.endrep
.segment "ROM_H" ; Сегмент кода в ПЗУ картриджа (страницы $C000-$FFFF)
; irq - процедура обработки прерывания IRQ
; Пока сразу же возвращается из прерывания как заглушка.
.proc irq
rti ; Инструкция возврата из прерывания
.endproc
; nmi - процедура обработки прерывания NMI
; Обрабатывает наступление прерывания VBlank от PPU (см. процедуру wait_nmi)
.proc nmi
inc vblank_counter ; Просто увеличим vblank_counter
rti ; Возврат из прерывания
.endproc
; wait_nmi - ожидание наступления прерывания VBlank от PPU
; Согласно статье https://wiki.nesdev.com/w/index.php/NMI ожидание VBlank
; опросом верхнего бита PPU_STATUS в цикле может пропускать целые кадры из-за
; специфической гонки состояний, поэтому правильнее всего перехватывать прерывание,
; в нём наращивать счётчик (процедура nmi выше) и ожидать его изменения как в коде ниже.
.proc wait_nmi
lda vblank_counter
notYet: cmp vblank_counter
beq notYet
rts
.endproc
; fill_palettes - заполнить все наборы палитр данными из адреса в памяти
; вход:
; arg0w - адрес таблицы с набором палитр (2 * 4 * 4 байта)
.proc fill_palettes
fill_ppu_addr $3F00 ; палитры в VRAM находятся по адресу $3F00
ldy # 0 ; зануляем счётчик и одновременно индекс
loop:
lda (arg0w), y ; сложный режим адресации - к слову лежащему в zero page
; по однобайтовому адресу arg0w прибавляется Y и
; в A загружается байт из полученного адреса
sta PPU_DATA ; сохраняем в VRAM
iny ; инкрементируем Y
cpy # 2 * 4 * 4 ; проверяем на выход за границу цикла
bne loop ; и зацикливаемся если она еще не достигнута
rts ; выходим из процедуры
.endproc
; fill_attribs - заполнить область цетовых атрибутов байтом в аккумуляторе
; адрес в PPU_ADDR уже должен быть настроен на эту область атрибутов!
.proc fill_attribs
ldx # 64 ; надо залить 64 байта цветовых атрибутов
loop: sta PPU_DATA ; записываем в VRAM аккумулятор
dex ; декрементируем X
bne loop ; цикл по счётчику в X
rts ; возврат из процедуры
.endproc
Почти весь код в начале мы уже видели в предыдущих уроках. Обращу внимание только на то, что единственная нужная именно этому примеру переменная — это scroll_x в zero page.
Идём дальше:
main.s — вторая часть
; num_to_spr - сконвертировать число в arg0b в шестнадцитиричное представление
; и записать старшую цифру как код ASCII по адресу { SPR_TBL, x }, а младшую
; по адресу на 4 байта больше. Т.е. X должен быть настроен на поле TILE
; первого спрайта.
.proc num_to_spr
lda # $F0 ; Оставляем только 4 верхних бита
and arg0b ; из arg0b в аккумуляторе и...
lsr
lsr
lsr ; сдвигаем их на 4 бита правее так что
lsr ; в A теперь лежит верхняя цифра
cmp # 10 ; проверяем не меньше ли она чем 10
bcs ten1 ; и если нет, то идём на код букв A-F
clc ; иначе складываем с кодом '0' чтобы
adc # '0' ; получить ASCII-код цифры 0-9
jmp next1 ; и идём на продолжение
ten1: clc ; В случае буквы надо сложить цифру
adc # 'A' - 10 ; с кодом 'A' за вычетом десяти
next1: sta SPR_TBL, x ; Сохраняем результат в память спрайтов
inx
inx
inx ; И увеличиваем x на 4 чтобы перейти
inx ; к следующему спрайту
lda # $0F
and arg0b ; Оставляем в аккумуляторе 4 нижних бита цифры
cmp # 10 ; проверяем не меньше ли она чем 10
bcs ten2 ; и если нет, то идём на код букв A-F
clc ; иначе складываем с кодом '0' чтобы
adc # '0' ; получить ASCII-код цифры 0-9
jmp next2 ; и идём на продолжение
ten2: clc ; В случае буквы надо сложить цифру
adc # 'A' - 10 ; с кодом 'A' за вычетом десяти
next2: sta SPR_TBL, x ; Сохраняем результат в память спрайтов
rts ; Возвращаемся из подпрограммы
.endproc
Эта процедура переводит байт в параметре arg0b в шестнадцатиричное представление и прописывает двум подряд идущим спрайтам такие номера тайлов чтобы они совпали с ASCII-представлением этого числа. Проще говоря мы в двух подряд идущих спрайтах как бы выводим два разряда шестндацитиричного представления байта. Шестнадцатиричное представление выбрано в связи с исключительной простотой его получения. Как видно процедура действительно небольшая. Регистр X перед вызовом процедуры уже должен быть настроен на поле SPR_TILE первого спрайта. С помощью этой процедуры и вспомогательных спрайтов висящих над полем которое будет скроллится мы будем выводить на экран координаты нулевого спрайта.
Идём дальше:
main.s — третья часть
; reset - стартовая точка всей программы - диктуется вторым адресом в сегменте
; VECTORS оформлена как процедура, но вход в неё происходит при включении консоли
; или сбросу её по кнопке RESET, поэтому ей некуда "возвращаться" и она
; принудительно инициализирует память и стек чтобы работать с чистого листа.
.proc reset
; ***********************************************************
; * Первым делом нужно привести систему в рабочее состояние *
; ***********************************************************
sei ; запрещаем прерывания
ldx # $FF ; чтобы инициализировать стек надо записать $FF в X
txs ; и передать его в регистр вершины стека командой
; Transfer X to S (txs)
; Теперь можно пользоваться стеком, например вызывать процедуры
jsr warm_up ; вызовем процедуру "разогрева" (см. neslib.s)
store_addr arg0w, palettes ; параметр arg0w = адрес наборов палитр
jsr fill_palettes ; вызовем процедуру копирования палитр в PPU
; **********************
; * Прячем все спрайты *
; **********************
lda # $FF ; Запоминаем в аккумуляторе $FF - координату по Y
ldx # 0 ; X настраиваем на начало таблицы спрайтов
loop1: sta SPR_TBL, x ; Записываем $FF в координату Y текущего спрайта
inx
inx
inx
inx ; Увеличиваем X на 4
bne loop1 ; И повторяем цикл пока X не станет равен 0
; Нулевой спрайт '|' в позицию ( 128, 24 ) с палитрой 1
set_sprite 0, # $80, # $18, # '|', # 1
; 4 спрайта под 4 шестнадцатиричных цифры разделенные пустым местом
set_sprite 1, # 8 * 10, # 100, # '0', # 2
set_sprite 2, # 8 * 11, # 100, # '0', # 2
set_sprite 3, # 8 * 13, # 100, # '0', # 2
set_sprite 4, # 8 * 14, # 100, # '0', # 2
; 4 "подкладочных" спрайта залитых белым цветом под цифрами чтобы
; из под них не просвечивал задний фон и они чётко выделялись.
set_sprite 5, # 8 * 10, # 100, # 3, # 0
set_sprite 6, # 8 * 11, # 100, # 3, # 0
set_sprite 7, # 8 * 13, # 100, # 3, # 0
set_sprite 8, # 8 * 14, # 100, # 3, # 0
fill_page_by PPU_SCR0, # $16 ; Сперва целиком зальём экранные
fill_page_by PPU_SCR1, # $16 ; области символом небольшого кружка.
frame_top = 3 ; Верхняя координата в тайлах рамки
frame_btm = 28 ; Нижняя координата в тайлах рамки
; Первые 3 строки PPU_SCR0 зальём символом из вертикальных полос
fill_vpage_line PPU_SCR0, 0, 0, # 3 * 32, # $07
; Краевые уголки рамки
poke_vpage PPU_SCR0, 0, frame_top, # $10
poke_vpage PPU_SCR0, 0, frame_btm, # $12
poke_vpage PPU_SCR1, 31, frame_top, # $11
poke_vpage PPU_SCR1, 31, frame_btm, # $13
; Горизонтальные линии сверху и снизу рамки в обеих экранных областях
fill_vpage_line PPU_SCR0, 1, frame_top, # 31, # $15
fill_vpage_line PPU_SCR0, 1, frame_btm, # 31, # $15
fill_vpage_line PPU_SCR1, 0, frame_top, # 31, # $15
fill_vpage_line PPU_SCR1, 0, frame_btm, # 31, # $15
; Включим инкремент PPU_ADD на 32 чтобы рисовать вертикальные линии
store PPU_CTRL, # PPU_ADDR_INC32
; Две вертикальных линии рамки
fill_vpage_line PPU_SCR0, 0, frame_top + 1, # (frame_btm - frame_top - 1), # $14
fill_vpage_line PPU_SCR1, 31, frame_top + 1, # (frame_btm - frame_top - 1), # $14
store PPU_CTRL, # 0 ; Вернёмся обратно в режим инкремента PPU_ADDR на 1
; Зальём цветовые атрибуты обеих экранных областей нулевой палитрой
fill_ppu_addr PPU_SCR0_ATTRS
lda # 0
jsr fill_attribs
fill_ppu_addr PPU_SCR1_ATTRS
lda # 0
jsr fill_attribs
Инициализационный код настраивает сразу и спрайты и задний фон.
После стандартного «разогрева» мы сперва прячем спрайты под нижним краем экрана прописывая им координату Y в 255 ($FF).
Дальше мы инициализируем с помощью нового макроса set_sprite 9 спрайтов — нулевой в котором будет выводится символ вертикальной черты, четыре цифры для вывода координат нулевого спрайта в двух парах разделённых пустым местом и четыре полностью закрашенных белым спрайта-подкладки для этих цифр, чтобы серый фон не сливался с их цветом. Я тут замечу, что было бы оптимальнее и по размеру и по скорости подготовить в ПЗУ директивами .byte область залитую нужными значениями и скопировать её в область спрайтов в одном цикле, но для теста и такое сойдёт.
Далее мы начинаем «раскрашивать» экранные области так чтобы у них получилась некая «шапка» и поле из кружков обведённое рамочкой. Цветовые атрибуты экранных обалстей заливаются нулевой палитрой, т.е. чёрно-белой с двумя градациями серого.
main.s — четвёртая часть
; **********************************************
; * Стартуем видеочип и запускаем все процессы *
; **********************************************
; Включим генерацию прерываний по VBlank и источником тайлов для спрайтов
; сделаем второй банк видеоданных где у нас находится шрифт.
store PPU_CTRL, # PPU_VBLANK_NMI | PPU_SPR_TBL_1000 | PPU_BGR_TBL_1000
; Включим отображение спрайтов и то что они отображаются в левых 8 столбцах пикселей
store PPU_MASK, # PPU_SHOW_SPR | PPU_SHOW_LEFT_SPR | PPU_SHOW_BGR | PPU_SHOW_LEFT_BGR
cli ; Разрешаем прерывания
; ***************************
; * Основной цикл программы *
; ***************************
main_loop:
jsr wait_nmi ; ждём наступления VBlank
; Чтобы обновить таблицу спрайтов в видеочипе надо записать в OAM_ADDR ноль
store OAM_ADDR, # 0
; И активировать DMA записью верхнего байта адреса страницы с описаниями
store OAM_DMA, # >SPR_TBL
store PPU_SCROLL, # 0 ; Перед началом кадра выставим скроллинг
store PPU_SCROLL, # 0 ; в (0, 0) чтобы панель рисовалась фиксированно
spr0hit1: ; Сперва надо дождаться очистки флага PPU_STAT_SPR0_HIT
bit PPU_STATUS ; Для этого надо крутится в цикле пока он взведён.
bvs spr0hit1 ; (операция bit как раз помещает этот бит во флаг oVerflow)
spr0hit2:
bit PPU_STATUS ; Теперь наоборот ждём пока этот флаг взведётся.
bvc spr0hit2 ; Таким образом выход из цикла произойдёт при zero sprite hit.
ldx # 10 ; Немного выждем в холостом цикле
wait1: dex ; чтобы задержать обновление скроллинга
bne wait1 ; Можете поэкспериментировать с величиной задержки
; Здесь мы уже находимся в кадре и применяем обновление скроллинга
store PPU_SCROLL, scroll_x
store PPU_SCROLL, # 0
После старта и происходит самое интересное — дождавшись VBlank и залив в OAM таблицу спрайтов через DMA мы выставляем параметры скроллинга в (0, 0) и начинаем ждать zero sprite hit.
Тут следует пояснить вот какую важную вещь: сразу в VBlank ждать когда он станет 1 нельзя — он здесь уже будет равен 1 потому что zero sprite hit случился на предыдущем кадре. Сами мы не можем обнулить этот флаг — обнуляет его только PPU и делает это сразу перед тем как начнёт рисовать первый сканлайн видеоизображения. Из-за этого нам сперва надо дождаться когда флаг PPU_STAT_SPR0_HIT в PPU_STATUS станет равен 0 — значит VBlank закончился и начался новый кадр. И вот тут уже мы начинаем ждать zero sprite hit — пока флаг не станет равен 1.
Дождавшись мы выдерживаем небольшую настраиваемую паузу холостым циклом по X и записываем в PPU_SCROLL новые параметры прокрутки согласно значению в переменной scroll_x.
Основную задачу мы выполнили. Остаётся только среагировать на нажатия кнопок чтобы картинка ожила и задвигалась:
main.s — конец
; ********************************************************
; * После работы с VRAM можно заняться другими вещами... *
; ********************************************************
jsr update_keys ; Обновим состояние кнопок опросив геймпады
store arg0b, SPR_FLD_X( 0 ) ; Сохраним в arg0b координату X спрайта 0
ldx # 1 * 4 + SPR_TILE ; в регистре X нацелимся на номер тайла спрайта 1
jsr num_to_spr ; сконвертируем arg0b в число с записью цифр в спрайты 1 и 2
store arg0b, SPR_FLD_Y( 0 ) ; Сохраним в arg0b координату Y спрайта 0
ldx # 3 * 4 + SPR_TILE ; в регистре X нацелимся на номер тайла спрайта 3
jsr num_to_spr ; сконвертируем arg0b в число с записью цифр в спрайты 3 и 4
; Если не нажата кнопка (B), то идём дальше на skip_b
jump_if_keys1_is_not_down KEY_B, skip_b
lda # 0
cmp scroll_x ; Если scroll_x уже ноль
beq skip_b ; то пропускаем
dec scroll_x ; уменьшение scroll_x
skip_b:
; Если не нажата кнопка (A), то идём дальше...
jump_if_keys1_is_not_down KEY_A, skip_a
lda # 255
cmp scroll_x ; Если scroll_x уже 255
beq skip_a ; то пропускаем
inc scroll_x ; увеличение scroll_x
skip_a:
store arg0b, keys1_is_down ; сперва пишем в arg0b копию keys1_is_down
; Если не зажат START, то идём дальше (в arg0b останется keys1_is_down)
jump_if_keys1_is_not_down KEY_START, skip_start1
store arg0b, keys1_was_pressed ; иначе в arg0b окажется копия keys1_was_pressed
skip_start1:
; Сделаем макрос подобный jump_if_keys1_is_not_down, но работающий с arg0b
; чтобы реагировать на кнопки направлений непрерывно или отрывисто в зависимости
; от того зажата или нет кнопка START из-за кода выше...
.macro jump_if_arg0_doesnt_contain key_code, label
lda arg0b
and # key_code
beq label
.endmacro
; В зависимости от нажатых кнопок изменяем координаты нулевого спрайта
jump_if_arg0_doesnt_contain KEY_LEFT, skip_left
dec SPR_FLD_X( 0 )
skip_left:
jump_if_arg0_doesnt_contain KEY_RIGHT, skip_right
inc SPR_FLD_X( 0 )
skip_right:
jump_if_arg0_doesnt_contain KEY_UP, skip_up
dec SPR_FLD_Y( 0 )
skip_up:
jump_if_arg0_doesnt_contain KEY_DOWN, skip_down
inc SPR_FLD_Y( 0 )
skip_down:
jmp main_loop ; И уходим ждать нового VBlank в бесконечном цикле
.endproc
Сперва мы выводим в спрайты-цифры координаты нулевого спрайта и далее смотрим какие кнопки нажаты.
Кнопка A скроллит экран вправо, а кнопка B — влево. Т.е. меняет scroll_x.
А вот кнопки направлений двигают нулевой спрайт в соответствующие стороны. Причём если зажата кнопка START, то они это делают шажками — т.е. чтобы сдвинуть спрайт на 1 пиксель надо нажать направление, а чтобы сдвинуть еще на 1 пиксель надо сперва отпустить и снова нажать. Это сделано для лёгкости точной настройки.
Смотрим что же получилось:
Действительно разрыв по горизонтали создаётся, действительно сверху создаётся неподвижное поле пригодное для панели информации и при данном холостом цикле по X нулевой спрайт надо разместить близко к середине экрана чтобы не появлялись перемигивающиеся артефакты.
Минусом данного подхода является то, что в одном сканлайне изображение портится и начинает выводится неправильно — это последствия того как устроен регистр PPU_SCROLL и PPU_ADDR. Вот теперь, если вы не сделали этого раньше, вы можете пройти в другую статью: Подводные камни скроллинга на Famicom/NES/Денди и изучить досконально как устроены эти регистры в Famicom/NES/Dendy и какие подводные камни мы сейчас нащупываем своими стопами.
Традиционным методом избавится от этих артефактов при данном подходе является размещение «косячного» сканлайна в линии с однотонным фоном что в видео тоже демонстрируется.
Остаётся еще заметить, что в механизме zero sprite hit в железе особенно первых ревизий денди были косяки — не работает он, например, на координате X=255. Всё это подробно изложено на nesdev.com, но я тут пересказывать не буду, т.к. практические цели выглядят так как на видео и с этими проблемами не сталкиваются.
Замечу еще только, что если вы выведете нулевой спрайт за пределы видимости, то программа зависнет, т.к. вечно будет ждать появления единичного бита в PPU_STATUS и конечно же никогда его не дождётся.
Ну вроде вот и всё что можно сказать об «ушибленном спрайте».
С накопленным на данный момент багажом уже можно было бы написать игру класса Super Mario, если бы не последняя тема которую надо рассмотреть — вывод звука. Этим мы и займёмся в следующем уроке.
В первую часть (оглавление)...
0 комментариев