Портирование Dangerous Dave для NES-Dendy

в 7:43, , рубрики: C, dendy, Famicom, Gamedev, indie, Nes, геймдев, инди, инди-разработка, разработка игр
D:РаботаПревью1.png

Сравнение оригинала Dangerous Dave с портом для NES/Dendy

Тема игр из детства до сих пор тревожит умы очень многих людей, а возможность реализовать свои фантазии в виде игры для любимой консоли вообще взрывает мозг (особенно в контексте игры, которую вы увидели на картинке ☺). И в этой статье я расскажу вам о своём опыте портирования Dangerous Dave in the Haunted Mansion для NES/Famicom/Dendy.

Предисловие

Большинство читателей наверняка узнали первый экран известного шутер-платформера Dangerous Dave in the Haunted Mansion, который вышел в 1991-м году для DOS. Игра была очень популярна в 90-х в странах СНГ. Да и сейчас она отлично играется. Всем кто не пробовал, очень советую ознакомиться с классикой (в неё можно поиграть онлайн, но намного приятнее раскопать старый компьютер из актуальной игре эпохи и запустить на нём).

Официально Dangerous Dave портировался только на мобильные телефоны. Для консолей существуют лишь фанатские порты. Вот пример для консоли Sega Mega Drive. Я же решил пойти дальше и начал портирование Dangerous Dave 2 для консоли NES (в СНГ был популярен её клон Dendy). Назвал проект Fami Dave (Famicom Dangerous Dave). Началось всё с попытки вывести на экран Дейва (главного героя игры) в рамках ограничений графики NES, но в итоге вылилось в создание полноценной игры (демки на данном этапе).

Мой проект можно назвать портом только условно, так как весь код написан с нуля, а графика и геймплей заметно изменены. Эту разработку корректнее называть ремейком или ремастером, или вообще игрой, вдохновлённой Dangerous Dave 2 и другими классическими survival horror (Resident Evil, Silent Hill и т. д.). Графику пришлось упрощать, так как графические возможности NES не позволяют полностью повторить оригинальную картинку (например, в NES для спрайтов доступно всего четыре палитры по три цвета). А элементы survival horror добавил в игру, так как они хорошо вписываются в тематику и делают геймплей разнообразнее. 

Постановка задач

Dangerous Dave in the Haunted Mansion представляет собой 2D- платформер с плавным скроллингом по двум осям (это была основная фича игры, ради неё Кармак всё и затеял). Из этого я выделил следующие задачи:

  • графика, максимально близкая к оригиналу;

  • плавный горизонтальный скроллинг;

  • анимации смертей как в оригинале;

  • стрельба с перезарядкой;

  • возможность обыскивать и осматривать предметы;

  • отзывчивое управление и управляемые прыжки;

  • наличие инвентаря и меню записок;

  • полноценный сюжет с нелинейностью;

  • загадки;

  • наличие саундтрека и звуков окружения (стрельба, щелчки механизмов, шаги, шум врагов и т. д.);

  • использование дешёвого и простого в производстве маппера;

  • выпуск игры на физических носителях.

Графика

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

D:РаботаMS-DOS - Dangerous Dave in The Haunted Mansion - Dangerous Dave.png

А вот пример раскадровки анимаций врагов:

D:РаботаMS-DOS - Dangerous Dave in The Haunted Mansion - Enemies.png

Тут уже кадров поменьше, но всё ещё много.

В оригинале используются готовые зеркальные спрайты, но NES умеет аппаратно отражать изображения по вертикали и горизонтали, что позволяет сэкономить видеопамять (4 килобайта  для спрайтов (256 тайлов 8х8 пикселей) и 4 килобайта для фонов). 

Из-за ограниченного количества видеопамяти NES решил сократить количество кадров на врага до двух, а на Дейва — до семи кадров. Поставил задачу использовать такие цвета, чтобы Дейв был максимально похож на оригинал. В итоге получился следующий адаптированный набор спрайтов:

Как вам адаптированная бабка? В динамике она ещё круче!

Как вам адаптированная бабка? В динамике она ещё круче!

Дейв значительно упростился, но явно похож (напишите в комментарии, если не согласны ☺). Два кадра для врага маловато, но если удачно нарисовать, получается вполне неплохо. Во многих играх эпохи NES обходились вообще одним кадром, а боссов часто рисовали фонами (фоны и спрайты в NES это отдельные сущности).

