Программирование для Famicom/NES/Денди в Nesicide+ca65: задний фон с прокруткой (4)

Итак, после создания модуля neslib который нам еще пригодится в будущем мы можем приступать к формированию основной программы — модуля 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 ниже)

Первым делом подключем заголовок neslib.inc чтобы иметь доступ ко всему что мы написали в нём и далее наполняем сегмент «VECTORS» векторами прерываний. Директива .addr эквивалентна директиве .word и добавляет в сегмент слово из двух байт. Если слово — это метка, а метки nmi, reset и irq будут созданы нами ниже (т.е. ассемблеру не обязательно иметь forward declaration в рамках текущего модуля чтобы использовать идентификатор), то они будут вставлены как соответствующие адреса. Сегмент VECTORS задан у нас в nes.ini так чтобы начинаться с адреса $FFFA, таким образом мы тремя директивами .addr заполнили адресами соответствующих процедур последние три слова в адресном пространстве. Для MOS 6502 это важно, т.к. здесь действительно должны лежать так называемые «вектора прерываний» и их три штуки: адрес (вектор) кода обрабатывающего немаскируемое прерывание (NMI), адрес кода откуда процессор начинает выполнение после включения или сброса и адрес обработчика маскируемого прерывания (IRQ) — именно в таком порядке как мы определили.
Думаю с вектором на метку reset всё понятно — это то откуда начнёт выполняться программа и там начнётся поток выполнения.
Прерывания чаще всего у нас наступают по сигналу от каких то внешних устройств. При этом выполнение программы прерывается, в стек сперва заталкивается PC, потом регистра флагов и далее происходит переход на адрес куда указывает соответствующий вектор. Когда код прерывания обработает запрос от внешнего устройства он возвращает поток выполнения основной программе инструкцией rti (return from interrupt), которая похожа на rts, но еще выталкивает из стека регистр флагов.
Маскируемое прерывание называется так потому что его можно запретить — инструкцией процессора SEI (а разрешить соответственно инструкцией CLI). Немаскируемое по наступлению всегда прерывает выполнение текущего потока кода, поэтому если его и можно запретить, то только конфигурируя через порты ввода-вывода те устройства которые его могут вызывать. Именно поэтому в процедуре warm_up первое что делается — это запрещаются все возможные прерывания записью в разные порты ввода-вывода — пока мы еще не готовы ничего обрабатывать.

Идём дальше:

.segment "ZPAGE": zp	; Сегмент zero page, это надо пометить через ": zp"
vblank_counter:	.byte 0	; Счётчик прерываний VBlank

.segment "RAM"		; Сегмент неинициализированных данных в RAM
ppu_ctrl_last:	.byte 0	; Последнее записанное в порт PPU_CTRL значение
xscroll:	.byte 0	; Скроллинг по X (в пределах базовой видеостраницы)
yscroll:	.byte 0	; Скроллинг по Y (в пределеах базовой видеостраницы)
update_addr:	.word 0	; Текущий адрес в VRAM бегущей строки обновления символов

.segment "ROM_L"	; Сегмент данных в ПЗУ картриджа (страницы $C000-$FFFF)
palettes:		; Подготовленные наборы палитр (для фона и для спрайтов)
	; Повторяем наборы 2 раза - первый для фона и второй для спрайтов
	.repeat 2	
	.byte $0F, $00, $10, $20	; Черный, серый, светло-серый, белый
	.byte $0F, $16, $1A, $11	; Чёрный, красный, зеленый, синий
	.byte $0F, $00, $10, $20	; Эта палитра и ниже здесь не используются
	.byte $0F, $00, $10, $20
	.endrep

