В этой части появляется первая играбельная демка в стиле Марио. Для этого надо разобраться с прокруткой и способами отладки.
<<< предыдущая следующая >>>
Прокрутка
Регистр $2005 управляет прокруткой фона. Первая запись туда выставляет положение горизонтальной прокрутки, а вторая — вертикальной. Если неизвестно, какая прокрутка была выставлена, можно сбросить на горизонтальную чтением из регистра $2002.
99% игр используют только 2 таблицы имен — фактически, это фон экрана. Еще две доступных таблицы зеркалируют первые две. Эмулятор получает информацию о настройках таблиц из заголовка iNES-образа картриджа. Байты 6 и 7 описывают маппер — сопроцессор в картридже. Младший бит байта 6 описывает направление прокрутки. 0 — скролл по вертикали, таблиц имен зеркалируются по горизонтали. 1 — наоборот. В итоге мы получаем доступную для работы область 1х2 экрана (или 2х1, в зависимости от выбранной прокрутки) и скользящее по этой области окно, отрендеренное на телевизоре.
Игра Gauntlet использует четырехстороннюю прокрутку. Это требует 2k дополнительной RAM на картридже. Игры с маппером MMC3 могут переключать режимы прокрутки в середине игры. Но в большинстве случаев режим прокрутки единый для всей игры, и используется всего 2 таблицы имен.
В первом примере мы настроим горизонтальную прокрутку. Это выставляется в файле reset.s. Стрелки на джойстике будут двигать фон. Спрайтами реализован показометр положения фона: H для горизонтального сдвига и V для вертикального. Настоятельно рекомендую запустить этот образ в FCEUX и посмотреть дебаггером таблицы имен во время движения.
После пересечения $FF по горизонтали вызывается смена таблицы имен через обращение к регистру PPU_CTRL — он расположен по адресу $2000. Для пользователя это незаметно.
Для подготовки демки использовались такие инструменты: буквы рисовались в Фотошопе, потом индексировались в четырехцветное изображение и копипастились в YY-CHR. Затем их надо сохранить в chr-файл и открыть его в NES Screen Tool, скомпоновать фон, а потом экспортировать с RLE-сжатием как .h файл. Теперь его можно загрузить при запуске приставки. Движение персонажа реализовано через сдвиг фона, а позиция спрайта не меняется.
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;
}
}
А при обновлении кадра происходит обновление спрайтов и выставление положения прокрутки:
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 — это один из способов отследить событие в середине кадра, например изменение позиции горизонтальной прокрутки. Это позволит нам сделать статичный верх экрана, например счетчик очков, и прокрутку нижней части экрана.
Есть несколько способов реализации:
- Sprite Zero Hit
- Переполнение спрайтов (не надо так делать)
- Прерывание звукового процессора (и так тоже)
- Некоторые мапперы поддерживают счетчики строк (годится, если использовать 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’. Получилось вот так:
Половина доступного процессорного времени уже потрачена, и это без музыки и противников. Для профилирования функций сделал запись в переменную до и после выполнения функции, и включил в дебаггере отслеживание записи по адресу этой переменной. А FCEUX умеет считать такты процессора между остановами. Получилось как-то так:
TEST = *((unsigned char*)0xFF) // этот адрес почти никогда не занят
++TEST;
Should_We_Buffer(); // 4422 такта
++TEST;
Оказалось, что тормозит работа с буфером. Ее можно разбить на две функции покороче, и выполнять их через кадр. Теперь загрузка процессора выглядит получше:
Дальше убираю прокрутку влево. Теперь хорошо бы реализовать, чтобы можно было побежать налево и упереться в край экрана. Сразу это не получилось, и отладка методом аналитического тупления в код ((с) 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. Пример использования есть в мейкфайле и бат-файле в исходнике к уроку.
Теперь прокрутка на 4 экрана работает, но сложнее, чем я себе это представлял. Вариант В аналогичен, но в нем вырезана вся отладка. Рекомендую посмотреть таблицы имен отладчиком и разобраться, как работает прокрутка.
Дропбокс
Гитхаб
В некоторых случаях линкер может отказаться вносить все метки в карту, тогда надо добавить эту строку в каждый ассемблерный файл:
.debuginfo
Иначе придется добавлять -g при каждом вызове cc65 и ca65.