Для рисования спрайтов и фонов используется программа YY-CHR. Она очень хороша для создания графики для старых консолей (NES, SNES, SEGA и т. д.), позволяет делать все операции, которые могут прийти в голову. Очень мощная вещь. Основной недостаток программы в том, что интерфейс устаревший и его нельзя масштабировать. На больших экранах окно мелкое, неудобно работать. В остальном претензий у меня к нет, можно даже импортировать подготовленные BMP-файлы (диапазон цветов нужно ограничить до четырёх и выставить разрешение не более 128х128 пикселей).

Теперь обсудим особенности спрайтов в играх для NES, чтобы понять, почему у меня Дейв получился именно таким. Как я писал выше, видеопамять консоли составляет всего 8 килобайта: по 4 на спрайты и фоны. Вся графика состоит из тайлов 8х8 пикселей (и фоны, и спрайты), это нужно было для реализации аппаратного скроллинга (Карман в оригинальном Дейве тоже использовал систему готовых тайлов). Каждый бит тайла кодируется двумя битами, то есть может иметь четыре цвета. Вот так выглядит общая палитра NES:

image

Набор цветов не слишком радует разнообразием, но, кроме этого, каждый тайл или спрайт метаспрайта (изображения, собранного из отдельных тайлов) может использовать только 4 цвета. Такой набор цветов называется палитрой. Одновременно можно использовать всего 4 палитры, при этом первый цвет палитры обозначает дырку (прозрачный пиксель, через него виден фон). Итого, мы на метаспрайт имеем всего 12 цветов (и не забывайте, что в рамках одного блока 8х8 пикселей можно использовать только 3 цвета + прозрачные пиксели). Это ограничение можно частично обойти с помощью переключения палитр прямо во время отрисовки кадра (во время возврата луча ЭЛТ к началу следующей строки), но такой финт требует использования маппера, поддерживающего прерывания по событию «конец строки» (например, MMC3). Вот пример метаспрайта:

image

Структура метаспрайта Марио из игры Super Mario Bros.

На рисунке видно, что маленький Марио состоит всего из четырёх спрайтов/тайлов, но при этом все детали его внешности отлично читаются (пиксельарт — довольно специфическое направление изобразительного искусства, особенно в рамках ограничений NES). 

Ещё нужно учитывать, что одновременно на экран можно вывести всего 64 спрайта (есть также режим вывода тайлов размером 8х16, тогда максимальное количество спрайтов на экране удваивается), при этом на одной строке может быть всего 8 спрайтов, следующие просто не будут отображаться. Это значит, что если вы хотите вывести 3 врага шириной 3 спрайта каждый, то у последнего врага крайний справа ряд спрайтов исчезнет (эти тайлы будут девятыми на строку). Вот пример такого исчезновения (у парня в чёрной майке пропал верх торса):

Скриншот из игры Double Dragon III

Скриншот из игры Double Dragon III

Эту проблему можно обойти с помощью эффекта стробоскопа. В той же Double Dragon III, когда спрайтов на строку становится больше 8, некоторые тайлы метаспрайтов начинают отображаться поочерёдно (мерцать), что создает некоторый эффект прозрачности, но позволяет выводить больше 8 спрайтов на строку.

Подробнее о структуре видеопамяти и работе со спрайтами можно ознакомиться здесь (хороший цикл переводных статей про разработку игр для NES на языке Си).

Теперь поговорим о фонах. Разрешение экрана в NES составляет 256×224 (32 х 28 тайлов) для NTSC-версий консоли и 256×240 (32 х 30 тайлов) для PAL-консолей. Кроме разрешения, NTSC- и PAL-консоли отличаются частотой кадров: NTSC обеспечивают 60 кадров в секунду, а PAL — всего 50. Поэтому на NTSC играть приятнее, так как картинка более плавная, но геймплей и музыка немного быстрее. Наши Dendy были в основном PAL-консолями (но процессор работал на частоте NTSC-версий для совместимости с играми NTSC-региона). Хотя были и уникальные примеры SECAM-консолей, которых среди официальных моделей вообще не существовало.

