Программирование для Famicom/NES/Денди в Nesicide+ca65: спрайты (5)
Прежде чем перейти к следующему примеру — спрайтам я бы хотел немного обсудить то что мы увидели в примере с задним фоном.
Во первых — в эмуляторе внутри самого Nesicide можно не заметить одну крайне важную вещь. Он почему то её даже не пытается эмулировать.
Поэтому лучше для финальных проверок использовать другой эмулятор, например я использую FCEUX.
В нём сразу же можно увидеть одну важную вещь: когда параметры скроллинга выставлены в (0,0) первую горизонтальную полоску тайлов с '0000000...' на экране не видно. И это важный момент которому посвящена статья на Overscan (англ). на nesdev.com. Часть изображения в телевизионном формате NTSC просто выпадает за границы экрана.
Приведу картинку из этой статьи:
Красным обозначены строки которые в NTSC практически гарантированно выпадают за границы экрана и не появляются на экране.
Самая внутренняя серая граница по документации самой Nintendo гарантированно отобразится.
Жёлто-коричневым цветом обозначены границы зоны которую большинство телевизоров покажут хотя бы частично, но из-за частичности располагать тут важную информацию не стоит.
Синим обозначена зона которая была самой Nintendo обозначена как ненадёжная, но на практике все телевизоры её показывают.
Интересно, что в системе PAL эти границы все раздвинулись и лично я очень часто замечал в детстве играя на денди на обычном советском чёрно-белом телевизоре всевозможные тайловые артефакты сверху и снизу экрана. А тот же FCEUX в настройках содержит прямые настройки того сколько линий изображения пропускать отдельно для режимов NTSC и PAL.
Создадим новый проект так же как предыдущий создав его из шаблона CA65 Hello world, удалив файл nes.h и добавив наши файлы neslib.inc и neslib.s скопировав их из предыдущего урока. Скопируем оттуда так же header.s и nes.ini поверх тех файлов что среда создала (их полные тела есть так же в предыдущем уроке).
В neslib.inc в конце секции «PPU (ВИДЕО)» (сразу перед секцией «INPUT (ВВОД)») добавим новый код описывающий структуру спрайтов в консоли:
Спрайты хранятся в особой плашке DRAM в PPU и не входят в обычную VRAM доступную через PPU_ADDR/DATA. Память спрайтов называется OAM (Object Attributes Memory) имеет размер 256 байт и по идее должна была бы быть доступной через похожие по смыслу порты ввода-вывода OAM_ADDR и OAM_DATA. OAM_ADDR это однобайтовый адрес в OAM который можно выставить записью в порт этого байта, а при записи в OAM_DATA записываемый байт записывается в OAM по адресу OAM_ADDR, а сам OAM_ADDR автоматически увеличивается на 1.
Однако в первых ревизиях PPU запись в OAM_ADDR портила память в OAM! А есть обязательное условие: когда PPU начинает рисовать кадр после VBlank, то OAM_ADDR должен быть равен 0, PPU на это полагается в своей работе.
Выходит что единственным способом сделать всё правильно становится запись в OAM_ADDR нуля после чего в OAM_DATA записывается 256 байт всей таблицы спрайтов чтобы OAM_ADDR «провернулся» и после переполнения стал равен нулю.
Но именно это по смыслу делает DMA консоли — запись в порт OAM_DMA байта XX передаст из адресного пространства процессора 256 байт начиная с адреса $XX00 в память спрайтов PPU. Причём DMA сделает это очень быстро — примерно вчетверо быстрее чем код на процессоре.
Поэтому нам доступ к OAM_DATA не нужен совсем, а в OAM_ADDR мы будем только писать 0 перед активацией DMA.
Хранить копию таблицы спрайтов которую DMA будет записывать в OAM мы будем с адреса $0200 — в свободной области сразу после стека — это как раз то почему мы разместили сегмент RAM с адреса $0300 оставив место под таблицу спрайтов.
В норме спрайт отображает один тайл, т.е. имеет размер 8x8 пикселей. В таблице из 256 байт описания спрайтов располагаются последовательно, каждое описание занимает 4 байта и таким образом всего получается возможно 64 спрайта. Первый и последний байты описания содержат координаты Y и X спрайта соответственно. С координатой Y надо учитывать, что PPU когда рисует текущий сканлайн считывает данные о спрайтах для следующего сканлайна. Из-за этого есть такой артефакт, что в первом сканлайне спрайты вообще не рисуются — из-за отсутствия предыдущего сканлайна данные о спрайтах в нём еще не считаны. Более того — PPU из-за этого как бы опаздывает на одну строку по спрайтам и они отображаются сдвинутыми на 1 пиксель вниз. Т.е. координату Y надо уменьшать на 1 чтобы спрайт оказался в нужной позиции по Y.
Второй байт содержит номер отображаемого тайла, а третий байт — битовый набор атрибутов. Атрибуты это выбор палитры (0-3) спрайта и два бита зеркалирования по горизонтали и вертикали которые могут быть включены одновременно.
Обязательно надо иметь ввиду то, что в одном сканлайне видеочип не может нарисовать больше восьми спрайтов — остальные будут просто проигнорированы. При этом видеочип в самом начале каждого кадра очищает в регистре PPU_STATUS бит PPU_STAT_OVERFLOW, а когда встречает более восьми спрайтов в строке, то взводит его — так что процессор может проверить наступило такое событие или нет. Однако и тут инженеры немножко накосячили и из-за бага в железе на этот бит нельзя полагаться на все 100% — могут возникнуть как ложные срабатывания так и неправильные несрабатывания. Спрайты отрисовываются и заслоняют друг друга в порядке своих позиций в таблице — т.е. нулевой спрайт всегда нарисуется первым и заслонит собой все остальные спрайты если они окажутся под ним и так далее. Поэтому популярной методикой борьбы с лимитом количества спрайтов в строке является постоянная смена их позиций — тогда в одних кадрах исчезнут одни спрайты, а в других — другие. Спрайты начинают тогда характерно перемигиваться, но их хотя бы видно все хоть и в разные моменты времени.
В норме спрайты выводятся как бы над задним фоном, но если зажечь бит SPR_BEHIND_BGR в поле SPR_ATTR спрайта, то он будет отображаться как бы за задним фоном и тут станет важен факт прозрачности пикселей в тайлах заднего фона.
В PPU_CTRL можно включить режим спрайтов 8x16. При этом нижний бит номера тайла в описании спрайта начинает означать какой банк тайловых данных будет использован как источник (т.е. флаг PPU_SPR_TBL_1000 в PPU_CTRL перестаёт играть какую либо роль) тайлов для этого спрайта. Номер тайла внутри таблицы поэтому получается всегда чётным числом и тайл с этим номером используется как верхние 8x8 пикселей составного тайла, а тайл со следующим (нечётным) номером — как нижние. Зеркалируются тайлы 8x16 как единое целое.
Теперь перейдём в main.s, сотрём там всё старое и начнём писать новый код:
main.s — начало
По сравнению с предыдущим примером тут исчезли переменные нужные в нём, а появилась только одна нужная для этого урока — cur_sprite.
Так же оказались немного подправлены данные для палитр — теперь все 4 субпалитры отличаются друг от друга.
Начало тоже стандартное — запретили прерывания, настроили стек, вызвали процедуру разогрева и залили в PPU палитры.
Отличия начинаются ниже:
main.s — вторая часть
Перво наперво мы создаём синоним для начала области $0200 — SPR_TBL.
Далее используя arg0-2b заполняем таблицу спрайтов так чтобы они выстроились все лесенкой начиная с координаты (0,0) увеличивая обе на 3 с каждым новым спрайтом и получили при этом тайлы символов алфавита по порядку начиная с символа '0'. Кроме того каждый новый спрайт будет получать новый номер палитры циклически от 0 до 3.
Далее стартуем видеочип — при этом надо указать что спрайты находятся в той таблице тайлов где у нас расположены символы алфавита (флаг PPU_SPR_TBL_1000). В PPU_MASK включаем отображение спрайтов и левой полоски пикселей для них, об этом как раз поговорим ниже.
Флага PPU_SHOW_LEFT_BGR — т.е. левая полоска пикселей для фонов тут вообще быть не должно, но похоже что эмулятор встроенный в Nesicide сейчас его неправильно обрабатывает, поэтому здесь его лучше включить чтобы в нём всё выглядело правильно. Об этих вещах мы поговорим ниже.
Идём дальше — в игровой цикл:
main.s — третья часть
Здесь мы как и полагается ждём NMI и сразу же с помощью DMA заливает таблицу спрайтов в PPU. Просто? Очень. В OAM_DMA надо записать верхний байт адреса страницы в адресном пространстве консоли и DMA шустренько зальёт все байты из этой страницы в OAM_DATA.
А дальше мы обновляем таблицу спрайтов «бегущей» на каждом кадре волной: текущему спрайту в cur_sprite мы циклически изменяем вариант зеркалирования и переводим cur_sprite на следующий спрайт. В конце он сам провернётся в 0 и волна обновления побежит снова с начала.
Кроме того последним двум спрайтам в таблице увеличиваются координаты X и Y соответственно. Адреса полей с этими координатами высчитываются выражением во время компиляции SPR_TBL + 4 * i даёт начало данных спрайта с номером i в таблице и прибавляя смещения полей внутри мы получаем адрес нужного атрибута.
Смотрим что получилось:
Из-за того что верхние 8 строк не отображаются спрайт с номером 0 почти не видно, но иногда в процессе зеркалирования белая полоска пикселей от него всё-таки появляется скраю экрана. Это следствие того, что спрайты как бы сдвинуты на 1 пиксель вниз — имея координаты (0,0) в атрибутах по факту спрайт отображается как будто имеет координаты (0,1). Это верно для всех спрайтов.
Со спрайтом который двигается вдоль горизонтали можно заметить одну неприятность. Размер экрана по горизонтали в пикселях — 256. Но и координате спрайта по горизонтали выделен 1 байт, т.е. диапазон возможных значений тоже 0-255. Когда спрайт подходит к правой границе экрана он начинает скрываться за ней плавно. Но на левой границе он появляется сразу весь целиком и плавно скрыться за ней неспособен — здесь не существует отрицательных координат чтобы такое провернуть. В некоторых играх (например Contra) спрайты врагов действительно плавно выбежав из-за правого края экрана и пробежав по экрану налево сразу исчезают как только коснутся левой границы экрана — зная всё это сей фактик можно легко заметить. Но иногда художник видит спрайты плавно исчезающими как за правой так и за левой границами экрана и вот чтобы это сделать и появились два битовых флага в регистре PPU_MASK — PPU_SHOW_LEFT_BGR и PPU_SHOW_LEFT_SPR. Если они не включены, то 8 самых левых колонок пикселей экрана не рисуются для фонов и спрайтов соответственно и ценой исчезновения этой полоски можно сделать плавное появление спрайта из-за левой границы экрана.
Для координаты по вертикали аналогичного флага нет, т.к. восемь первых сканлайнов вообще не должны отображаються на экране телевизора. Более того — самый правильный способ скрыть неиспользованный спрайт — это вывести его за нижнюю границу экрана записав в его координату Y число из диапазона $EF-$FF, ведь даже если его попытаться скрыть назначив полностью прозрачный тайл, то он продолжает влиять на лимит спрайтов в строке и может этим помешать.
Еще стоит заметить, что память OAM в оригинальных PPU выполнена по технологии DRAM, т.е. реализована на динамической памяти которую надо обновлять периодически в так называемом процессе регенерации. Но автоматического регенератора у неё нет — если отображение спрайтов включено, то PPU считывая OAM каждый кадр как бы сам производит регенерацию. Но если отображение спрайтов выключить, то уже через пару кадров OAM начнёт деградировать и её содержимое будет портится, поэтому никогда не надо полагаться на то что спрайты в OAM сохранятся надолго выключая их рендеринг.
В первую часть (оглавление)...
Во первых — в эмуляторе внутри самого Nesicide можно не заметить одну крайне важную вещь. Он почему то её даже не пытается эмулировать.
Поэтому лучше для финальных проверок использовать другой эмулятор, например я использую FCEUX.
В нём сразу же можно увидеть одну важную вещь: когда параметры скроллинга выставлены в (0,0) первую горизонтальную полоску тайлов с '0000000...' на экране не видно. И это важный момент которому посвящена статья на Overscan (англ). на nesdev.com. Часть изображения в телевизионном формате NTSC просто выпадает за границы экрана.
Приведу картинку из этой статьи:
Красным обозначены строки которые в NTSC практически гарантированно выпадают за границы экрана и не появляются на экране.
Самая внутренняя серая граница по документации самой Nintendo гарантированно отобразится.
Жёлто-коричневым цветом обозначены границы зоны которую большинство телевизоров покажут хотя бы частично, но из-за частичности располагать тут важную информацию не стоит.
Синим обозначена зона которая была самой Nintendo обозначена как ненадёжная, но на практике все телевизоры её показывают.
Интересно, что в системе PAL эти границы все раздвинулись и лично я очень часто замечал в детстве играя на денди на обычном советском чёрно-белом телевизоре всевозможные тайловые артефакты сверху и снизу экрана. А тот же FCEUX в настройках содержит прямые настройки того сколько линий изображения пропускать отдельно для режимов NTSC и PAL.
Спрайты
Создадим новый проект так же как предыдущий создав его из шаблона CA65 Hello world, удалив файл nes.h и добавив наши файлы neslib.inc и neslib.s скопировав их из предыдущего урока. Скопируем оттуда так же header.s и nes.ini поверх тех файлов что среда создала (их полные тела есть так же в предыдущем уроке).
В neslib.inc в конце секции «PPU (ВИДЕО)» (сразу перед секцией «INPUT (ВВОД)») добавим новый код описывающий структуру спрайтов в консоли:
; *** SPRITE DESC - описание спрайта
; SPR_Y - смещение до координаты Y спрайта. Из-за того, что данные о спрайтах запаздывают на
; один сканлайн значение нужно уменьшить на 1.
SPR_Y = 0
; SPR_TILE - смещение до номера тайла спрайта (0-255)
SPR_TILE = 1
; SPR_ATTR - смещение до атрибутов спрайта (см. ниже)
SPR_ATTR = 2
; SPR_X - смещение до координаты X спрайта
SPR_X = 3
; *** Битовые флаги атрибутов спрайта (SPR_ATTR):
; Биты 0-1: палитра спрайта (0-3)
SPR_PAL_0 = %00000000
SPR_PAL_1 = %00000001
SPR_PAL_2 = %00000010
SPR_PAL_3 = %00000011
; Бит 5: приоритет - под задним фоном
SPR_BEHIND_BGR = %00100000
; Бит 6: зеркалирование по горизонтали
SPR_FLIP_H = %01000000
; Бит 7: зеркалирвоание по вертикали
SPR_FLIP_V = %10000000
Спрайты хранятся в особой плашке DRAM в PPU и не входят в обычную VRAM доступную через PPU_ADDR/DATA. Память спрайтов называется OAM (Object Attributes Memory) имеет размер 256 байт и по идее должна была бы быть доступной через похожие по смыслу порты ввода-вывода OAM_ADDR и OAM_DATA. OAM_ADDR это однобайтовый адрес в OAM который можно выставить записью в порт этого байта, а при записи в OAM_DATA записываемый байт записывается в OAM по адресу OAM_ADDR, а сам OAM_ADDR автоматически увеличивается на 1.
Однако в первых ревизиях PPU запись в OAM_ADDR портила память в OAM! А есть обязательное условие: когда PPU начинает рисовать кадр после VBlank, то OAM_ADDR должен быть равен 0, PPU на это полагается в своей работе.
Выходит что единственным способом сделать всё правильно становится запись в OAM_ADDR нуля после чего в OAM_DATA записывается 256 байт всей таблицы спрайтов чтобы OAM_ADDR «провернулся» и после переполнения стал равен нулю.
Но именно это по смыслу делает DMA консоли — запись в порт OAM_DMA байта XX передаст из адресного пространства процессора 256 байт начиная с адреса $XX00 в память спрайтов PPU. Причём DMA сделает это очень быстро — примерно вчетверо быстрее чем код на процессоре.
Поэтому нам доступ к OAM_DATA не нужен совсем, а в OAM_ADDR мы будем только писать 0 перед активацией DMA.
Хранить копию таблицы спрайтов которую DMA будет записывать в OAM мы будем с адреса $0200 — в свободной области сразу после стека — это как раз то почему мы разместили сегмент RAM с адреса $0300 оставив место под таблицу спрайтов.
В норме спрайт отображает один тайл, т.е. имеет размер 8x8 пикселей. В таблице из 256 байт описания спрайтов располагаются последовательно, каждое описание занимает 4 байта и таким образом всего получается возможно 64 спрайта. Первый и последний байты описания содержат координаты Y и X спрайта соответственно. С координатой Y надо учитывать, что PPU когда рисует текущий сканлайн считывает данные о спрайтах для следующего сканлайна. Из-за этого есть такой артефакт, что в первом сканлайне спрайты вообще не рисуются — из-за отсутствия предыдущего сканлайна данные о спрайтах в нём еще не считаны. Более того — PPU из-за этого как бы опаздывает на одну строку по спрайтам и они отображаются сдвинутыми на 1 пиксель вниз. Т.е. координату Y надо уменьшать на 1 чтобы спрайт оказался в нужной позиции по Y.
Второй байт содержит номер отображаемого тайла, а третий байт — битовый набор атрибутов. Атрибуты это выбор палитры (0-3) спрайта и два бита зеркалирования по горизонтали и вертикали которые могут быть включены одновременно.
Обязательно надо иметь ввиду то, что в одном сканлайне видеочип не может нарисовать больше восьми спрайтов — остальные будут просто проигнорированы. При этом видеочип в самом начале каждого кадра очищает в регистре PPU_STATUS бит PPU_STAT_OVERFLOW, а когда встречает более восьми спрайтов в строке, то взводит его — так что процессор может проверить наступило такое событие или нет. Однако и тут инженеры немножко накосячили и из-за бага в железе на этот бит нельзя полагаться на все 100% — могут возникнуть как ложные срабатывания так и неправильные несрабатывания. Спрайты отрисовываются и заслоняют друг друга в порядке своих позиций в таблице — т.е. нулевой спрайт всегда нарисуется первым и заслонит собой все остальные спрайты если они окажутся под ним и так далее. Поэтому популярной методикой борьбы с лимитом количества спрайтов в строке является постоянная смена их позиций — тогда в одних кадрах исчезнут одни спрайты, а в других — другие. Спрайты начинают тогда характерно перемигиваться, но их хотя бы видно все хоть и в разные моменты времени.
В норме спрайты выводятся как бы над задним фоном, но если зажечь бит SPR_BEHIND_BGR в поле SPR_ATTR спрайта, то он будет отображаться как бы за задним фоном и тут станет важен факт прозрачности пикселей в тайлах заднего фона.
В PPU_CTRL можно включить режим спрайтов 8x16. При этом нижний бит номера тайла в описании спрайта начинает означать какой банк тайловых данных будет использован как источник (т.е. флаг PPU_SPR_TBL_1000 в PPU_CTRL перестаёт играть какую либо роль) тайлов для этого спрайта. Номер тайла внутри таблицы поэтому получается всегда чётным числом и тайл с этим номером используется как верхние 8x8 пикселей составного тайла, а тайл со следующим (нечётным) номером — как нижние. Зеркалируются тайлы 8x16 как единое целое.
Теперь перейдём в 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 ; Нижний байт адреса текущего спрайта в таблцие спрайтов
.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
; 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
По сравнению с предыдущим примером тут исчезли переменные нужные в нём, а появилась только одна нужная для этого урока — cur_sprite.
Так же оказались немного подправлены данные для палитр — теперь все 4 субпалитры отличаются друг от друга.
Начало тоже стандартное — запретили прерывания, настроили стек, вызвали процедуру разогрева и залили в PPU палитры.
Отличия начинаются ниже:
main.s — вторая часть
SPR_TBL = $0200 ; Создадим символ для таблицы спрайтов в RAM
; ********************************************************
; * Инициализируем все 64 спрайта выстроив их 'лесенкой' *
; ********************************************************
store arg0b, 0 ; arg0b будет хранить нарастающую координату
store arg1b, # $30 ; arg1b будет хранит ASCII-код символа начиная с '0'
store arg2b, 0 ; arg2b будет хранить нарастающий номер палитры 0-4
ldx # 0 ; занулим X
; Для того чтобы в макрос store передать параметр содержащий запятую нужно
; заключить его в фигурные скобки - это нужно для режима адресации ADDR+x
loop: store { SPR_TBL, x }, arg0b ; в поле SPR_Y сохраним arg0b (координату)
inx ; переходим к следующему полю
store { SPR_TBL, x }, arg1b ; в поле SPR_TILE сохраним номер тайла
inx ; переходим к следующему полю
store { SPR_TBL, x }, arg2b ; в поле атрибутов сохраним нарастающий номер палитры
inx ; переходим к следующему полю
store { SPR_TBL, x }, arg0b ; и, наконец, сохраняем координату в SPR_X
inx ; переходим к следующему тайлу
inc arg0b
inc arg0b
inc arg0b ; нарастающую координату увеличим на 3
inc arg1b ; номер тайла увеличим на 1
inc arg2b ; номер палитры увеличим на 1
lda arg2b ; но т.к. он не должен выходить за пределы двух бит
and # %011 ; то грузим его в аккумулятор и сбрасываем остальные биты
sta arg2b ; и сохраняем обратно
cpx # 0 ; проверим не провернулся ли x в ноль (значит вся таблица пройдена)
bne loop ; и если нет, то идём на следующую итерацию
; **********************************************
; * Стартуем видеочип и запускаем все процессы *
; **********************************************
; Включим генерацию прерываний по VBlank и источником тайлов для спрайтов
; сделаем второй банк видеоданных где у нас находится шрифт.
store PPU_CTRL, # PPU_VBLANK_NMI | PPU_SPR_TBL_1000
; Включим отображение спрайтов и то что они отображаются в левых 8 столбцах пикселей
store PPU_MASK, # PPU_SHOW_SPR | PPU_SHOW_LEFT_SPR | PPU_SHOW_LEFT_BGR
cli ; Разрешаем прерывания
Перво наперво мы создаём синоним для начала области $0200 — SPR_TBL.
Далее используя arg0-2b заполняем таблицу спрайтов так чтобы они выстроились все лесенкой начиная с координаты (0,0) увеличивая обе на 3 с каждым новым спрайтом и получили при этом тайлы символов алфавита по порядку начиная с символа '0'. Кроме того каждый новый спрайт будет получать новый номер палитры циклически от 0 до 3.
Далее стартуем видеочип — при этом надо указать что спрайты находятся в той таблице тайлов где у нас расположены символы алфавита (флаг PPU_SPR_TBL_1000). В PPU_MASK включаем отображение спрайтов и левой полоски пикселей для них, об этом как раз поговорим ниже.
Флага PPU_SHOW_LEFT_BGR — т.е. левая полоска пикселей для фонов тут вообще быть не должно, но похоже что эмулятор встроенный в Nesicide сейчас его неправильно обрабатывает, поэтому здесь его лучше включить чтобы в нём всё выглядело правильно. Об этих вещах мы поговорим ниже.
Идём дальше — в игровой цикл:
main.s — третья часть
; ***************************
; * Основной цикл программы *
; ***************************
main_loop:
jsr wait_nmi ; ждём наступления VBlank
; Чтобы обновить таблицу спрайтов в видеочипе надо записать в OAM_ADDR ноль
store OAM_ADDR, # 0
; И активировать DMA записью верхнего байта адреса страницы с описаниями
store OAM_DMA, # >SPR_TBL
; ********************************************************
; * После работы с VRAM можно заняться другими вещами... *
; ********************************************************
jsr update_keys ; Обновим состояние кнопок опросив геймпады
; Теперь можно обновить спрайты в SPR_TBL
ldx cur_sprite ; Загрузим в X адрес (нижний байт) текущего спрайта
inx
inx ; увеличим X на 2, т.е. перейдём к полю SPR_ATTR
lda SPR_TBL, x ; Загрузим SPR_ATTR текущего спрайта в A
clc ; Перед сложением надо очистить Carry
adc # %01000000 ; Сложив с $01000000 мы будем циклически изменять
; все 4 варианта зеркалирования спрайта в верхих битах
sta SPR_TBL, x ; Сохраним полученные атрибуты спрайта обратно
inx
inx ; Увеличиваем X на 2 чтобы перейти с следующему спрайту
stx cur_sprite ; Сохраним указатель на новый текущий спрайт в cur_sprite
inc SPR_TBL + 4 * 62 + SPR_X ; в предпоследнем спрайте увеличим X
inc SPR_TBL + 4 * 63 + SPR_Y ; в последнем спрайте увеличим Y
jmp main_loop ; И уходим ждать нового VBlank в бесконечном цикле
.endproc
Здесь мы как и полагается ждём NMI и сразу же с помощью DMA заливает таблицу спрайтов в PPU. Просто? Очень. В OAM_DMA надо записать верхний байт адреса страницы в адресном пространстве консоли и DMA шустренько зальёт все байты из этой страницы в OAM_DATA.
А дальше мы обновляем таблицу спрайтов «бегущей» на каждом кадре волной: текущему спрайту в cur_sprite мы циклически изменяем вариант зеркалирования и переводим cur_sprite на следующий спрайт. В конце он сам провернётся в 0 и волна обновления побежит снова с начала.
Кроме того последним двум спрайтам в таблице увеличиваются координаты X и Y соответственно. Адреса полей с этими координатами высчитываются выражением во время компиляции SPR_TBL + 4 * i даёт начало данных спрайта с номером i в таблице и прибавляя смещения полей внутри мы получаем адрес нужного атрибута.
Смотрим что получилось:
Из-за того что верхние 8 строк не отображаются спрайт с номером 0 почти не видно, но иногда в процессе зеркалирования белая полоска пикселей от него всё-таки появляется скраю экрана. Это следствие того, что спрайты как бы сдвинуты на 1 пиксель вниз — имея координаты (0,0) в атрибутах по факту спрайт отображается как будто имеет координаты (0,1). Это верно для всех спрайтов.
Со спрайтом который двигается вдоль горизонтали можно заметить одну неприятность. Размер экрана по горизонтали в пикселях — 256. Но и координате спрайта по горизонтали выделен 1 байт, т.е. диапазон возможных значений тоже 0-255. Когда спрайт подходит к правой границе экрана он начинает скрываться за ней плавно. Но на левой границе он появляется сразу весь целиком и плавно скрыться за ней неспособен — здесь не существует отрицательных координат чтобы такое провернуть. В некоторых играх (например Contra) спрайты врагов действительно плавно выбежав из-за правого края экрана и пробежав по экрану налево сразу исчезают как только коснутся левой границы экрана — зная всё это сей фактик можно легко заметить. Но иногда художник видит спрайты плавно исчезающими как за правой так и за левой границами экрана и вот чтобы это сделать и появились два битовых флага в регистре PPU_MASK — PPU_SHOW_LEFT_BGR и PPU_SHOW_LEFT_SPR. Если они не включены, то 8 самых левых колонок пикселей экрана не рисуются для фонов и спрайтов соответственно и ценой исчезновения этой полоски можно сделать плавное появление спрайта из-за левой границы экрана.
Для координаты по вертикали аналогичного флага нет, т.к. восемь первых сканлайнов вообще не должны отображаються на экране телевизора. Более того — самый правильный способ скрыть неиспользованный спрайт — это вывести его за нижнюю границу экрана записав в его координату Y число из диапазона $EF-$FF, ведь даже если его попытаться скрыть назначив полностью прозрачный тайл, то он продолжает влиять на лимит спрайтов в строке и может этим помешать.
Еще стоит заметить, что память OAM в оригинальных PPU выполнена по технологии DRAM, т.е. реализована на динамической памяти которую надо обновлять периодически в так называемом процессе регенерации. Но автоматического регенератора у неё нет — если отображение спрайтов включено, то PPU считывая OAM каждый кадр как бы сам производит регенерацию. Но если отображение спрайтов выключить, то уже через пару кадров OAM начнёт деградировать и её содержимое будет портится, поэтому никогда не надо полагаться на то что спрайты в OAM сохранятся надолго выключая их рендеринг.
В первую часть (оглавление)...
0 комментариев