Здесь мы в сегментах ZPAGE и RAM создаём несколько переменных:
  • счётчик кадров (о нём чуть ниже)
  • xscroll и yscroll — нижние 8 бит прокрутки видеостраниц по горизонтали и вертикали
  • ppu_ctrl_last — в последних двух битах порта PPU_CTRL хранятся по одному верхнему биту (сверх восьми) прокруток по горизонтали и вертикали (т.е. суммарно это 9-битные значения), но помимо этого там есть много других нужных бит, поэтому мы буферизируем нужные нам значения в PPU_CTRL в этой переменной, чтобы не портить текущие настройки прокруткой. Т.е. менять нужные биты прокрутки мы будем в ppu_ctrl_last и после всех обновлений будем получившееся значение записывать в PPU_CTRL.
  • update_addr — как мы видели в видео по экрану раскрашенному синим бежит процесс обновления символов увеличивающий код символа пробегающей сверху-вниз слева-направо волной. Это update_addr — адрес в видеопамяти который инкрементируется каждый кадр и увеличивает символ по новому адресу.
В сегменте ROM_L, т.е. нижних 16Кб ROM картриджа мы создаём набор палитр. Каждая палитра состоит из 4 байт каждый из которых описывает один из четырёх цветов в палитре. Для фона существует четыре палитры и для спрайтов существует четыре своих палитры, таким образом всего их восемь. Здесь через директиву .repeat я сделал данные для того и другого одинаковыми.
Четыре слота в каждой палитре соответствующие четырёхцветности всех тайлов хранят всего один байт — номер цвета. Откуда же он берётся?
Из глобальный палитры всех возможных в Famicom цветов, её можно посмотреть например здесь: wiki.nesdev.com/w/index.php/PPU_palettes

(номера даны конечно же в 16-ричной системе, как и задано в коде)
Т.е., например, в каждом спрайте задаётся какой тайл (битмап) он использует (0-255) и какую палитру он использует (0-3). Пиксели в тайле будучи двухбитными хранят абстрактные числа от 0 до 3 которые выбирают из палитры выбранной в спрайте лишь индекс в глобальной палитре цветов. И вот уже индекс в глобальной палитре порождает конкретный цвет.
В VRAM сперва хранятся четыре палитры фона, потом четыре палитры спрайтов начиная с адреса $3F00.
И очень важно еще заметить, что нулевой цвет актуален только для самой нулевой палитры фонов — это «глобальный цвет задника». На самом деле в самих тайлах как битмапах нулевой цвет всегда означает не нулевой цвет из палитры, а прозрачность! Это очень важно. Т.е. если бит какого то тайла нулевой (будь то фон или спрайт) — то он не отображается и за ним видно либо еще какой то тайл если он там есть, либо глобальный цвет задника, если ни один ненулевой пиксель не оказался над этим местом экрана. Нулевые цвета всех остальных семи палитр поэтому не играют никакой роли, а нулевой цвет первой палитры суть есть не цвет из этой палитры, а глобальный цвет задника.

А теперь вернёмся к прерываниям и их обработке. Дело в том, что перехватывать в Денди прерывания не только можно, но и нужно — и как правило самое важное для нас прерывание будет NMI — немаскируемое. И поступать оно будет от PPU в тот момент когда PPU заканчивает рисовать очередной кадр, начинает «отдыхать» (период VBlank) и в этот момент на короткое время можно будет писать/читать в память PPU и производить его настройку перед следующим кадром. Именно этот момент мы ловили в процедуре warm_up считывая в циклах верхний бит из порта PPU_STATUS. Как раз верхний бит этого регистра взводится в 1 когда PPU входит в VBlank. Однако этот бит еще и обнуляется когда мы считываем PPU_STATUS, т.е. считав 1 следующие чтения тут же будут возвращать 0. Это надо иметь ввиду. Кроме того существует вероятность гонки состояний когда считывание PPU_STATUS в первые 2 машинных цикла от начала периода VBlank возвращает 0 и при этом успевает сбросить флаг VBlank и NMI вообще не наступает! Это плохо, т.к. есть вероятность пропускать целые кадры, поэтому такая техника была допустима в момент инициализации консоли, но является нежелательной во время нормальной работы.
Поэтому существует другая более надёжная и пока еще простая возможность — мы разрешим NMI от PPU и будем в нём инкрементировать «счётчик кадров» — тот самый vblank_counter и если нужно будет дождаться VBlank, то просто будем ждать момента когда эта переменная сменит своё значение.

