Начнём с разрешения nes (dendy) в PAL и NTSC они разные но возьмем разрешение в PAL 256/240 пикселя. В денди есть структура NameTable которая заполняется спрайтами background'а 2 банка памяти chr. NameTable заполняется sprit'ами размером 8x8 пикселей. Выходит что 256 пикселей это 32 тайла в ширину. И 240 пикселей это 30 тайлов. И так теперь у нас есть разрешение экрана 32 плитки на 30 плиток. Эту плитку можем представить как матрицу, в которой вписан каждый тайл и если значение $00 то данный спрайт не вызывает коллизии, а если $FF то коллизия произошла. Но если 32*30 то мы получим 960 байт, в этом случае будет сложно "вытащить" нужный байт, так как 8 бит это 255 в пиковом случае, то есть 11111111 или $FF. Значит как мы можем это оптимизировать, мы можем представить матрицу из 1 и 0. Итого в 1 байте = 8 бит. Если мы разделим 32/8 = 4 то получим 4 байта. С 30 строками по Y мы получаем 30*4 = 120 байт. В итоге получаем структуру:

Структура карты коллизий
collisionMap:
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %11111111, %11111111, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %11111111, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000011, %11111111, %11111111, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 
  .byt %00000000, %00000000, %00000000, %00000000 

Структуру определили теперь надо сделать несколько шагов:

  • Вычислить позицию X (в переводе на тайлы) которая будет указывать на разряд в байте, разряд имею ввиду определённую цифру на конкретной позиции, к примеру X = 10 значит надо взять 2 байт, и посмотреть 2-ю цифру в этом байте.

  • Вычислить позицию Y который укажет на ряд в котором находиться байт

  • Вычислить порядковый номер байта Y*X по сути.

В последнем пункте всплывает проблема что сделать умножение на ассемблере 6502 довольно сложно, можно сделать цикл и складывать числа, но это не оптимально. Но мы можем умножать и делить на 2-ва. Просто смещая биты числа вправо или влево соответственно. К примеру число 100 в бинарном виде будет выглядеть так 01100100 а 50 будет такой 00110010 как мы видим при смещение в право мы делим число на 2, а вот к примеру, если взять число 200 то в битах оно будет 11001000 то есть если бинарные обозначение числа 100 сдвинуть на 1 бит влево то мы получим умножение на 2.

Как вычислить позицию байта в таблице. Нам необходимо привести некое 10-чное значение координаты, пусть она будет 151 пиксель (далее будем использовать это значение в качестве примера) нам необходимо разделить это значение на 8 количество пикселей в 1-м спрайте и потом ещё на 8 количество байт, то есть необходимо координату x разделить на 64. Таких чисел может быть всего 3-ри (0, 1, 2, 3). В результате, если 6 раз сдвинуть биты в числе мы это число разделим на 2^6 (два в степени 6 то соответствуют 64)

Код деления на 64
lda xCoordinate ; xCoordinate - переменная в zeropage
lsr ; сдвиг в право на один бит делим на 2
lsr ; делим на (2*2=4)
lsr ; делим на 8
lsr ; делим на 16
lsr ; 32
lsr ; сдвинули 6 раз, в результате разделили на 64
sta tmpX ; tmpx = 2

Следующий шаг самый сложный, необходимо вычислить X*Y где Y координата тайла в таблице то есть это Y = yCoordinate/8, 8 это 2 в степени 3, по этому смещаем на 3 бита координату Y. К примеру yCoordinate = 121.

Делим Y на 8
lda yCoordinate
lsr
lsr
lsr ; здесь в акумуляторе число 15

Теперь необходимо X*Y что бы получить позицию байта в таблице (условной конечно же, они не хранятся таблицей), и тут решение довольно простое, мы можем умножить строку которая получилась на 4 а потом прибавить просто позицию X. 4 в данном случае количество байт в строке 4 (количество байт) * 8 (количество бит) * 8 (количество пикселей в тайле) = 256 это как раз разрешение nes. 4 это 2^2 а значит нам необходимо получившееся выше число сдвинуть 2 раза в лево и прибавить X который мы получили выше.

Умножаем Y на 4 и складываем с X
asl ; сдвиг в лево
asl
adc tmpX ; сложение
tay ; содержимое A в Y переносим