Далее мы будем ориентироваться на NTSC-формат. Это значит, что фон у нас представлен таблицей размером 32х28 тайлов. То есть один экран представляет собой таблицу, каждая ячейка которой хранит номер тайла (от 0 до 255), записанного в видеопамять. Такая таблица называется таблицей имён, всего их четыре. Каждая таблица хранит информацию о фонах на одном экране. Такое решение позволяет реализовать плавный скроллинг с помощью плавного перемещения объектива игровой камеры с одной таблицы на другую. Если представить таблицы имён расположенными на координатной плоскости, то первая таблица будет располагаться в левой верхней четверти, вторая — в правой верхней, и т. д. При этом объектив игровой камеры представляет собой рамку 32х28 тайла, которая выводит на экран те тайлы, которые расположены под ней.

В качестве иллюстрации здесь можно представить разворот обычной школьной тетради (два целых листа с одной общей стороной — наши таблицы имен), на который положили сверху одиночный тетрадный лист (он означает объектив камеры). И, перемещая этот лист по поверхности разворота тетради, на экран будет выводиться всё то, что расположено под этим тетрадным листом в данный момент времени. На практике это выглядит вот так:

D:Работаd6f68d6f7f1e05516af8dcae1443bcb9.gif

На рисунке объектив камеры показан красной рамкой, он перемещается вдоль верхнего края экрана. Если требуется вывести уровень шириной больше двух экранов, то приходится в реальном времени переписывать таблицу имён в невидимой части экрана возле края рамки, чтобы при перемещении объектива можно было вывести новые части фонов уровня игры.

Вот пример заполнения таблицы имён в моей игре:

Портирование Dangerous Dave для NES-Dendy - 9

На рисунке показано содержимое таблицы имён. Видно, что комната состоит из двух экранов. Так как большая часть игры происходит в замкнутых пространствах, я решил ограничиться двумя экранами, доступными без загрузки. 

Реализация скроллинга с изменением таблицы имён в реальном времени намного сложнее, особенно, если реализовать перемещение объектива камеры и влево, и вправо (в играх NES часто нельзя вернуться назад, если вы проскроллили экран). А так выглядит плавный скроллинг в в моей игре:

Игрок идёт через всю комнату, и камера перемещается за ним. Вся комната занимает ровно две таблицы имён.

Ещё одна важная проблема при скроллинге — это вывод интерфейса. Если он выводится с помощью фонов, то проскроллив экран, интерфейс останется за экраном. Реализация интерфейса при скроллинге имеет довольно много тонкостей, о которых почему-то мало кто пишет, поэтому расскажу об этом в одной из следующих статей.

Кроме скроллинга и заполнения таблицы имён требуется раскрашивать фон, то есть устанавливать палитры для тайлов фона. Для этого существует специальная таблица атрибутов для каждой таблицы имён. Основная проблема в установке палитр фонов заключается в том, что палитру можно установить только для блока 2х2 тайла (16х16 пикселей), отдельному тайлу задать свою палитру нельзя. Это ещё сильнее усложняет рисование фонов, при том, что для фонов можно использовать всего 4 палитры по 3 цвета (в фонах нулевой цвет — это тоже прозрачный пиксель, который заполняется цветом заливки фона, а цвет заливки фона для всего экран один).

Причём таблица атрибутов таблицы имен имеет неочевидную структуру: один байт кодирует цвета сразу четырёх блоков (далее блоком будем называть блок тайлов 2х2), то есть участок фона 32х32 пикселя. Кодирование происходит следующим образом:

image

Младшие два бита кодируют номер палитры для блока в верхнем правом углу, следующие два бита кодируют правый верхний блок, и т. д. В итоге имеем 64 байта атрибутов для каждой таблицы имён.

Ориентируясь на все ограничения, которые приведены выше, я нарисовал все фоны для игры Fami Dave. Пример фонов начального уровня:

Портирование Dangerous Dave для NES-Dendy - 11

На рисунке показан набор из 256 тайлов, из которых собирается весь нулевой уровень игры. В один момент времени доступен для отрисовки фонов только один такой набор. Это значит, что вывести изображение больше, чем 128х128 пикселей, не используя тайлы по несколько раз, не получится. Это очень сильно ограничивает вывод изображений на весь экран. 