.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

vblank_counter достаточно сделать байтом в ZPAGE, т.к. само число наступивших периодов VBlank и их количество нам неважны — важно лишь ловить момент наступления каждого следующего. Для простых схем этого достаточно, но более совершенный код будет сложнее обрабатывать NMI.


; 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_colors:
	sta PPU_DATA		; записываем в VRAM аккумулятор
	dex			; декрементируем X
	bne loop_colors		; цикл по счётчику в X
	rts			; возврат из процедуры
.endproc


Мы разместили данные для палитр в ПЗУ, но чтобы они вступили в силу и начали окрашивать своими цветами пиксели тайлов надо сперва загрузить их в PPU. Палитры находятся в памяти PPU, т.е. в VRAM по адресу 3F00.
Чтобы писать что-то в память PPU надо сперва двумя последовательными записями выставить в регистре PPU_ADDR нужный адрес (первый записываемый байт будет старшим), а потом записывать последовательно байты в адрес PPU_DATA. После каждой записи в PPU_DATA адрес в PPU_ADDR будет автоматически увеличиваться на 1, но если в зажжён бит PPU_ADDR_INC32 в регистре PPU_CTRL — то на 32. Последнее удобно чтобы заливать данные в видеостраницы поколоночно.

Процедура fill_palettes первый пример у нас сразу по нескольким статьям. Во первых в неё передаётся параметр-адрес в той самой области временных переменных в zero page — это arg0w в котором должен быть передан адрес таблицы палитр (у нас тут в ПЗУ).
Во вторых она пишет в VRAM и использует для выставления конкретного адреса (адреса палитр в VRAM) макрос fill_ppu_addr.
Еще она использует один из сложных косвенных режимов адресации — когда адрес в zero page (как раз arg0w) сложенный с Y даёт адрес откуда загружается байт. Здесь нам это весьма удобно. Вообще в боевых условиях такая процедура не очень удачна в силу неоптимизированности, но пока я бы как раз хотел избегать связанных с оптимизациями сложностей. Как пример пойдёт.

Процедура fill_attribs — это участок кода который будет использован ниже два раза, поэтому его логично было выделить в процедуру. Всё что она делает — это заливает в PPU_DATA (т.е. в VRAM) 64 раза значение аккумулятора. Это будет заливка цветовых атрибутов двух наших экранных областей, память под которые существует в Famicom, поэтому код предполагает, что PPU_ADDR уже хранит нужное значение и в аккумуляторе находится байт с цветовыми атрибутами.

Итак, мы подошли к началу всей программы — точке входа «reset»:

; 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
	
	; **************************************************************************
	; * Зальём первую видеостраницу расходящимся веером символов начиная с '0' *
	; **************************************************************************
	fill_ppu_addr $2000	; Будем писать в видеостраницу $2000
	; Нам нужны переменные текущей строки (cur_y) и текущего столбца (cur_x)
	; сиволов экрана - для самоописуемости назначаем их как синонимы ячеек
	; памяти в локальных переменных zero page.
cur_x	= arg0b			; текущий столбец (0-31)
cur_y	= arg1b			; текущая строка (0-29) (всего 32*30 символов/тайлов)
	store cur_x, # 0	; cur_x = 0
	store cur_y, # 0	; cur_y = 0
loop_fill:
	lda cur_x		; Сравниваем loc_x (помещённый в A)
	cmp cur_y		; с loc_y и если loc_y > loc_x, то
	bcc cur_y_bigger	; флас Carry будет 0 и тогда мы пропустим
	lda cur_y		; загрузку loc_y в A, т.е. A = min( loc_x, loc_y )
