Программирование для 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
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 комментариев

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