Pull to refresh

Разработка игр под NES на C. Главы 11-13. Пишем и отлаживаем простой платформер

Reading time 7 min
Views 7.5K
Original author: Nesdoug

В этой части появляется первая играбельная демка в стиле Марио. Для этого надо разобраться с прокруткой и способами отладки.


<<< предыдущая следующая >>>


image


Источник


Прокрутка


Регистр $2005 управляет прокруткой фона. Первая запись туда выставляет положение горизонтальной прокрутки, а вторая — вертикальной. Если неизвестно, какая прокрутка была выставлена, можно сбросить на горизонтальную чтением из регистра $2002.


99% игр используют только 2 таблицы имен — фактически, это фон экрана. Еще две доступных таблицы зеркалируют первые две. Эмулятор получает информацию о настройках таблиц из заголовка iNES-образа картриджа. Байты 6 и 7 описывают маппер — сопроцессор в картридже. Младший бит байта 6 описывает направление прокрутки. 0 — скролл по вертикали, таблиц имен зеркалируются по горизонтали. 1 — наоборот. В итоге мы получаем доступную для работы область 1х2 экрана (или 2х1, в зависимости от выбранной прокрутки) и скользящее по этой области окно, отрендеренное на телевизоре.


Игра Gauntlet использует четырехстороннюю прокрутку. Это требует 2k дополнительной RAM на картридже. Игры с маппером MMC3 могут переключать режимы прокрутки в середине игры. Но в большинстве случаев режим прокрутки единый для всей игры, и используется всего 2 таблицы имен.


В первом примере мы настроим горизонтальную прокрутку. Это выставляется в файле reset.s. Стрелки на джойстике будут двигать фон. Спрайтами реализован показометр положения фона: H для горизонтального сдвига и V для вертикального. Настоятельно рекомендую запустить этот образ в FCEUX и посмотреть дебаггером таблицы имен во время движения.


image


После пересечения $FF по горизонтали вызывается смена таблицы имен через обращение к регистру PPU_CTRL — он расположен по адресу $2000. Для пользователя это незаметно.


Для подготовки демки использовались такие инструменты: буквы рисовались в Фотошопе, потом индексировались в четырехцветное изображение и копипастились в YY-CHR. Затем их надо сохранить в chr-файл и открыть его в NES Screen Tool, скомпоновать фон, а потом экспортировать с RLE-сжатием как .h файл. Теперь его можно загрузить при запуске приставки. Движение персонажа реализовано через сдвиг фона, а позиция спрайта не меняется.


move_logic()
void move_logic(void) {
 if ((joypad1 & RIGHT) != 0){
   state = Going_Right;
   ++Horiz_scroll;
   if (Horiz_scroll == 0)
      ++Nametable;
 }
 if ((joypad1 & LEFT) != 0){
   state = Going_Left;
   --Horiz_scroll;
   if (Horiz_scroll == 0xff)
      ++Nametable;
 }
 Nametable = Nametable & 1; // меняет по кругу 0<->1
if ((joypad1 & DOWN) != 0){
   state = Going_Down;
   ++Vert_scroll;
   if (Vert_scroll == 0xf0)
      Vert_scroll = 0;
 }
 if ((joypad1 & UP) != 0){
   state = Going_Up;
   --Vert_scroll;
   if (Vert_scroll == 0xff)
      Vert_scroll = 0xef;
  }
 }

А при обновлении кадра происходит обновление спрайтов и выставление положения прокрутки:


every_frame()
void every_frame(void) {
OAM_ADDRESS = 0;
OAM_DMA = 2; // Спрайты пишутся в адреса $200-$2FF RAM
PPU_CTRL = (0x90 + Nametable); // экран и NMI включены
PPU_MASK = 0x1e;
SCROLL = Horiz_scroll;
SCROLL = Vert_scroll;  // выставляется положение прокрутки
Get_Input();
}

В втором примере прокрутка вертикальная, и таблица имен "закольцовывается" через левый и правый край экрана. Это выставлено все в том же reset.s. Для вертикальной прокрутки используются таблицы имен 0 и 2.


Максимальная позиция вертикальной прокрутки равна $EF, потому что экран высотой 240 пикселей. Это обрабатывается аналогично предыдущему примеру. Еще одно отличие — переключение таблиц имен из нулевой во вторую и обратно:


PPU_CTRL = (0x90 + (Nametable << 1));