cur_y_bigger:
	clc			; Перед сложением надо сбросить Carry
	adc # $30		; $30 - это символ '0', сложение A даст расходящийся 
				; веер символов вдоль осей.
	sta PPU_DATA		; Сохраним итоговый символ в VRAM
	inc cur_x		; Увеличиваем loc_x
	lda # 32		; и сравниваем его
	cmp cur_x		; с 32 (концом столбцов в текущей строке)
	bne loop_fill		; и если не равны, то новая итерация.
	store cur_x, # 0	; Обнуляем loc_x (сброс итераций по этой переменной)
	inc cur_y		; и увеличиваем loc_y
	lda # 30		; сравнивая его с
	cmp cur_y		; 30 (концом строк)
	bne loop_fill		; и если конец не достигнут - новая итерация.
	
	lda # 0			; атрибуты цвета в палитру 0
	jsr fill_attribs	; заполним оставшуюся область цветовых атрибутов

	; *************************************************************************
	; * Зальём вторую видеостраницу нарастающими по алфавиту символами текста *
	; *************************************************************************
	fill_ppu_addr $2400	; настроим PPU_ADDR на $2400
	ldy # $20		; $20 - это ASCII код пробела в таблице символов
				; и в тестом банке CHR он же номер тайла пробела
	ldx # 32 * 30 / 4	; нужно залить 32*30 тайлов, но чтобы счётчик цикла
				; влез в байт будем сразу лить порциями по 4 тайла
loop_tiles:
	.repeat 4		; код между .repeat и .endrep размножится 4 раза подряд...
	sty PPU_DATA		; сохраняем код символа в VRAM
	iny			; и переходим к следующему символу
	.endrep
	cpy # 80		; сравниваем с концом таблицы символов
	bne loop_skip		; и если конец символов не достигнут, то пропускаем...
	ldy # $20		; сброс опять на ASCII-код пробела
loop_skip:
	dex			; декрементируем счётчик
	bne loop_tiles		; и если он не ноль, то повторяем цикл

	lda # %01010101		; атрибуты цвета в палитру 1
	jsr fill_attribs	; заполним область цветовых атрибутов

Тут надо заметить, что после выхода из warm_up PPU находится в «выключенном» состоянии — значит он не генерирует никакого сигнала кроме выдачи глобального цвета задника, поэтому доступ к VRAM через PPU_ADDR и PPU_DATA доступны без проблем. Этим мы тут активно пользуемся.
Сперва заливаем в VRAM палитры. Потом инициализируем первую видеостраницу так что она оказывается заполнена расходящимся «веером» символов, где хорошо видно границы. Причём заливка цветовых атрибутов идёт сразу же вызовом как раз fill_attribs и это нулевой номер палитры который перед вызовом загружаем в A. Но тут не всё так просто как просто ноль, о чём чуть ниже.
Следом заполняем просто рядом постоянно нарастающих слева-направо символов вторую видеостраницу, но вот её цветовые атрибуты зальём уже номером палитры 1.
Это двоичное число %01010101. Тут уже надо вспоминать как устроена таблица атрибутов в каждой видеостранице: wiki.nesdev.com/w/index.php/PPU_attribute_tables
Каждый байт в таблице атрибутов описывает какие будут палитры у целых 16 тайлов в экранной области (блок 4x4)! Только так 64 байта таблицы атрибутов хватает чтобы покрыть все 32x30 тайлов. Каждый блок 4x4 в свою очередь побит на 4 меньших блока размером 2x2 тайла.
Так что если блок 4x4 представить как следующий набор из блоков 2x2 (здесь каждая AA — это первый блок 2x2, BB — второй и т.д):

AABB
CCDD