Это ограничение обходится с помощью повторного использования тайлов (в рамках одного экрана) или динамического переключения банков видеопамяти (так ещё часто делают анимации фонов). Динамическое переключение банков видеопамяти достигается за счет того, что видеопамять представляет собой отдельную микросхему памяти на картридже ёмкостью в 8 килобайтов. Если установить микросхему видеопамяти емкостью от 16 килобайтов, то, переключая старшие адреса, можно в реальном времени переключать доступное для процессора адресное пространство. Такое переключение банков видеопамяти достигается с помощью мапперов. О них мы поговорим ниже.

Примеры реализации вывода фонов и спрайтов на экран на языке Си с компилятором СС65 я показывал в статье о разработке своей первой игры для NES (ту игру я немного улучшил, но в текущем виде она мне не нравится, там значительную часть кода надо переписывать и переделывать архитектуру проекта). В Fami Dave работа с графикой ничем принципиально не отличается, если не считать добавление скроллинга и использование маппера (в прошлой игре я обходился без него). 

Детальное описание реализации скроллинга требует ещё одной полноценной статьи, но для этого мне надо подготовить хороший пример реализации с динамическим редактированием таблицы имён (там тоже есть несколько подходов, особенно интересен используемый в Battletoads). Возможно, продвинутый скроллинг мне понадобится в одном из следующих проектов.

Анимации

Ещё одной важной фишкой оригинальной Dangerous Dave in the Haunted Mansion было наличие красочных анимаций, которые выводились при смерти игрока. Мне идея таких анимаций очень нравится, так как позволяет добавить мелкие детали в игру, в которой все персонажи нарисованы очень условно, а анимации позволяют выводить события крупным планом и в динамике. Вот раскадровки оригинальных анимаций:

Cowboy(UA) Site - Interesting materials for "Dangerous Dave" - Page 3

Один кадр оригинальной анимации имеет разрешение 40х42 пикселей. Немного, но за счёт большого количества доступных цветов они выглядят красиво и читаемо.

В условиях графических ограничений NES проблематично нарисовать что-то внятное в таком небольшом разрешении, поэтому я решил сделать один кадр анимации размером 8х8 тайлов, то есть 64х64 пикселя. 4 кадра анимации как раз занимали одну полную страницу видеопамяти спрайтов. Анимации рисовал спрайтами для достижения большей детализации (в спрайтах можно задать палитру каждому отдельному тайлу, а не блоку). В играх эпохи NES такие решения практически не применяли, так как полноценные многокадровые анимации потребляют очень много памяти (1 килобайт на кадр в моём случае). Реализация красивых и крупных анимаций была бы непозволительной роскошью, а картриджи старались сделать максимально дешёвыми, поэтому экономили каждый байт.

Вот так выглядит одна анимация в памяти картриджа:

Пример раскадровки анимации из игры Fami Dave

Пример раскадровки анимации из игры Fami Dave

А вот так выглядит эта анимация в динамике в Fami Dave:

В игре есть более яркие и интересные анимации, чем открытие двери, но не уверен, насколько они были бы уместны на Хабре ☺. В конце статьи будет демонстрация геймплея и ссылка для скачивания демо-версии игры, там вы сможете всё попробовать и посмотреть лично.

Вывод анимации ничем не отличается от вывода обычных метаспрайтов персонажей. Из нововведений — пришлось написать функцию задержки, которая позволяет выставить продолжительность каждого кадра. Вот код функций вывода анимации и задержки:

Hidden text
// Функция вывода анимации
void start_animation (void) {
    Wait_Vblank ();
    Reset_Scroll ();
    // Банк с информацией об анимациях (карты коллизий  и т. д.)
    bank_table [ANIME_INFO_BANK] = ANIME_INFO_BANK;

	temp2 = 0x00; // Выбирает номер кадра анимации
	// Задаём атрибуты анимаций + выбор кадра
	p_metasprite_attributes = anime_attributes [art_number] + 0;
	draw_animation_frame (); // кадр 0
// Установка длительности кадра
// Время каждого кадра прописывается в файле конфигурации анимации
	delay_value = anime_delays [art_number * 4 + 0];
	delay_in_frames ();

	temp2 = 0x08;
	p_metasprite_attributes = anime_attributes [art_number] + 64;
	draw_animation_frame (); // кадр 1
	delay_value = anime_delays [art_number * 4 + 1];
	delay_in_frames ();

	temp2 = 0x80;
	p_metasprite_attributes = anime_attributes [art_number] + 128;
	draw_animation_frame (); // кадр 2
	delay_value = anime_delays [art_number * 4 + 2];
	delay_in_frames ();

	temp2 = 0x88;
	p_metasprite_attributes = anime_attributes [art_number] + 192;
	draw_animation_frame (); // кадр 3

	// Задерживаем последний кадр
	delay_value = anime_delays [art_number * 4 + 3];
	delay_in_frames ();

    // Включаем фоны
    PPU_MASK = PPU_MASK_BG_ON_MODE;
    // Возвращаем базовый банк
    bank_table [0] = 0;
}

