[Программирование, Assembler] Программирование NES (dendy), assembler 6502

Автор Сообщение
news_bot ®

Стаж: 6 лет 7 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
08-Апр-2021 19:32

У меня с детства была мечта написать игру для любимой приставки денди, шло время, мечта то появлялась то затихала снова. Она меня направляла в сторону магии программирование, и вот прошло больше 20-ти лет я программист, и снова в который раз пытаюсь постигнуть магию той самой денди что так будоражило моё воображение в тяжелом но счастливом детстве. Сегодня Я расскажу вам как постиг секреты этой магии, наконец то смог вывести спрайты на экран и научился рисовать фон.
Я php программист и по этому довольно далек от низкоуровневого программирования, по этому мне приходилось довольно долго изучать сам assembler но он оказался проще чем я думал, оказалось надо просто освободить голову от стереотипов современного программирования и смотреть на assembler как на инструмент передачи информации в некое api черный ящик. Так вот этим черным ящиком и является для нас Dendy (или NES, Famicom), так вот у этого ящика есть некие области памяти, одни предназначены только для чтения, другие для записи, а третьи и для чтения и для записи. Приставка воспроизводя игру использует несколько ключевых подсистем (процессоров):
1. CPU - центральный процессор
2. PPU - Графический процессор
3. APU - Аудио процессор Сегодня, Я хотел бы затронуть тему работы с PPU ибо это наверное ключевое для многих людей графическое представление данных. Для начала разберем из чего состоит картридж, важными в картридже являются 2 чипа с памятью один это CHR (тут храниться битовая карта спрайтов и фона) имеет два банка памяти. Битовая карта представляет из себя набор 0-й и 1-к, размером 8х8, один спрайт состоит из 2-х таких областей. Пример ниже.
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 1 0 0 + 1 1 1 1 1 1 1 1
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
При сложение этих двух банков они накладываются друг на друга и определяется цвет пикселя, таких цвета может быть 3-и для спрайта:
  • 0 + 0 = 0 - прозрачный цвет
  • 0 + 1 = 1 - первый цвет
  • 1 + 1 = 2 - второй цвет
  • 1 + 0 = 3 - третий цвет