, то битовая маска байта цветового атрибута имеет следующий шаблон: %DDCCBBAA, т.е. два бита CC описывают одну из четырёх палитр нижнего-левого блока «CC». И каждый такой блок в свою очередь это 4 тайла 2x2. Т.е. каждый тайл в Famicom не может иметь независимую от соседей палитру. Из-за этого (да и много из-за чего еще) очень популярно разбитие уровней на метатайлы как раз по 2x2 или даже 4x4 тайла.
Таким образом когда мы хотим закрасить все тайлы второй видеостраницы палитрой где буквы будут синие (палитра %01) мы заливаем в неё 64 байта с двоичным числом %01010101.

	; **********************************************
	; * Стартуем видеочип и запускаем все процессы *
	; **********************************************
	; Включим отображение заднего фона (отложим в ppu_ctrl_last)
	store ppu_ctrl_last, # PPU_VBLANK_NMI | PPU_BGR_TBL_1000
	; Обновив ppu_ctrl_last теперь записываем его в PPU_CTRL для применения
	store PPU_CTRL, ppu_ctrl_last
	; Включим отображение заднего фона и левой колонки его пикселей на экране
	store PPU_MASK, # PPU_SHOW_BGR | PPU_SHOW_LEFT_BGR
	cli			; Разрешаем прерывания
	
	store xscroll, # 0	; скроллинг по X и Y инициализируем в 0
	store yscroll, # 0
	store_addr update_addr, $2400	; начальный адрес инкрементации VRAM

Наконец то мы готовы активировать PPU и начать «игровой цикл».
В ppu_ctrl_last заливаем флаги активирующие VBlank NMI и бит говорящий, что таблица тайлов для заднего фона — вторая из двух таблиц тайлов в Characters. Мы действительно видели в редакторе что она вторая по счёту.
Чтобы сразу же применить — записываем ppu_ctrl_last в PPU_CTRL.
Далее в PPU_MASK надо активировать отображение заднего фона — это битовый флаг PPU_SHOW_BGR.
Интересен еще один добавленный флаг — PPU_SHOW_LEFT_BGR — без него PPU будет подавлять вывод слева всего экрана полоски фона из 8 пикселей в ширину. Мы активируем фон «в полную ширину». К обсуждению того зачем этот странный флаг вообще нужен мы вернёмся в статье про спрайты.
Далее разрешаем прерывания и инициализируем параметры скроллинга начальными значениями.

Следом идёт начало игрового цикла:

	; ***************************
	; * Основной цикл программы *
	; ***************************
main_loop:
	jsr wait_nmi		; ждём наступления VBlank

	; ********************************************************************************
	; * Каждый кадр увеличим символ во второй видеостранице и переходим к следующему *
	; ********************************************************************************
	; выставим в PPU_ADDR адрес update_addr для считывания номера тайла
	store PPU_ADDR, update_addr + 1	; сперва старший байт
	store PPU_ADDR, update_addr + 0
	; (!) Заметьте, что выше мы не могли использовать fill_ppu_addr update_addr, т.к.
	; надо чётко понимать разницу между адресом и значением которое по адресу лежит.
	ldx PPU_DATA		; первое чтение из PPU_DATA после смены PPU_ADDR надо игнорировать
	ldx PPU_DATA		; читаем номер тайла/символа в X
	inx			; увеличиваем его
	cpx # $80		; проверяем на выход за максимальный символ (ASCII $80)
	bne skip_x20		; если не вышел - идём дальше
	ldx # $20		; иначе откатываем символ в пробел
skip_x20:			; снова выставляем адрес PPU_DATA т.к. он ушёл вперёд
	store PPU_ADDR, update_addr + 1
	store PPU_ADDR, update_addr + 0
	stx PPU_DATA		; сохраняем инкрементированный символ в VRAM
	
	inc update_addr + 0	; увеличиваем младший байт адреса update_addr
	bne skip_inc_high	; если он не обнулился, то пропускаем
	inc update_addr + 1	; увеличение старшего адреса update_addr
skip_inc_high:
	ldx # $C0		; чтобы проверить не равен ли update_addr адресу $27C0
	cpx update_addr + 0	; сперва сверяем нижний его байт с $C0
	bne end_of_updater	; если не равно - идём дальше
	ldx # $27		; иначе сверяем верхний байт с $27
	cpx update_addr + 1	
	bne end_of_updater	; и если не равно - идём дальше
	; если же update_addr стал равен $27C0 (конец тайлов экрана), то сбрасываем его в начало
	store_addr update_addr, $2400