Остаётся ещё один шаг вычисление позиции бита в найденном байте, мы уже знаем что в одном байте 8 бит, размер спрайта то же 8х8 пикселей, и ранее мы делили X на 8 что бы получить позицию тайла (по сути бита), потом еще раз на 8 что бы получить позицию байта. При этом неизбежно была округлена дробная часть числа которая и содержит указание на тот бит который необходимо сравнить. Возвращаюсь к примеру с числом 151 на 64 оно целым не делиться, в результате деление будет число 2.35.... то есть мы понимаем что мы взяли 2 байта, буквально 00000000 00000000 ...и тут ещё некое количество битов... Вычислить это количество бит довольно легко, необходимо лишь взять число 8 в бинарном виде (а точнее 7 потому как координаты с 0 считаются) а это 00000111 и число 151 в бинарном виде это 0010010111 отсеять лишний "хвост" с помощью логического "И', в результате 0 и 0 = 0, 0 и 1 = 0, 1 и 1 = 1, мы получаем 00000111 7 пиксель. Далее представим что число стало 150 то в бинарном виде оно будет 0010010110 то есть 00000110 что соответствует 6.

Остаётся ещё один момент, что бы вычислить бит нам надо создать маску битов из 8 байт

Создаем такую маску с 8 байтами

bitMask:
  .byt %10000000 ; 1
  .byt %01000000 ; 2
  .byt %00100000 ; 3
  .byt %00010000 ; 4
  .byt %00001000 ; 5
  .byt %00000100 ; 6
  .byt %00000010 ; 7
  .byt %00000001 ; 8

Далее берем индекс байта который мы вычислили Y*4+X. И индекс бита когда мы применяли маску 00000111 с помощью логического "И"

Вычисляем позицию бита и смотрим 1 или 0?

  lda collisionMap, y ; берём байт
  and bitMask, x ; берем байт с позицией бита X
  beq gravityIncrement ; нет колизии
  bne gravityStopIncrement ; колизия была вычеслена

Как это работает при операции AND в асемблере, она влияет на флаг Z, Я пока точно не уверен в том как команды AND, EOR влияют на Z, то есть в какой момент он 1 а в какой он 0, пока предполагаю что когда после AND 0 то Z=1 если после AND результат > 0 то Z=0, но могу ошибаться. Буду благодарен если кто то в комментариях разъяснит этот момент.

В качестве вывода, программирование на NES (Asembler 6502) с каждым днём открывает для меня новый мир. Сегодня мы научились определять столкновение с платформой, в следующих статьях Я хотел бы затронуть тему флагов, врагов, столкновение с врагами.

Комментарии (9)


  1. igrishaev
    13.09.2021 15:07
    +5

    Пара слов о пунктуации: ее нет.


  1. lizergil
    13.09.2021 15:47

    Интересно ознакомиться. А есть что-то полноценное более высоко-уровневое для nes на рынке?


    1. tvoybro
      13.09.2021 17:26

      https://github.com/cc65/cc65


    1. shiru8bit
      13.09.2021 17:26
      +1

      Есть компилятор C, древний, не самый оптимальный, но достаточно развитый и стабильный - cc65. Есть ряд других затей типа Python и LISP, но они едва ли выходят за рамки proof of concept. Также есть выскоуровневые ассемблеры с дополнительными конструкциями, предположительно упрощающими их использование, но особой популярности они не имеют.


  1. tvoybro
    13.09.2021 17:24
    +1

    В первом же предложении какая-то нелепая жесть о "разрешении" в PAL и NTSC. Простите за резкость, но лучше возьмите талмуд по денди (вики nesdev подойдет) и изучите матчасть прежде чем писать мануалы снабжённые заблуждениями.

    UPD:
    Myth: The NTSC NES picture is only 224 pixels tall.

    Fact: The NTSC NES picture is 242 pixels tall: 240 lines of picture and 2 lines of vertical border. The PPU fetches and generates a signal for all 240 lines, even if TVs cut off the edges.

    https://wiki.nesdev.com/w/index.php/Myths


    1. himysay Автор
      13.09.2021 18:22

      1. tvoybro
        13.09.2021 20:03

        Ну, давайте вместе почитаем, что там написано:

        The NES PPU always generates a 256x240 pixel picture.

        Всё остальное уже качество кинескопа и системы развёртки.


  1. alex_231
    14.09.2021 02:00
    +2

    Флаг Z - это признак нуля, если результатом выполнения операции является ноль, то флаг устанавливается в 1, в остальных случаях он равен 0. Это относится ко всем командам, влияющим на этот флаг.

    И да, с такой пунктуацией текст очень сложно воспринимается.


    1. himysay Автор
      14.09.2021 10:42

      Большое спасибо за Ваш комментарий, экспериментально Я подошёл к этому но не стал писать дабы не ошибиться и не внести смуту )) Пунктуацию постараюсь поправить.