// Функция вывода одного кадра анимации
void draw_animation_frame (void) {
    oam_counter = 0;
    temp_x = 0;
    temp_y = 0;
    temp = 0; // хранит номер текущего тайла для вывода
    for (j = 0; j < 64; ++j ) {
        SPRITES[oam_counter] = Y + temp_y;
        ++oam_counter;
        // temp2 — задаёт выбор кадра
        // temp2 = 0 (0 кадр)/temp2 = 0x08 (1 кадр)/
        // temp2 = 0x80 (2 кадр)/temp2 = 0x88 (0 кадр)/
        SPRITES[oam_counter] = temp + temp2; 
        ++oam_counter;
        SPRITES[oam_counter] = p_metasprite_attributes [j];
        ++oam_counter;
        SPRITES[oam_counter] = X + temp_x; 
        ++oam_counter;

	  // Вычисляем относительное положение каждого тайла анимации
        // Изменяем относительное положение текущего тайла
        temp_x += 8;
        ++temp;
        if (temp_x >= 64) {
            temp_x = 0;
            temp_y += 8;
            // Переходим на следующую строчку тайлов
            temp -= 8;
            temp += 0x10;
        }
    }
}

// Функция задержки
// Задержка устанавливается в кадрах
// 1 кадр = 1/60 секунды
void delay_in_frames (void) {
    temp2 = delay_value;
    while (temp2) {
        --temp2;
        Wait_Vblank ();
    }
}

Каждая анимация хранится в выделенных для них банках памяти. В моём случае под анимации выделено 2 банка по 16 килобайтов. В 32 килобайта поместится 8 анимаций. Это не слишком рационально, но анимации того стоят (или нет?). Их можно было бы попробовать сжать, но картинки плохо сжимаются, поэтому я не стал заморачиваться с этим.

Файлы анимаций формируются с помощью программы YY-CHR. Каждый файл содержит по 2 анимации, то есть 8 килобайтов, что равняется стандартному размеру видеопамяти. Подключаются они в файле инициализации следующим образом:

; Анимации по 4 кадра 8х8 тайлов (Выводятся через спрайты)
.segment "CHR_TILESET_0_ANIME"
	.incbin "CHR/anime/anime_0.chr" ; Роскомнадзор и открытие двери
.segment "CHR_TILESET_0_ANIME"
	.incbin "CHR/anime/anime_1.chr" ; Смерть от зомби и Дейв с лого
.segment "CHR_TILESET_1_ANIME" ; Банк 7
	.incbin "CHR/anime/anime_2.chr" ; Смерть от босса лвл1 и пустота

Список сегментов прописывается в файле конфигурации:

Hidden text
# Разметка реальной памяти консоли на блоки
MEMORY {
#RAM Addresses:
    # Zero page (Нулевая страница), часть адресов используется консолью, все 255 байт использовать не получится
    ZP: start = $0000, size = $100, type = rw, define = yes;
	
    # Здесь хранится копия табоицы ОАМ (таблица информации о всех спрайтах - 64 штуки)
    # 4 байта на один спрайт
	OAM_RAM: start = $0200, size = $0100, define = yes;
	# ОЗУ для общего пользования - 1024 байта
	RAM: start = $0300, size = $0400, define = yes;

#INES Header:
    # Эта часть памяти используется для заголовка INES, который нужен для работы эмулятора
    HEADER: start = $0, size = $10, file = %O ,fill = yes;

#ROM Addresses:
    # Количество банков должно совпадать с количеством банков указнном в iNES заголовок 
    # Переключаемые банки по 16 килобайт ($4000)
    PRG0: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG1: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG2: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG3: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG4: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG5: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG6: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG7: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG8: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRG9: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRGA: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRGB: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRGC: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRGD: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    PRGE: start = $8000, size = $4000, file = %O ,fill = yes, define = yes;
    
    # Используется половина PRG ROM
    PRGF: start = $c000, size = $3ffa, file = %O ,fill = yes, define = yes;

    # Hardware Vectors at end of the ROM (тут хранятся адреса обработчиков прерываний, всего 3 прерывания)
    VECTORS: start = $fffa, size = $0006, file = %O, fill = yes;
}
# Тут объявляются сегменты кода и прикрепляются к реальным участкам памяти которые описаны в блоке MEMORY
# Так же указываются режимы работы этих сегментов, смотрите документацию компилятора сс65
SEGMENTS {
    HEADER:   load = HEADER,         type = ro;

    STARTUP:  load = PRGF,            type = ro,  define = yes;
    LOWCODE:  load = PRGF,            type = ro,                optional = yes;
    INIT:     load = PRGF,            type = ro,  define = yes, optional = yes;

    BANK0:     load = PRG0,            type = ro,  define = yes;
    # Хранит инфу для вывода анимаций (константные массивы)
    BANK1:     load = PRG1,            type = ro,  define = yes;

    # Храним 4 тайлсета по 4 килобайта в банке 1
    CHR_TILESET_0_BG: load = PRG2, type = ro,  define = yes,  offset = $0000;

    # Каждый банк хранит 4 тайлсета по 4 килобайта
    CHR_TILESET_0_SPRITES: load = PRG4, type = ro,  define = yes,  offset = $0000;

    # Хранит 4 набора тайлов для вывода анимаций
    CHR_TILESET_0_ANIME: load = PRG6, type = ro,  define = yes,  offset = $0000;
    CHR_TILESET_1_ANIME: load = PRG7, type = ro,  define = yes,  offset = $0000;

    # Хранит 4 набора тайлов для вывода артов
    CHR_TILESET_0_ARTS: load = PRG8, type = ro,  define = yes,  offset = $0000;

    #Хранит тайлсет босса для 0 уровня и массивы 0 уровня
    LVL_0_INFO: load = PRGA, type = ro,  define = yes,  offset = $0000;
    LVL_1_INFO: load = PRGB, type = ro,  define = yes,  offset = $0000;

    # Самый важный код, который должен храниться в фиксированном банке
    CODE:      load = PRGF,            type = ro,  define = yes;

    RODATA:    load = PRGF,            type = ro,  define = yes;
    RW_PRG:    load = PRGF,            type = rw,  define = yes;
    #run = RAM, это означает, что данные или код, которые загружаются в PRG ROM (load = PRGF),
    #будут копироваться и выполняться из RAM при запуске программы.
    DATA:     load = PRG0, run = RAM, type = ro,  define = yes;

    ONCE:     load = PRGF,            type = ro,  define = yes;

    # Векторы прерываний
    VECTORS:  load = VECTORS,        type = rw;
    
    # Оперативка
    BSS:      load = RAM,            type = bss, define = yes;
    HEAP:     load = RAM,            type = bss, optional = yes;
    ZEROPAGE: load = ZP,             type = zp;
	OAM:	  load = OAM_RAM,			 type = bss, define = yes;	
}

Я обязательно сделаю проект по быстрому началу разработки игр для NES с подробной инструкцией по файлу конфигурации, там тоже очень много тонкостей, а документация не слишком подробная.

В представленном выше коде, думаю, всё понятно. В файле конфигурации я описываю сегменты и прикрепляю их к физическим банкам памяти. Затем в коде прикрепляю бинарные файлы анимаций к нужным сегментам. Сборщик компилятора компонует все эти файлы по указанным адресам в общий ROM-файл, который уже можно запустить в эмуляторе или закинуть на флеш-картридж. Всё просто.

Мапперы

Раз мы заговорили про банки памяти, самое время поговорить о мапперах — электронных схемах, которые позволяют управлять памятью и расширяют возможности консоли. Мапперов существует очень много видов, их разрабатывали под разные нужды (переключение банков памяти, ОЗУ вместо ПЗУ для видеопамяти, улучшенные звуковые чипы и т. д.).