Исходный код:
Дропбокс
Гитхаб


Простейший платформер


А сейчас будем делать демку с горизонтальной прокруткой и прыжками по платформам. Карты коллизий для 2 страниц фона будут храниться в памяти и займут там $200 байт.


Сначала сделаем гравитацию. Каждый кадр спрайты должны падать на (++Y), если они не стоят на платформе. Будем считать, что низ метаспрайта выравнен с фоном. Так что можно проверять, не провалились ли нижние углы метаспрайта в платформу:


Платформы и гравитация

// Сначала работаем с левым нижним углом метаспрайта
 // В какой мы таблице имен?
 NametableB = Nametable;
 Scroll_Adjusted_X = (X1 + Horiz_scroll + 3); // поправка на прозрачный левый край спрайта
 high_byte = Scroll_Adjusted_X >> 8;
 if (high_byte != 0){ // Если спрайт ушел дальше чем на 255 точек, то переходим на другую таблицу
  ++NametableB;   
  NametableB &= 1; // Она должна меняться 0<->1
 }
 // твердый ли метатайл по нашим координатам?
 collision_Index = (((char)Scroll_Adjusted_X>>4) + ((Y1+16) & 0xf0));
 collision = 0;
 Collision_Down(); // если это платформа, то делаем ++collision

// А теперь правый нижний угол
...точно так же, только (X1 + Horiz_scroll + 12); 

void Collision_Down(void){
 if (NametableB == 0){ // первая карта коллизий
  temp = C_MAP[collision_Index];
  collision += PLATFORM[temp];
 }
 else { // вторая карта коллизий
  temp = C_MAP2[collision_Index];
  collision += PLATFORM[temp];
 }
}
// Массив platform содержит нули и единицы
// и показывает, провалится ли спрайт сквозь нее

// гравитация
if(collision == 0){
   Y_speed += 2;
}
else {
   Y_speed = 0;
   Y1 &= 0xf0; // выровнять по границе метатайла
}

Дальше надо поработать над плавностью движений и прыжков. Понадобится много переменных для координат позиции спрайта и фона, скорости, ускорения и пару констант для максимально допустимых скоростей. Но я на это забил. В итоге скорость прокрутки хранится в старшем полубайте X_speed.
Horiz_scroll += (X_speed >> 4);
Обычно прокрутка фона начинается, когда персонаж приближается к краю экрана. А когда он в центральной части, то движется сам по себе со статичным фоном. Здесь такая техника не используется, опять же для упрощения. Возможно, когда-нибудь сделаю рефакторинг.


Исходный код:
Дропбокс
Гитхаб


Работа с нулевым спрайтом. Отладка


Sprite Zero Hit — это один из способов отследить событие в середине кадра, например изменение позиции горизонтальной прокрутки. Это позволит нам сделать статичный верх экрана, например счетчик очков, и прокрутку нижней части экрана.


Есть несколько способов реализации:


  1. Sprite Zero Hit
  2. Переполнение спрайтов (не надо так делать)
  3. Прерывание звукового процессора (и так тоже)
  4. Некоторые мапперы поддерживают счетчики строк (годится, если использовать MMC3)

Нам годится только первый способ — он самый простой и безглючный.


Нулевой спрайт хранится в OAM по адресам $0-$3. Если он содержит непрозрачный пиксель и этот пиксель отрисуется поверх непрозрачного пикселя фона, то в регистре $2002 выставится бит 0x40. Если же спрайт рисуется поверх прозрачного фона, то игра уходит в бесконечный цикл. Мы можем воспользоваться этим для настройки прокрутки. Процедура написана на Ассемблере.


Сначала сделаем все что надо в V-blank. Потом выставим в ноль горизонтальную прокрутку и включим нужную таблицу имен. Затем вызовем SpriteZero(), и она уйдет в ожидание события — отрисовки строки, где наложатся нужные пиксели. Потом мы можем переключить прокрутки и таблицу имен — это произойдет посреди отрисовки экрана.


// В обработчике NMI прокрутка и таблица имен обнулены - для верхней части экрана
Sprite_Zero(); // ждем события
SCROLL = Horiz_scroll;
SCROLL = 0;  // включаем прокрутку
PPU_CTRL = (0x94 + Nametable);

В нашем примере нулевой спрайт содержит символ нуля, просто для наглядности. И еще сделал, чтобы он исчезал при нажатии Start.