Из этого примера и вытекают следующее умозаключение, что у background 4 цвета в палитре, а у спрайта всего 3-три. Потому что фон не имеет прозрачного цвета, 1-й цвет у него цвет фона. Давайте перейдем к ассемблеру, у нес есть несколько аккумуляторов, их можно сравнить с переменными это A, X, Y. X, Y они имеют косвенное отношение к координатам. Чаще всего мы будем пользоваться аккумулятором A.
LDA {argument} - Загрузить в A значение argument (десятичное #20, hex #$20, двоичное %00010010)
STA {address} - Сохранить A в память (адрес ppu для примера)
Команды с аккумулятором X и Y
LDX {argument} - Загрузить аргумент в X
LDY {argument}
STX {address} - Сохранить X
STY {address} - Сохранить Y
INX - increment X
INY - increment Y
CPX {argument} - Сравнить x с аргументом
CPY {argument} - Сравнить y с аргументом
Команды ветвления:
JMP {address} - переход к адресу
BEQ {address} - перейти к адресу если CPX true
BNQ {address} - перейти если условие в CPX не равно
С первоначальными командами разобрались, дальше надо разобраться с двумя основными вопросами:
  • Как отобразить фон?
  • Как нарисовать спрайт?
Отображение фонаРазрешение NES, В системе PAL 256x240, в системе NTSC 256x224. Фон состоит из спрайтов 8х8, соответственно 32х30 спрайтов в PAL, в NTSC верхний ряд спрайтов и нижний обрезаются. Для того что бы отобразить фон, необходимо 3 условия: палитра, nametable (таблица имен), таблица атрибутов.
  • Палитра - палитра для фона содержит 16 цветов 4 вариации по 4 цвета. Как и палитра спрайтов собственно.
  • Nametable - таблица 32х30 спрайтов, которая заполнена для отображения спрайтов на одном экране, таких таблиц может быть 2 без дополнительной оперативной памяти, если картридж содержит дополнительную память в размере 2КБайт то таких таблиц может быть 4-ре процессор поддерживает 4 таблицы. Но из за ограничения по памяти их две из коробки, в денди существует функционал зеркального отображения фона, который мы разберем в следующих статьях.
  • Таблица атрибутов - в nes экран делиться на сетку с размером ячейки 32х32 или 2х2 спрайта, 8х8 ячеек, каждая ячейка содержит 1 байт битовой маски, %00000000. Что бы понять как она формируется давайте рассмотрим ячейку спрайтов 2 на 2 спрайта.
    -----------------
    | 0 | 1 |
    -----------------
    | 2 | 3 |
    -----------------
    Я специально в разные ячейки поставил разные цифры, эти цифры означают порядковый номер политры, который будет применен к спрайту фона. Так вот каждые 2 бита маски обозначают какую палитру применить к спрайту в ячейке:
00 - 0-вая последовательность 4-х цветов
01 - 1-я
11 - 2-я
10 - 3-я
Немного отвлечемся и поговорим о принципе программирование на nesДля того что бы отобразить к примеру спрайт, фон, или сделать скролинг экрана необходимо просто записать в соответствующий адрес данные, к примеру для прокрутки экрана по оси X нужно просто в адрес PPU $2005 записать сначала X координату потом Y, подчеркну последовательно.
LDA #25 ; десятичное значение X координаты
STA $2005 ; записываем #25 в адрес
LDA #00 ; 0 по оси Y
STA $2005 ; записываем #0 в адрес
Разберем другой пример рисование спрайта он довольно прост
LDA #100     ; координата Y спрайта
STA $2004   ; записываем в адрес OEM data PPU
LDA #00     ; номер спрайта в таблице тайлов (по сути таил и есть таблица спрайтов)
STA $2004   ; записываем в адрес
LDA #%00010111  ; маска которая определяет отражение спрайта по вертикали
                ; и горизонтали, а так же номер палитры
STA $2004   ; сохраняем в адрес
LDA #192    ; загружаем Y координату
STA $2004   ; и снова записываем
Такая последовательность команд выведет спрайт 8х8 в скриншоте таких спрайтов 6. Теперь перейдем к более сложному, допустим мы определили что у нас есть таблица имен адресное пространство таблицы от $2000 - $2400 видео памяти, вторая таблица $2400 - $2800 и так далее. Для начала наша задача записать в данную ячейку памяти данные nametable. Для этого нам надо просто
LDA #$20 ; старший байт
STA $2006 ; регистр 2байта тут мы указываем какой участок памяти мы бы хотели использовать
LDA #$00 ; младший байт
STA $2006
Тут мы снова видим принцип обычной последовательности записи, сначала X потом Y, либо адрес памяти в которую хотим произвести запись данных.
LDA $24 ; номер тайла или спрайта в банке CHR
STA $2007 ; регистр для записи данных
; далее возможно записывать каждый спрайт в таблицу имен отдельно
Конечно ни кто не записывает руками данные а использует циклы, как и Я, приведу простой пример цикла на ассемблере
LDX #00 ; загружаем 0 в x значение decimal так проще чем #$00 - #$FF к примеру
section:
  ; какие то действия загрузка тех же спрайтов
  LDA $24
  STA $2007
  CPX #255
  INX
  BNE section
Код секции выполниться 255 раз и заполнит 255 значений таблице имен, это как пример. Для заполнения всех значений необходимо использовать аккумулятор Y и пройтись 4 раза таким циклом. Далее используя ту же логику мы можем загрузить палитру, палитра в памяти храниться с адреса $3F00-$3F0F - 16 байт соответственно. Используя предыдущую логику загрузки nametable нам необходимо сделать следующее
LDA #$3F ; старший байт адреса $3F
STA $2006
LDA #$00 ; младший байт
STA $2006
LDA $22 ; цвет палитры nes синий
STA $2007 ; записываем цвет в адресс $3F00
LDA $05 ; красный
STA $2007 ; записываем цвет в адресс $3F01
; и так далее еще 14 раз
Опять же палитру можно определить как последовательность байт и загрузить ее циклом который пробежится 16 раз или 16 байт
background_pallete:
  .byt $01, $02, $03, $04
  .byt $05, $22, $23, $24
  .byt $25, $26, $27, $28
  .byt $31, $32, $33, $34
   LDX #$00 ; x равен 0
   LDA #$3F ; старший байт адреса $3F
  STA $2006
  LDA #$00 ; младший байт
  STA $2006
loadPallete:
   LDA background_pallete, x ; background_pallete ссылается на адресс
                             ; в котором храниться байт , x добавляет +1 к адресу
   STA $2007 ; записываем данные
   CPX #$0F ; сравниваем X с 16
   INX ; X = X+1
   BNE loadPallete ; повторяем до тех пор пока X не будет 16
                   ; Тогда будет загруженны все цвета палитры в видеопамять
Осталось разобраться с тем как загрузить атрибуты фона, это тоже довольно просто атрибуты хранятся в адресном пространстве видеопамяти начиная с $23C0 - $23FF каждый ряд начинается с $23C0, $23C8 и так далее то есть 8 рядов по 8 столбцов.Теперь нам известны область памяти для применения определенной палитры для спрайта фона, так что, что бы ее применить нам необходимо, снова, очень просто сказать в какую ячейку памяти мы бы хотели записать нашу маску, загрузить ее в аккумулятор А и записать в регистр $2007
LDA #$23
STA $2006
LDA #$C2
STA $2006 ; получаем адрес $23C2
LDA %00001100 ; загружаем нашу маску атрибута, как ее вычислить я писал выше
STA $2007 ; записываем в регистр
Таким образом отображение фона превращается в довольно тривиальную задачу, результатом 2-х недель изучения темы асемблеры вы видели на скриншоте в заголовке статьи. На самом деле assembler nes не такой сложный если сломать стереотипы программирования которыми мы сейчас пользуемся, и взглянуть на все это проще, когда наступает момент понимание того что, асемблером возможно обратиться в доступный участок памяти записать туда некоторую информацию, которая потом волшебным образом отобразиться на экране. Это маленькая победа над собой, маленький шаг к мечте. Спасибо всем за внимание и до скорых встреч.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_assembler, #_nes, #_programmirovanie (программирование), #_assembler, #_6502, #_dendy, #_famicom, #_programmirovanie (
Программирование
)
, #_assembler
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Сен 14:25
Часовой пояс: UTC + 5