Самый распространённый маппер, и при этом достаточно навороченный, — это MMC3 (умеет вызывать прерывания на любой строке при отрисовке кадра, реализует скроллинг по двум осям и многое другое). Этот маппер всем хорош, кроме того, что он реализован на специальной ASIC-микросхеме от компании Nintendo. Её проблематично купить сейчас, так как производство прекращено (можно реализовать маппер на FPGA, но это требует дополнительного этапа разработки и сделает картридж значительно дороже). А так как планируется выпустить игру на физических носителях, для меня важна итоговая цена и сложность сборки одного картриджа с игрой. Изучив все простые мапперы, я остановился на UNROM, который позволяет использовать переключение банков памяти программ (фиксированный банк памяти 16 килобайтов и до 15 переключаемых банков) и установить ОЗУ вместо ПЗУ в качестве видеопамяти. А реализуется маппер всего на паре микросхем логики. Подробно о маппере UNROM я рассказывал здесь. Вот так выглядит его схема:

http://elektropage.ru/cartmod/nes/nes_cart/FC_UxROM_Schematics.png

Прочая информация о проекте

На данном этапе основные силы брошены на проработку геймплея и графики, а музыка в игре находится в зачаточном состоянии (добавлены только звуки выстрелов). Поэтому я и не стал писать отдельную главу о ней. Но в одной из следующих статей о музыке мы тоже поговорим. Звукоизвлечение в NES — тема очень интересная (есть мысли вывести средствами консоли небольшой мультик со звуком, это вполне реально, пусть и в адаптированной графике с низкой частотой кадров).

Теперь стоит немного сказать о геймплее, структуре проекта и процессе разработки.

Игра представляет собой набор уровней (сейчас в демо-версию добавлены только два). Каждый полностью открыт для исследования, но при переходе на следующий уровень вернуться назад уже нельзя.

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

Оригинальная игра ограничивала действия игрока стрельбой и платформингом, я же пошёл дальше. В Fami Dave игрок может не только прыгать и стрелять, но и взаимодействовать со всеми объектами окружения: осматривать предметы, обыскивать ящики, нажимать на рычаги, собирать предметы и так далее.

Ещё одним нововведением является инвентарь. Он позволяет использовать предметы, менять тип патронов (будет реализовано позже), читать собственный дневник и записки. Я также постарался добавить полноценный нелинейный сюжет в игру (записки и дневник в игре не просто так). Найденные записки будут помогать решать загадки, принимать сюжетные решения и погружать игрока в сюжет. В игре даже есть простенький стелс-режим и секретные комнаты ☺.

Теперь об инструментах разработки. Инструментарий для разработки используется небольшой:

  • fceux.com — эмулятор для отладки игры;

  • YY-CHR — программа для рисования пиксельной графики в формате NES;

  • NEXXT — программа для сборки уровней из готовых тайлов (тайлы в программу загружаются в виде .chr файла, который создается в YY-CHR);

  • Tiled — программа для построения карты коллизий фонов уровня (готовых решений для NES я не нашёл, поэтому пришлось адаптировать Tiled);

  • GIMP — все задачи по обработке изображений;

  • СС65 — компилятор языка С для консоли NES;

  • Visual Studio Code — редактирование кода.

Примеры использования представленного инструментария я давал в прошлой статье. А вот запуск игры на реальной Dendy из 90-х: 

Портирование Dangerous Dave для NES-Dendy - 15

Сейчас игра находится на этапе демо-версии, но дальнейшая разработка будет происходить быстрее, так как основная часть движка уже написана. Нужно только добавлять контент и некоторые механики.

Разработка игры началась в декабре 2023, а полноценная демо-версия появилась только в конце июля 2024. Проект развивается не слишком быстро, так как сейчас за студией SwampTech скрывается всего лишь два разработчика (я, как программист, и художник). Бюджета у игры нет, всё на чистом энтузиазме и любви к старым играм.

Вот планы по развитию проекта:

  • реализовать прокачку ружья (разные патроны, размер магазина и т. д.);

  • новые уровни;

  • новые звуки (шаги Дейва, щелчки механизмов, крики монстров и т. д.);

  • полноценный саундтрек;

  • новые типы загадок;

  • проработка ЛОРа игры (основным вдохновителем является Лавкрафт);

  • расширение бестиария игры;

  • выпуск игры на физических носителях с коробкой и красивым мануалом.

А теперь немного геймплея для тех, кто дочитал до конца ☺.

И видео с более подробным рассказом о истории разработки и больше подробностей геймплея игры:

Полезные ссылки

Автор: Swamp_Dok

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js