end_of_updater:

В начале игрового цила — main_loop мы ждём наступления периода VBlank — после этого мы должны как можно быстрее залить данные в PPU.
Сперва идёт инкремент символа по адресу VRAM в update_addr и инкремент update_addr. Заметьте, что т.к. это 16-битное значение, то он инкрементируется «частями» с проверками на переполнение каждый раз.

	; *************************************************************
	; * Если нажат SELECT, то сбросим параметры прокрутки в (0,0) *
	; *************************************************************
	; С помощью макроса skip_if_key1_not пропускаем куски кода
	; если не нажата соответствующая кнопка. Здесь - KEY_SELECT
	jump_if_keys1_is_not_down KEY_SELECT, skip_scroll_reset
	store xscroll, # 0	; Занулили параметры прокрутки
	store yscroll, # 0	; в пределах текущего экрана...
	lda ppu_ctrl_last	; А верхние биты прокрутки (или они же - выбор
	and # %11111100		; текущего экрана из четырёх) надо занулить в PPU_CTRL
	sta ppu_ctrl_last	; для чего сбросим их битовой операцией в ppu_ctrl_last.
skip_scroll_reset:

Тут проверяется нажатие кнопки SELECT и мы видим как для этого использовать макрос jucmp_if_key1_is_no_down. С помощью него мы пропускаем блок кода если кнопка не нажата. Если же она нажата, то сбрасываются параметры скроллинга. Т.к. верхние биты сдвига экранов находятся в PPU_CTRL, то надо обнулить их в ppu_ctrl_last.

	; *************************************************************
	; * Скроллимся в соответствии с нажатыми кнопками направлений *
	; *************************************************************
swap_x	= arg0b	; Сделаем псевдонимы для переменных: нужно ли изменить верхние биты 
swap_y	= arg1b	; параметров прокрутки по X и Y в ppu_ctrl_last после скроллинга
	store swap_x, # 0	; Верхний бит swap_x будет флагом по X
	store swap_y, # 0	; Верхний бит swap_y будет флагом по Y
	; Если нажата кнопка ВЛЕВО, то надо уменьшить скролл по X
	jump_if_keys1_is_not_down KEY_LEFT, skip_l	; Идём дальше если кнопка не нажата
	dec xscroll		; пока просто уменьшаем xscroll
	lda # $FF		; и проверяем не провернулся ли он через 0
	cmp xscroll		; и не стал ли тогда $FF
	bne skip_l		; если нет, то идём дальше
	store swap_x, # $80	; а если да, то взводим флаг проворота экрана по X
skip_l:	; Если нажата кнопка ВПРАВО, то надо увеличить скролл по X
	jump_if_keys1_is_not_down KEY_RIGHT, skip_r	; Идём дальше если кнопка не нажата
	inc xscroll		; Увеличим xscroll
	bne skip_r		; Сразу можно тестировать на 0 и если он им не стал, то идём дальше
	store swap_x, # $80	; Иначе значит он "провернулся" из $FF->0 и надо взвести флаг по X
skip_r:	; Если нажата кнопка ВНИЗ, то надо увеличить скролл по Y
	jump_if_keys1_is_not_down KEY_DOWN, skip_d
	inc yscroll		; Увеличим yscroll
	lda # 240		; и проверям не стал ли он равен 240
	cmp yscroll
	bne skip_d		; если нет, то идём дальше
	store yscroll, # 0	; а если да, то надо обнулить yscroll
	store swap_y, # $80	; И взвести флаг проворота экрана по Y
skip_d:	; Если нажата кнопка ВВЕРХ, то надо уменьшить скролл по Y
	jump_if_keys1_is_not_down KEY_UP, skip_u
	dec yscroll		; Уменьшим yscroll
	lda # $FF		; И проверяем не провернулся ли он через 0
	cmp yscroll		; и не стал равным $FF
	bne skip_u		; если нет, то идём дальше
	store yscroll, # 239	; иначе загружаем в yscroll 239
	store swap_y, # $80	; и взводим флаг необходимости проворота экрана по Y
skip_u:	; Теперь можно применить флаги проворота экрана по X и Y если они взведены
	lda ppu_ctrl_last	; Нижние 2 бита ppu_ctrl_last - это то что нам возможно надо поменять
	bit swap_x		; Тестируем зажжён ли старший бит в swap_x
	bpl skip_inv_x		; если нет, то идём дальше (флаг не взведён)
	eor # %001		; иначе инвертируем (через XOR) 0-ой бит ppu_ctrl_last в A
skip_inv_x:	
	bit swap_y		; Тестируем зажжён ли старший бит в swap_y
	bpl skip_inv_y		; если нет, то идём дальше
	eor # %010		; иначе инвертируем (через XOR) 1-ый бит ppu_ctrl_last в A
skip_inv_y:
	sta ppu_ctrl_last	; Сохраняем возможно изменённый ppu_ctrl_last из аккумулятора

Далее проверяем нажатие кнопок направлений и увеличиваем или уменьшаем параметры скроллинга в заданном направлении. Если байтовые x/y_scroll проворачиваются через границы, то взводим флаги того, что надо обновить их девятый бит в ppu_ctrl_last и в конце этого блока кода делаем это.
И остаётся последний кусок кода:

	; **************************************************
	; * Применяем все накопленные параметры скроллинга *
	; **************************************************
	store PPU_CTRL, ppu_ctrl_last	; Загружаем состояние PPU из ppu_ctrl_last
	store PPU_SCROLL, xscroll	; Обновляем параметры скроллинга
	store PPU_SCROLL, yscroll

	; ************************************************************
	; * После работы с VRAM можно заняться другими вещами        *
	; * чтобы не занимать ценное время VBlank ничем кроме этого. *
	; * Теперь можно, например, обновить состояние кнопок.       *
	; ************************************************************
	jsr update_keys		; Обновим состояние кнопок опросив геймпады
	
	jmp main_loop		; И уходим ждать нового VBlank в бесконечном цикле
.endproc

(опять таки всё что находится между jsr wait_nmi и jmp main_loop — специфично для нашего приложения)
Прежде всего он загружает в PPU_CTRL и PPU_SCROLL накопившиеся изменения.
Регистр PPU_SCROLL как и PPU_ADDR «двойной». В него два раза подряд надо записать два байтовых значения: величину прокрутки по X и по Y соответственно (значение по Y не должно быть больше 239!). Но важны еще два нижних бита в PPU_CTRL которые управляют в какой из четырёх экранных областей находится базис скроллинга. Их еще можно трактовать как девятые биты значений в PPU_SCROLL.
Есть еще одно важное замечание: запись в регистр PPU_SCROLL портит значение в регистре PPU_ADDR, поэтому должно быть сделано после всех обновлений VRAM. Почитать статью о том как эти регистры изнутри связаны и взглянуть в полную глубину скроллинга на денди можно тут. Эта статья с одной стороны может сейчас многое прояснить, а с другой даже запутать. Попробуйте прочитать, но если будет тяжело — просто возвращайтесь сюда.
В конце всего просто отправляется на новый круг.

Вот теперь, после всех внесённых изменений сохраните код и нажимайте F5-F6-F7 чтобы увидеть результат.



Ах и да, пришло время настроить кнопки эмулятора в меню Emulator->Setup->Controllers иначе вы не сможете скроллить.
Всегда еще желательно иметь под рукой другой эмулятор, например, FCEUX, чтобы тестировать получающееся в альтернативных эмуляторах. Мы действительно заметим важную разницу, но про это будет уже в следующей статье.

В первую часть (оглавление)...

0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.