if ((joypad1 & START) > 0){
SPRITE_ZERO[1] = 0xff; // Подменяем спрайт на содержащий одну точку
SPRITE_ZERO[2] = 0x20; // Прячем его за фоном
}     

В первой версии туториала урок здесь и заканчивался, но потом решил расширить тему. Так что сейчас запилим демку с фоном шириной 4 экрана и динамической генерацией фона на метатайлах, без RLE-сжатия.


При перемещении персонажа на 16 пикселей демка будет дорисовывать 2 столбца тайлов за границей экрана, в нужную таблицу имен. Это можно уместить в V-blank. Для ускорения процедуру записи PPUupdate пришлось написать на Ассемблере и развернуть циклы. Таблица атрибутов фона тоже изменяется в ходе работы.


Получилось сложно и громоздко, и на отладку ушло много времени. Так что будет хороший пример, чтобы показать техники отладки.


Во-первых, реализация прокрутки медленная и не вкладывается во время. Чтобы понять это, пришлось вставить команду


PPU_MASK = 0x1F;

в main() перед ожиданием V-blank. Начиная с этого момента, экранные строки будут рендериться в черно-белом цвете. Этот хак совместим не со всеми эмуляторами, например в FCEUX надо включить опцию ‘old PPU’. Получилось вот так:


image


Половина доступного процессорного времени уже потрачена, и это без музыки и противников. Для профилирования функций сделал запись в переменную до и после выполнения функции, и включил в дебаггере отслеживание записи по адресу этой переменной. А FCEUX умеет считать такты процессора между остановами. Получилось как-то так:



TEST = *((unsigned char*)0xFF) // этот адрес почти никогда не занят
++TEST;
Should_We_Buffer(); // 4422 такта
++TEST; 

Оказалось, что тормозит работа с буфером. Ее можно разбить на две функции покороче, и выполнять их через кадр. Теперь загрузка процессора выглядит получше:


image


Дальше убираю прокрутку влево. Теперь хорошо бы реализовать, чтобы можно было побежать налево и упереться в край экрана. Сразу это не получилось, и отладка методом аналитического тупления в код ((с) DIHALT ) не помогла. Пришлось генерировать карту адресов. Для этого надо вызывать линковщик с опцией:
ld65 ... -Ln “labels.txt”
И компилятор с транслятором с опцией -g.


По этим файлам видно, что подозрительная функция move_logic() находится по адресу $C5B2, так что ставлю туда брейкпоинт. В принципе, можно расставить метки прямо в сишном коде и отключить оптимизацию, но я делал вызов пустой функции в нужном месте (движение персонажа влево) и отслеживал ее точное расположение по карте меток. Но перехват записи переменной компактней и удобней.


Отладку все равно пришлось делать по ассемблерному листингу, но неправильное сравнение ‘if (X_speed < 0)’ нашлось довольно быстро. В этом месте X_speed обнулялась даже если нажать Влево. Изменил сравнение на <=, и все стало хорошо.


В FCEUX для обработки джойстика с включенным отладчиком надо замапить опцию ‘auto-hold’ на кнопку клавиатуры и сначала включить холд, нажать Влево, и потом уже ставить брейкпоинт в отладчике.


Юзер Rainwarrior из Nesdev сделал, а я слегка подправил скрипт на Python, который конвертирует метки ca65 в файл для отладчика FCEUX. На вход он берет label.txt. Пример использования есть в мейкфайле и бат-файле в исходнике к уроку.


image


Несколько версий скрипта

Ссылка на скрипт(rainwarior разрешил использовать и распространять):
Дропбокс


Оригинальная версия, без моих правок:
Форум


Еще одна версия, пригодная для сборки бат-файлом:
Дропбокс


Теперь прокрутка на 4 экрана работает, но сложнее, чем я себе это представлял. Вариант В аналогичен, но в нем вырезана вся отладка. Рекомендую посмотреть таблицы имен отладчиком и разобраться, как работает прокрутка.
Дропбокс
Гитхаб


В некоторых случаях линкер может отказаться вносить все метки в карту, тогда надо добавить эту строку в каждый ассемблерный файл:
.debuginfo


Иначе придется добавлять -g при каждом вызове cc65 и ca65.

Tags:
Hubs:
+13
Comments 0
Comments Leave a comment

Articles